Caused by: java.lang.ClassNotFoundException: com.zaxxer.hikari.metrics.MetricsTrackerFactory
at java.base/java.net.URLClassLoader.findClass(URLClassLoader.java:471)
at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:588)
at org.springframework.boot.loader.LaunchedURLClassLoader.loadClass(LaunchedURLClassLoader.java:92)
at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:521)
… 27 common frames omitted
项目本地运行没问题,打包到服务器上报了这个错,啥原因?
以下是问题解决后个人认为比较正确的问题定位思路。
springboot项目版本号为2.2.2。
我们知道Hikari是一款连接池,在springboot2.0之后采用的默认的数据库连接池就是Hikari。但是我目前的项目用的数据库连接池是druid,莫非是初始化还是加载了hikari?
查了资料发现hikari是在spring-boot-starter-jdbc和spring-boot-starter-data-jpa包下默认解析HikariCP依赖,项目里并没有引用到hakari。于是我查看了一下目前的maven引用情况:
于是我把该引用去除:
io.shardingsphere
sharding-jdbc
3.0.0.M3
com.zaxxer
HikariCP-java6
重新部署该项目,发现项目成功启动了。
但是这个时候有个疑问,为什么把hikari依赖去除了,反而“加载成功了呢”?
到这里,应该去了解一下springboot,它是如何默认初始化Hikari连接池的。
在这之前,我们需要了解一个知识,即springboot的配置项依赖是如何实现的:
大家应该都知道,我们的springboot启动类上要添加一个注解@SpringBootApplication
,
下面展示一些该注解的内部。
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication
里面关键的是
@SpringBootConfiguration
@EnableAutoConfiguration
@SpringBootConfiguration
这个注解,只是@Configuration注解的派生注解,跟@Configuration注解的功能一致,标注这个类是一个配置类,@Configuration用于定义配置类,可替换xml配置文件,被注解的类内部包含有一个或多个被@Bean注解的方法,这些方法将会被AnnotationConfigApplicationContext或AnnotationConfigWebApplicationContext类进行扫描,并用于构建bean定义,初始化Spring容器。这个不是本文重点,先不展开。
@EnableAutoConfiguration
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
这里导入了AutoConfigurationImportSelector类:
接下来看该类的selectImports方法:
@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return NO_IMPORTS;
}
AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader
.loadMetadata(this.beanClassLoader);
AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(autoConfigurationMetadata,
annotationMetadata);
return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}
先看下去,loadMetadata方法:
protected static final String PATH = "META-INF/spring-autoconfigure-metadata.properties";
private AutoConfigurationMetadataLoader() {
}
static AutoConfigurationMetadata loadMetadata(ClassLoader classLoader) {
return loadMetadata(classLoader, PATH);
}
可以看到,它通过PATH加载了需要自动装配的元数据:
org.springframework.boot.autoconfigure.data.jpa.JpaRepositoriesAutoConfiguration=
org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration.AutoConfigureAfter=org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration
org.springframework.boot.autoconfigure.data.cassandra.CassandraReactiveDataAutoConfiguration.ConditionalOnClass=com.datastax.driver.core.Cluster,reactor.core.publisher.Flux,org.springframework.data.cassandra.core.ReactiveCassandraTemplate
org.springframework.boot.autoconfigure.data.solr.SolrRepositoriesAutoConfiguration.ConditionalOnClass=org.apache.solr.client.solrj.SolrClient,org.springframework.data.solr.repository.SolrRepository
org.springframework.boot.autoconfigure.security.oauth2.client.servlet.OAuth2ClientAutoConfiguration.ConditionalOnWebApplication=SERVLET
org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration=
.....
等号右边是需要进行自动装载的类。
在其中,可以看到一个DataSourceAutoConfiguration类。字面意思可以看出来这是自动配置数据源的类。
到这里,我们可以看出spring容器在初始化时通过找到元数据,加载其内的配置类,实现自动装配,包括我们要找的数据源Datasource(我们可以通过添加配置元数据properties文件添加默认的装配,也可以通过@configuration来实现自定义的装配)。
接下来就来看看DataSourceAutoConfiguration是怎么实现数据源的初始化的:
ConditionalOnMissingBean中,datasource已经通过druid生成了,但是XADAtaSource没有生成。因此继续import。
@Configuration(proxyBeanMethods = false)
@Conditional(PooledDataSourceCondition.class)
@ConditionalOnMissingBean({ DataSource.class, XADataSource.class })
@Import({ DataSourceConfiguration.Hikari.class, DataSourceConfiguration.Tomcat.class,
DataSourceConfiguration.Dbcp2.class, DataSourceConfiguration.Generic.class,
DataSourceJmxConfiguration.class })
protected static class PooledDataSourceConfiguration {
}
可以看到其引入了包括Hikari的四个数据源配置类,都在DataSourceConfiguration中:
/**
* Hikari DataSource configuration.
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(HikariDataSource.class)
@ConditionalOnMissingBean(DataSource.class)
@ConditionalOnProperty(name = "spring.datasource.type", havingValue = "com.zaxxer.hikari.HikariDataSource",
matchIfMissing = true)
static class Hikari {
@Bean
@ConfigurationProperties(prefix = "spring.datasource.hikari")
HikariDataSource dataSource(DataSourceProperties properties) {
HikariDataSource dataSource = createDataSource(properties, HikariDataSource.class);
if (StringUtils.hasText(properties.getName())) {
dataSource.setPoolName(properties.getName());
}
return dataSource;
}
}
接下来具体看hikari数据源的创建过程,首先看到@ConditionalOnClass(HikariDataSource.class)
这个注解意思是该类的初始化需要找到HikariDataSource。
这先解释了为什么我把hikari的依赖去掉后,项目启动成功,因为它根本不会去创建hikari连接池了,也不会出现之后的问题。
同时再看dataSource上的注解
@ConfigurationProperties(prefix = “spring.datasource.hikari”)
它从元数据spring-configuration-metadata.json中匹配name前缀为spring.datasource.hikari的属性,其中就有:
{
"name": "spring.datasource.hikari.metrics-tracker-factory",
"type": "com.zaxxer.hikari.metrics.MetricsTrackerFactory",
"sourceType": "com.zaxxer.hikari.HikariDataSource"
},
也就是说需要初始化该对象。
这时我没有依赖hikari,因此找不到该类,该数据源也不会进行初始化.
具体的体现方式是在
@Import({ DataSourceConfiguration.Hikari.class, DataSourceConfiguration.Tomcat.class,
DataSourceConfiguration.Dbcp2.class, DataSourceConfiguration.Generic.class,
DataSourceJmxConfiguration.class })
这边类引入的时候就出现了加载错误。
Caused by: java.lang.ClassNotFoundException: com.zaxxer.hikari.metrics.MetricsTrackerFactory
at java.base/java.net.URLClassLoader.findClass(URLClassLoader.java:471)
at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:588)
at org.springframework.boot.loader.LaunchedURLClassLoader.loadClass(LaunchedURLClassLoader.java:92)
at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:521)
... 27 common frames omitted
那么我把之前的依赖加上看看:
io.shardingsphere
sharding-jdbc
3.0.0.M3
先查看createDateSource方法:
这边传入的type即HikariDataSource.class
protected static T createDataSource(DataSourceProperties properties, Class extends DataSource> type) {
return (T) properties.initializeDataSourceBuilder().type(type).build();
}
查看build()方法:
public T build() {
Class extends DataSource> type = this.getType();
DataSource result = (DataSource)BeanUtils.instantiateClass(type);
this.maybeGetDriverClassName();
this.bind(result);
return result;
}
可以看到通过BeanUtils去加载对应到数据源(在这里是HikariDataSource):
这时查看该类:
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package com.zaxxer.hikari;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.health.HealthCheckRegistry;
import com.zaxxer.hikari.pool.HikariPool;
import com.zaxxer.hikari.proxy.IHikariConnectionProxy;
import com.zaxxer.hikari.util.DriverDataSource;
import java.io.Closeable;
import java.io.PrintWriter;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.sql.Wrapper;
import java.util.HashMap;
import java.util.Iterator;
import javax.sql.DataSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class HikariDataSource extends HikariConfig implements DataSource, Closeable {
private static final Logger LOGGER = LoggerFactory.getLogger(HikariDataSource.class);
private final HashMap multiPool;
private volatile boolean isShutdown;
private final HikariPool fastPathPool;
private volatile HikariPool pool;
public HikariDataSource() {
this.fastPathPool = null;
this.multiPool = new HashMap();
}
public HikariDataSource(HikariConfig configuration) {
configuration.validate();
configuration.copyState(this);
this.multiPool = new HashMap();
LOGGER.info("HikariCP pool {} is starting.", configuration.getPoolName());
this.pool = this.fastPathPool = new HikariPool(this);
this.multiPool.put(new HikariDataSource.MultiPoolKey(this.getUsername(), this.getPassword()), this.pool);
}
public Connection getConnection() throws SQLException {
if (this.isShutdown) {
throw new SQLException("Pool has been shutdown");
} else if (this.fastPathPool != null) {
return this.fastPathPool.getConnection();
} else {
HikariPool result = this.pool;
if (result == null) {
synchronized(this) {
result = this.pool;
if (result == null) {
this.validate();
LOGGER.info("HikariCP pool {} is starting.", this.getPoolName());
this.pool = result = new HikariPool(this);
this.multiPool.put(new HikariDataSource.MultiPoolKey(this.getUsername(), this.getPassword()), this.pool);
}
}
}
return result.getConnection();
}
}
/** @deprecated */
@Deprecated
public Connection getConnection(String username, String password) throws SQLException {
if (this.isShutdown) {
throw new SQLException("Pool has been shutdown");
} else {
HikariDataSource.MultiPoolKey key = new HikariDataSource.MultiPoolKey(username, password);
HikariPool hikariPool;
synchronized(this.multiPool) {
hikariPool = (HikariPool)this.multiPool.get(key);
if (hikariPool == null) {
hikariPool = new HikariPool(this, username, password);
this.multiPool.put(key, hikariPool);
}
}
return hikariPool.getConnection();
}
}
public PrintWriter getLogWriter() throws SQLException {
return this.pool.getDataSource() != null ? this.pool.getDataSource().getLogWriter() : null;
}
public void setLogWriter(PrintWriter out) throws SQLException {
if (this.pool.getDataSource() != null) {
this.pool.getDataSource().setLogWriter(out);
}
}
public void setLoginTimeout(int seconds) throws SQLException {
Iterator var2 = this.multiPool.values().iterator();
while(var2.hasNext()) {
HikariPool hikariPool = (HikariPool)var2.next();
hikariPool.getDataSource().setLoginTimeout(seconds);
}
}
public int getLoginTimeout() throws SQLException {
HikariPool hikariPool = (HikariPool)this.multiPool.values().iterator().next();
return hikariPool != null ? hikariPool.getDataSource().getLoginTimeout() : 0;
}
public java.util.logging.Logger getParentLogger() throws SQLFeatureNotSupportedException {
throw new SQLFeatureNotSupportedException();
}
public T unwrap(Class iface) throws SQLException {
if (iface.isInstance(this)) {
return this;
} else {
if (this.pool != null) {
if (iface.isInstance(this.pool.getDataSource())) {
return this.pool.getDataSource();
}
if (this.pool.getDataSource() instanceof Wrapper) {
return this.pool.getDataSource().unwrap(iface);
}
}
throw new SQLException("Wrapped DataSource is not an instance of " + iface);
}
}
public boolean isWrapperFor(Class> iface) throws SQLException {
if (iface.isInstance(this)) {
return true;
} else {
if (this.pool != null) {
if (iface.isInstance(this.pool.getDataSource())) {
return true;
}
if (this.pool.getDataSource() instanceof Wrapper) {
return this.pool.getDataSource().isWrapperFor(iface);
}
}
return false;
}
}
public void setMetricRegistry(Object metricRegistry) {
boolean isAlreadySet = this.getMetricRegistry() != null;
super.setMetricRegistry(metricRegistry);
if (this.fastPathPool != null || this.pool != null) {
if (isAlreadySet) {
throw new IllegalStateException("MetricRegistry can only be set one time");
}
this.pool.setMetricRegistry((MetricRegistry)super.getMetricRegistry());
}
}
public void setHealthCheckRegistry(Object healthCheckRegistry) {
boolean isAlreadySet = this.getHealthCheckRegistry() != null;
super.setHealthCheckRegistry(healthCheckRegistry);
if (this.fastPathPool != null || this.pool != null) {
if (isAlreadySet) {
throw new IllegalStateException("HealthCheckRegistry can only be set one time");
}
this.pool.setHealthCheckRegistry((HealthCheckRegistry)super.getHealthCheckRegistry());
}
}
public void evictConnection(Connection connection) {
if (!this.isShutdown && this.pool != null && connection instanceof IHikariConnectionProxy) {
this.pool.evictConnection((IHikariConnectionProxy)connection);
}
}
public void suspendPool() {
if (!this.isShutdown && this.pool != null) {
this.pool.suspendPool();
}
}
public void resumePool() {
if (!this.isShutdown && this.pool != null) {
this.pool.resumePool();
}
}
public void close() {
this.shutdown();
}
public void shutdown() {
if (!this.isShutdown) {
this.isShutdown = true;
if (this.fastPathPool != null) {
this.shutdownHelper(this.fastPathPool);
}
Iterator var1 = this.multiPool.values().iterator();
while(var1.hasNext()) {
HikariPool hikariPool = (HikariPool)var1.next();
this.shutdownHelper(hikariPool);
}
}
}
public String toString() {
return String.format("HikariDataSource (%s)", this.pool);
}
private void shutdownHelper(HikariPool hPool) {
try {
hPool.shutdown();
} catch (InterruptedException var3) {
LoggerFactory.getLogger(this.getClass()).warn("Interrupted during shutdown", var3);
}
if (hPool.getDataSource() instanceof DriverDataSource) {
((DriverDataSource)hPool.getDataSource()).shutdown();
}
}
private static class MultiPoolKey {
private String username;
private String password;
MultiPoolKey(String username, String password) {
this.username = username;
this.password = password;
}
public int hashCode() {
return this.password == null ? 0 : this.password.hashCode();
}
public boolean equals(Object obj) {
HikariDataSource.MultiPoolKey otherKey = (HikariDataSource.MultiPoolKey)obj;
if (this.username != null && !this.username.equals(otherKey.username)) {
return false;
} else if (this.username == null && otherKey.username != null) {
return false;
} else if (this.password != null && !this.password.equals(otherKey.password)) {
return false;
} else {
return this.password != null || otherKey.password == null;
}
}
}
}
发现该版本的hikaridatasource中并没有该属性。从而导致启动时报找不到该类的错误。
问题的原因找到了,接下来看看springboot其要求的hikari版本
org.springframework.boot
spring-boot-starter-parent
2.2.2.RELEASE
查看spring-boot-starter-parent
org.springframework.boot
spring-boot-dependencies
2.2.2.RELEASE
../../spring-boot-dependencies
查看spring-boot-dependencies
3.4.1
可以看到其支持的版本为3.4.1。
那么现在我主动引入该版本看看。
com.zaxxer
HikariCP
3.4.1
查看HikariDataSource类
public void setMetricsTrackerFactory(MetricsTrackerFactory metricsTrackerFactory) {
boolean isAlreadySet = this.getMetricsTrackerFactory() != null;
super.setMetricsTrackerFactory(metricsTrackerFactory);
HikariPool p = this.pool;
if (p != null) {
if (isAlreadySet) {
throw new IllegalStateException("MetricsTrackerFactory can only be set one time");
}
p.setMetricsTrackerFactory(super.getMetricsTrackerFactory());
}
}
看到了MetricsTrackerFactory。
发布到服务器报错到原因找到了,那么为什么本地跑不报错呢?
看下本地的maven仓库:
发现有好几个hikari包,其中也包括了最新版本的包(Hikari),mvn clean,mvn package之后发现,高版本的包还是存在(发现spring-boot-dependencies里的很多没用到的依赖也被下载到了本地仓库),因此它能够找到该类。
经过调试发现我们实际引入3.4.1之后,还是不会生成hikari数据源。
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(HikariDataSource.class)
static class HikariPoolDataSourceMetadataProviderConfiguration {
@Bean
DataSourcePoolMetadataProvider hikariPoolDataSourceMetadataProvider() {
return (dataSource) -> {
HikariDataSource hikariDataSource = DataSourceUnwrapper.unwrap(dataSource, HikariDataSource.class);
if (hikariDataSource != null) {
return new HikariDataSourcePoolMetadata(hikariDataSource);
}
return null;
};
}
}
是因为在这里,获得了druid数据源,并无法wrap(转换)为hikari数据源,直接return null,结束了hikari数据源的初始化。
该错误原因是引入了低版本的hikari,导致springboot默认配置的hikari对象属性类找不到,出现classNotFound异常。去掉其低版本依赖就可以了。
在springboot容器初始化时候报的错,我们可以去追溯其初始化过程,理解spring装载过程,就很容易解决这类问题了。
除此之外,默认的hikari数据源在生成时会判断是否已经创建了数据源,若创建的数据源为hikari类型,则返回其,表明hikari创建成功,若不是,则返回null,表明hikari数据源创建失败。