深入认识ClassLoader - 一次投产失败的复盘

发布时间:2026/7/1 1:54:33
深入认识ClassLoader - 一次投产失败的复盘 正好我也在旁边记录下一起排查解决的过程。定位与解决问题分析错误日志拉了版本分支代码从下往上看输出的错误日志发现是DruidDataSourceWrapper这个类中40行出错看下这个类以及出错的位置ConfigurationProperties(spring.datasource.druid) class DruidDataSourceWrapper extends DruidDataSource implements InitializingBean { Autowired private DataSourceProperties basicProperties; Override public void afterPropertiesSet() throws Exception { //if not found prefix spring.datasource.druid jdbc properties ,spring.datasource prefix jdbc properties will be used. if (super.getUsername() null) { // 关键行这一行出错basicProperties.determineUsername()这个方法会出现异常 super.setUsername(basicProperties.determineUsername()); } if (super.getPassword() null) { super.setPassword(basicProperties.determinePassword()); } if (super.getUrl() null) { super.setUrl(basicProperties.determineUrl()); } if (super.getDriverClassName() null) { super.setDriverClassName(basicProperties.getDriverClassName()); } } ...DruidDataSourceWrapper归属于druid-spring-boot-starter这个依赖是 alibaba druid 数据库连接池的一个 starter。结合错误日志看下basicProperties.determineUsername()这个方法里面出错的位置public String determineUsername() { if (StringUtils.hasText(this.username)) { return this.username; } // 关键行调用determineDriverClassName()这个方法出错 if (EmbeddedDatabaseConnection.isEmbedded(determineDriverClassName(), determineUrl())) { return sa; } return null; }再次结合错误日志看下determineDriverClassName()这个方法里面出错的位置public String determineDriverClassName() { if (StringUtils.hasText(this.driverClassName)) { Assert.state(driverClassIsLoadable(), () - Cannot load driver class: this.driverClassName); return this.driverClassName; } String driverClassName null; if (StringUtils.hasText(this.url)) { driverClassName DatabaseDriver.fromJdbcUrl(this.url).getDriverClassName(); } if (!StringUtils.hasText(driverClassName)) { driverClassName this.embeddedDatabaseConnection.getDriverClassName(); } if (!StringUtils.hasText(driverClassName)) { // 关键行在这边抛出的异常 throw new DataSourceBeanCreationException(Failed to determine a suitable driver class, this, this.embeddedDatabaseConnection); } return driverClassName; }定位到了出错的位置分析这块代码抛出异常的原因意思就是如果spring.datasource.druid.username这个配置的值为空那么读取spring.datasource.username这个配置如果还是空尝试从spring.datasource.url配置信息中解析jdbc驱动类解析不出来就抛出DataSourceBeanCreationException异常。版本变动点是配置信息有问题问了下这个项目的配置原本是放在配置文件中的公共配置放在了application.yml中不同环境的配置采用application-{profile}.yml放置如下application.yml application-dev.yml ... application-pro.yml在application.yml中使用占位符借助 maven 打包时添加-P参数设置激活的profilespring: profiles: # env active: env项目 pom 文件中多个 profile 配置如下这是本次版本的一个变动点profiles !-- DEV 开发环境-- profile iddev/id properties envDEV/env ... /properties /profile ... !-- PRO 生产环境-- profile idpro/id properties envPRO/env ... /properties /profile /profilesmaven 打生产包spring.profiles.active的值被设置成了PRO也就是生产环境将使用application-PRO.yml这个配置文件。这个版本的另一个变动点是接入了 apollo 配置中心但是没有删除不同环境的配置文件配置文件application.yml中增加了 apollo 相关的配置app: id: app-xxx-web apollo: bootstrap: namespaces: application enabled: true eagerLoad: enabled: true分析 SpringBoot 的配置加载流程触发时机SpringBoot 应用启动时在 SpringApplicationprepareEnvironment方法中发布ApplicationEnvironmentPreparedEvent事件EnvironmentPostProcessorApplicationListener 中监听了这个事件触发配置信息读取不同来源的配置信息有专门实现了EnvironmentPostProcessor接口的类进行处理这些类实现postProcessEnvironment方法apollo-client使用的是v1.9.0版本其包含一个META-INF/spring.factoriesorg.springframework.boot.autoconfigure.EnableAutoConfiguration\ com.ctrip.framework.apollo.spring.boot.ApolloAutoConfiguration org.springframework.context.ApplicationContextInitializer\ com.ctrip.framework.apollo.spring.boot.ApolloApplicationContextInitializer org.springframework.boot.env.EnvironmentPostProcessor\ com.ctrip.framework.apollo.spring.boot.ApolloApplicationContextInitializercom.ctrip.framework.apollo.spring.boot.ApolloApplicationContextInitializer会被扫描到然后执行其postProcessEnvironment方法多个EnvironmentPostProcessor的执行顺序由其内部的order属性决定越小的越靠前ApolloApplicationContextInitializer的order为0属于是靠后的SpringBoot 中后加载的属性源可以覆盖先加载的属性源定义的值参考属性源的优先级顺序因此 apollo 中的配置会覆盖配置文件中的配置。难道是 apollo 中的配置写错了看了下 apollo 中没有spring.datasource.url这个配置数据库的连接信息是写在spring.datasource.druid这个前缀开头下面的apollo 中有两个名为application的命名空间一个格式是properties另一个格式是yml这些配置是写在yml格式命名空间下的properties格式命名空间下的配置为空。spring: # druid pool datasource: druid: url: jdbc:mysql://...:3306/...?useUnicodetruecharacterEncodingUTF-8useSSLfalse... username: ... password: ... driver-class-name: com.mysql.cj.jdbc.Driver type: com.alibaba.druid.pool.DruidDataSource ...idea 启动参数指定 apollo 配置启动项目本地 apollo 的缓存文件夹config-cache下是有配置文件存在的不过只有一个文件app-xx-webdefaultapplication.properties里面是空的。yml格式命名空间下的配置呢看了下 apollo 的文档原来yml格式命名空间下的配置在客户端使用需要填写带后缀的完整名字。注1yaml/yml格式的namespace从1.3.0版本开始支持和Spring整合注入时需要填写带后缀的完整名字比如application.yml注2非properties、非yaml/yml格式如xmljson等的namespace暂不支持和Spring整合。配置文件application.yml中修改apollo的配置将namespaces从application修改为application.ymlapp: id: app-xxx-web apollo: bootstrap: namespaces: application.yml enabled: true eagerLoad: enabled: true本地调试启动okapollo 中的配置可以正常拉取项目启动成功。生产环境 apollo 中的配置没有生效的话可application-{profile}.yml文件还在应该还是能读取配置文件中的配置完成启动的吧额不对 maven 打生产包spring.profiles.active的值被设置成了PRO但classpath下生产环境配置文件名称为application-pro.yml大小写不一致能正常加载吗将application.yml配置文件中的app.apollo.bootstrap.namespaces配置还原在 maven 的 Profiles 中勾选 dev spring.profiles.active的值被设置成了DEVidea 中正常启动项目说明application-dev.yml这个配置文件被读取了。拿生产包在本地java -jar启动apollo 的配置服务器指定为dev环境和生产环境报一样的错误java -Dapp.idapp-xxx-web -Dapollo.metahttp://10.100.x.x:8072 -jar app-xxx-web.jar难道是 CICD 打包的问题没有加载的配置文件本地打了一个包启动也是报一样的错误奇怪了idea 里面启动和打成FatJar之后启动的行为还不一样。idea 里面启动spring.profiles.active的值是大写的DEVapplication-dev.yml中的配置是能正常读取的打成FatJar之后spring.profiles.active的值是大写的PROapplication-pro.yml中的配置却不能正常读取。apollo 的app.id这个配置是放在application.yml中的启动后本地 apollo 的配置缓存文件夹config-cache下是有配置的说明application.yml是生效的只是不同环境application-{profile}.yml文件中的配置没有生效。得着重看看 SpringBoot 中读取配置文件的逻辑了。配置文件的加载流程上面分析到EnvironmentPostProcessorApplicationListener 中监听了ApplicationEnvironmentPreparedEvent事件做配置信息读取动作不同来源的配置信息有专门实现了EnvironmentPostProcessor接口的类进行处理配置文件的处理类是哪一个debug 看了下是ConfigDataEnvironmentPostProcessor其 postProcessEnviron