当前位置: 首页 > news >正文

深入认识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参数设置激活的profile

spring: profiles: # env active: @env@

项目 pom 文件中多个 profile 配置如下(这是本次版本的一个变动点):

<profiles> <!-- DEV 开发环境--> <profile> <id>dev</id> <properties> <env>DEV</env> ... </properties> </profile> ... <!-- PRO 生产环境--> <profile> <id>pro</id> <properties> <env>PRO</env> ... </properties> </profile> </profiles>

maven 打生产包,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.factories:

org.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.ApolloApplicationContextInitializer

com.ctrip.framework.apollo.spring.boot.ApolloApplicationContextInitializer会被扫描到,然后执行其postProcessEnvironment方法,多个EnvironmentPostProcessor的执行顺序由其内部的order属性决定,越小的越靠前,ApolloApplicationContextInitializerorder为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/...?useUnicode=true&characterEncoding=UTF-8&useSSL=false&... username: ... password: ... driver-class-name: com.mysql.cj.jdbc.Driver type: com.alibaba.druid.pool.DruidDataSource ...

idea 启动参数指定 apollo 配置,启动项目,本地 apollo 的缓存文件夹config-cache下是有配置文件存在的,不过只有一个文件app-xx-web+default+application.properties,里面是空的。

yml格式命名空间下的配置呢?

看了下 apollo 的文档,原来yml格式命名空间下的配置在客户端使用需要填写带后缀的完整名字。

注1:yaml/yml格式的namespace从1.3.0版本开始支持和Spring整合,注入时需要填写带后缀的完整名字,比如application.yml

注2:非properties、非yaml/yml格式(如xml,json等)的namespace暂不支持和Spring整合。

配置文件application.yml中修改apollo的配置,将namespacesapplication修改为application.yml

app: id: app-xxx-web apollo: bootstrap: namespaces: application.yml enabled: true eagerLoad: enabled: true

本地调试启动ok,apollo 中的配置可以正常拉取,项目启动成功。

生产环境 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的值被设置成了DEV,idea 中正常启动项目,说明application-dev.yml这个配置文件被读取了。

拿生产包在本地java -jar启动,apollo 的配置服务器指定为dev环境,和生产环境报一样的错误:

java -Dapp.id=app-xxx-web -Dapollo.meta=http://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

http://www.cnnetsun.cn/news/3075555.html

相关文章:

  • DeepSeek美化-为 DeepSeek 网页版引入 Obsidian Border 主题视觉风格
  • RAG基础
  • 做智驾十年,为何Momenta上市换锚?
  • 企业DLP选型指南:从入门到决策,一篇讲透
  • PEAK框架:自然语言驱动的GPU内核优化技术解析
  • Lyra框架:RISC-V处理器验证的异构加速与语义生成技术
  • 郑州翻译公司 俄语保险翻译清单
  • 模板题这道模板题非常全面,相比应用李超线段树的时候实现的东西要多的多:
  • 基于STM32单片机的颜色识别 TCS3200 RGB 检测系统2(设计源文件+万字报告+讲解)(支持资料、图片参考_降重降ai)
  • X-diagnosis实战案例:解决生产环境中的10个典型系统故障
  • Spring MVC的工作流程
  • Go语言代码覆盖率实现一、什么是代码覆盖率
  • 2026年乐清高定全屋木作品牌深度评测:木艺空间定制馆凭何领跑?
  • 气泡特效的核心在于BubbleEffect类,它继承自Manim的Animation类,通过重写关键方法来实现气泡的上升、变大和透明度变化效果。
  • 一文搞懂巴别鸟版本管理:从历史回溯到冲突解决的完整攻略
  • 河南AI大模型人才培养观察:从通识普及到产业实战的多元路径
  • 快马AI三步搭建OpenClaw安卓自动化测试环境:告别手动配置噩梦
  • 别乱改!Multisim14.2三极管仿真参数修改的实战避坑指南(以2N3904为例)
  • 把 quicklink 的预加载思想搬到 API 层:我设计了一套‘懒请求调度器’,首屏并发从 9 降到了 2
  • 化学图像识别工具横评:DECIMER、Img2Mol、MolScribe,哪个更适合你的科研流水线?
  • 《Debezium + Kafka Connect 实战:从零搭建 MySQL CDC 数据管道,踩坑全记录》
  • M4Markets:技术架构的路径复盘
  • open harmony 项目实战:用 AppStorage 实现轻量级页面路由和状态管理
  • open harmony 项目实战:用 ArkTS 实现诗词收藏和阅读历史
  • 基于51/STM32单片机温湿度控制系统设计大棚检测成品恒温恒湿光照44(设计源文件+万字报告+讲解)(支持资料、图片参考_相
  • JavaScript Promise详解
  • Grid布局开发实践
  • C++虚函数工作原理
  • Angular基础开发教程
  • 阅读APP书源配置终极指南:一键解锁全网小说库的完整教程