Spring + Hikari的一个SQLFeatureNotSupportedException问题

1.
题记:
遇到个使用Spring xml方式配置Hikari,因为一个Bean的命名原因导致爆出 SQLFeatureNotSupportedException,追溯原因过程其实很简单,但是找到问题所在确实浪费不少时间。
2.
先看源码。
由于某些原因,只能使用 Spring XML方式配置 Hikari的 datasource,我自己使用jdk 1.7版本,不过粗看了下适用1.8+的Hikari代码,应该也是存在这个问题的。

参考Hikari官方的参数配置,如果在你的xml里这么配置 Hikari的 datasource

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"></property>
</bean>
<bean id="dataSource" class="com.zaxxer.hikari.HikariDataSource" destroy-method="close">
<property name="driverClassName" value="com.mysql.jdbc.Driver" />
<property name="jdbcUrl" value="${jdbc.url}" />
<property name="username" value="${jdbc.username}" />
<property name="password" value="${jdbc.password}" />
<property name="maximumPoolSize" value="20" />
<property name="minimumIdle" value="2" />
<!-- <property name="dataSource" > <null/></property> -->
<property name="connectionTestQuery" value="select 1 " />
<property name="dataSourceProperties">
<props>
<prop key="cachePrepStmts">true</prop>
<prop key="prepStmtCacheSize">250</prop>
<prop key="prepStmtCacheSqlLimit">2048</prop>
<prop key="useServerPrepStmts">true</prop>
</props>
</property>
</bean>

那么,当你的程序运行到到从该dataSource取connection时候,很可能会遇到下面这样的异常:

1
2
3
4
5
6
7
8
9
10
11
Caused by: java.sql.SQLFeatureNotSupportedException
at com.zaxxer.hikari.HikariDataSource.getConnection(HikariDataSource.java:119)
at com.zaxxer.hikari.pool.PoolBase.newConnection(PoolBase.java:341)
at com.zaxxer.hikari.pool.PoolBase.newPoolEntry(PoolBase.java:193)
at com.zaxxer.hikari.pool.HikariPool.createPoolEntry(HikariPool.java:428)
at com.zaxxer.hikari.pool.HikariPool.checkFailFast(HikariPool.java:499)
at com.zaxxer.hikari.pool.HikariPool.<init>(HikariPool.java:112)
at com.zaxxer.hikari.HikariDataSource.getConnection(HikariDataSource.java:97)
at org.springframework.jdbc.datasource.DataSourceUtils.doGetConnection(DataSourceUtils.java:111)
at org.springframework.jdbc.datasource.DataSourceUtils.getConnection(DataSourceUtils.java:77)
... 7 more

即这个dataSource不能使用。
我先说一下解决办法:
这个异常其实是 id=”dataSource”/ref=”dataSource” 导致的,如果改一下名字,任何非 dataSource的名字,就会神奇的发现,程序正常运行了。
3.
原因呢?
HikariDataSource.java

1
2
3
4
5
6
/** {@inheritDoc} */
@Override
public Connection getConnection(String username, String password) throws SQLException
{
throw new SQLFeatureNotSupportedException();
}

发现这里并没有错,的的确确该抛异常了,所以,正常的 getConnection()不是在这个类/方法里。
实际上,正常是在 Hikari的 DriverDataSource 这个类里

1
2
3
4
5
6
7
8
9
10
11
@Override
public Connection getConnection() throws SQLException
{
return driver.connect(jdbcUrl, driverProperties);
}
@Override
public Connection getConnection(String username, String password) throws SQLException
{
return getConnection();
}

那么是什么导致 DataSource使用了错误的实现类?
从HikariDataSource构造函数入口,到 getConnection 看起,追踪到 -> HikariPool(this) -> PoolBase(final HikariConfig config) -> initializeDataSource()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
private void initializeDataSource()
{
final String jdbcUrl = config.getJdbcUrl();
final String username = config.getUsername();
final String password = config.getPassword();
final String dsClassName = config.getDataSourceClassName();
final String driverClassName = config.getDriverClassName();
final Properties dataSourceProperties = config.getDataSourceProperties();
DataSource dataSource = config.getDataSource();
if (dsClassName != null && dataSource == null) {
dataSource = createInstance(dsClassName, DataSource.class);
PropertyElf.setTargetFromProperties(dataSource, dataSourceProperties);
}
else if (jdbcUrl != null && dataSource == null) {
dataSource = new DriverDataSource(jdbcUrl, driverClassName, dataSourceProperties, username, password);
}
if (dataSource != null) {
setLoginTimeout(dataSource);
createNetworkTimeoutExecutor(dataSource, dsClassName, jdbcUrl);
}
this.dataSource = dataSource;
}

这里似乎看起来正常,但是,如果我们仔细看 DataSource dataSource = config.getDataSource(),也就是说 HikariConfig 其实有 get/setDataSource属性
再看看我们xml里定义的bean,bean id=”dataSource” class=”com.zaxxer.hikari.HikariDataSource”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class HikariDataSource extends HikariConfig implements DataSource, Closeable{
....
}
public class HikariConfig implements HikariConfigMXBean
{
...
private DataSource dataSource;
/**
* Set a {@link DataSource} for the pool to explicitly wrap. This setter is not
* available through property file based initialization.
*
* @param dataSource a specific {@link DataSource} to be wrapped by the pool
*/
public void setDataSource(DataSource dataSource)
{
this.dataSource = dataSource;
}
...
}

发现没有,HikariDataSource 其实还有一个 setDataSource 属性,即我们xml里定义的 id=”dataSource” 其实还是会将自己注入给自己的!
这也就是为什么上文中 initializeDataSource 方法里,最终实例化的不是 DriverDataSource 而是自己。

所以,改一个名字就可以了。


4.
关于 SpringBoot
这令我好奇,默认支持使用Hikari作为datasource的SpringBoot是怎么实例化HikariDataSource?会有上述问题吗?
这部分代码在

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
org.springframework.boot.autoconfigure.jdbc.DataSourceConfiguration
@ConditionalOnClass(HikariDataSource.class)
@ConditionalOnProperty(name = "spring.datasource.type", havingValue = "com.zaxxer.hikari.HikariDataSource", matchIfMissing = true)
static class Hikari extends DataSourceConfiguration {
@Bean
@ConfigurationProperties(prefix = "spring.datasource.hikari")
public HikariDataSource dataSource(DataSourceProperties properties) {
return createDataSource(properties, HikariDataSource.class);
}
}
org.springframework.boot.autoconfigure.jdbc.DataSourceConfiguration
protected <T> T createDataSource(DataSourceProperties properties,
Class<? extends DataSource> type) {
return (T) properties.initializeDataSourceBuilder().type(type).build();
}
...

不深入了,看下去,反射生成,并且未设置datasource属性,即实际是没有这个问题的。