news 2026/7/4 23:07:17

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
深入认识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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/4 20:00:36

DeepSeek美化-为 DeepSeek 网页版引入 Obsidian Border 主题视觉风格

简介 访问脚本发布页面&#xff1a; DeepSeek 美化 - Greasy Fork DeepSeek-Refined 是一款基于 Tampermonkey 的用户脚本&#xff0c;适用于 DeepSeek Chat 网页版 (chat.deepseek.com)。该脚本通过覆写 DeepSeek 的 CSS 自定义属性&#xff0c;将 Obsidian Border 主题 的标…

作者头像 李华
网站建设 2026/7/5 7:01:23

RAG基础

一、什么是 RAG RAG 是什么 RAG&#xff08;Retrieval-Augmented Generation&#xff09;&#xff0c;中文称为检索增强生成&#xff0c;是一种将**信息检索&#xff08;Retrieval&#xff09;与大语言模型生成&#xff08;Generation&#xff09;**结合的 AI 技术。 一句话理解…

作者头像 李华
网站建设 2026/7/4 20:00:04

做智驾十年,为何Momenta上市换锚?

定价超过地平线&#xff0c;能被资本市场接受吗&#xff1f;文&#xff5c;罗镇昊编&#xff5c;刘俊宏近日&#xff0c;Momenta转以“物理AI”的身份&#xff0c;终于要在港交所主板挂牌了。本次上市&#xff0c;Momenta计划全球发售约1993.8万股&#xff0c;每股定价为295.6港…

作者头像 李华
网站建设 2026/7/4 18:41:54

企业DLP选型指南:从入门到决策,一篇讲透

全球数据泄露事件单次平均损失已突破400万美元&#xff0c;DLP不再是"可选项"&#xff0c;而是企业数据安全的"生存刚需"。综合多份行业深度报告与最新市场动态&#xff0c;从防护体系架构、主流厂商对比、场景化选型到未来趋势&#xff0c;帮你一次性理清…

作者头像 李华
网站建设 2026/7/3 18:14:54

PEAK框架:自然语言驱动的GPU内核优化技术解析

1. PEAK框架&#xff1a;用自然语言重构GPU内核优化范式在深度学习与高性能计算领域&#xff0c;GPU内核优化一直是决定算力利用率的关键因素。传统优化过程需要工程师深入理解GPU内存层次结构、并发模型及硬件特性&#xff0c;这种高度专业化的技能门槛使得性能调优成为少数专…

作者头像 李华
网站建设 2026/7/4 13:33:15

Lyra框架:RISC-V处理器验证的异构加速与语义生成技术

1. Lyra框架设计背景与核心挑战 1.1 处理器验证的现状与痛点 在现代芯片开发流程中&#xff0c;验证环节通常占据整个开发周期的70%以上。随着RISC-V等开源指令集的普及&#xff0c;处理器设计复杂度呈指数级增长&#xff0c;传统验证方法面临两大核心瓶颈&#xff1a; 性能瓶…

作者头像 李华