拓冰建站拓冰建站
首页 / 资讯中心 / 正文

spring.factories 和 imports 两个文件没对齐,自动装配成了“薛定谔的加载“

title: spring.factories 和 imports 两个文件没对齐自动装配成了薛定谔的加载tags: [Java, Spring, SpringBoot, 源码分析]线下好好的灰度环境行为不一样我们团队维护一个内部 starter给所有微服务统一注入一套数据源 多租户路由的自动配置。某次把 Spring Boot 从 2.6 升级到 2.7本地的单元测试、集成测试全绿发到预发也正常。结果灰度放了一台生产机器那台机器的日志里完全没有我们的自动配置生效痕迹——数据源没被包装成多租户版本租户路由直接失效一部分请求查错了库。最折磨人的是同样的 jar、同样的配置10 台机器里有 2 台表现异常8 台正常。我们一度怀疑是环境差异配置中心下发顺序折腾了两天。根因在一条我们以为升级兼容、不用管的迁移细节上。那段两边都有的配置Spring Boot 2.7 是一个过渡版本META-INF/spring.factories里的EnableAutoConfiguration写法被废弃但还能用同时推荐迁移到新的META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件。我们升级时图省事两个文件都留着了但内容没对齐# META-INF/spring.factories旧废弃但 2.7 仍读 org.springframework.boot.autoconfigure.EnableAutoConfiguration\ com.xxx.starter.datasource.TenantDataSourceAutoConfiguration,\ com.xxx.starter.tenant.TenantRouteAutoConfiguration # META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports新 com.xxx.starter.datasource.TenantDataSourceAutoConfiguration注意看旧文件里列了两个配置类新文件里只列了第一个TenantRouteAutoConfiguration在新文件里漏掉了。这就埋下了部分机器只加载到一半配置的雷。Spring Boot 到底从哪读自动配置自动配置的入口是AutoConfigurationImportSelector它决定到底加载哪些配置类。关键看getCandidateConfigurations// AutoConfigurationImportSelectorSpring Boot 2.7简化 protected ListString getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) { // ① 先读 spring.factories 的 EnableAutoConfiguration ListString configurations SpringFactoriesLoader .loadFactoryNames(getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader()); // ② 再读 imports 文件2.7 新增的 ImportCandidates configurations.addAll(ImportCandidates.load(AutoConfiguration.class, getBeanClassLoader()).getCandidates()); return configurations; }逐行看- 第 5 行SpringFactoriesLoader.loadFactoryNames读的是老spring.factories拿到了那两个类。- 第 8 行ImportCandidates.load读的是新imports文件拿到了第一个类。- 两者会被合并第 7 行addAll。那问题来了两个文件都有TenantDataSourceAutoConfiguration合并后它会出现两次吗答案是可能。Spring 后面有一步去重按类名但TenantRouteAutoConfiguration只在旧文件里所以正常应该两边加起来都能拿到。那为什么灰度机只加载了一半这就牵扯到第二个机制配置的过滤与生效顺序。AutoConfigurationImportSelector拿到候选类后还要过一遍filter// 候选类经过 OnBeanCondition / OnClassCondition 等过滤器 protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata metadata) { ListString configurations getCandidateConfigurations(metadata, attributes); configurations filter(configurations, autoConfigurationMetadata); // ① 条件过滤 // ② 再按 AutoConfigureBefore/After 排序 configurations sort(configurations, autoConfigurationMetadata); return new AutoConfigurationEntry(configurations, exclusions); }逐行看- 第 3 行filter会评估ConditionalOnMissingBean、ConditionalOnClass等条件。我们的TenantRouteAutoConfiguration上有AutoConfigureAfter(DataSourceAutoConfiguration.class)——它依赖数据源配置先就绪。- 当新旧两个文件的内容不一致、导致类加载顺序在部分类加载器灰度机用的是另一套类加载路径下出现差异时TenantRouteAutoConfiguration的条件评估时机错位被filter判定为条件不满足而跳过。也就是说jar 里同时存在两份清单Spring 在合并它们时不同机器因类加载器、缓存、甚至是 jar 扫描顺序的微小差异得到了不完全一致的最终候选集。这就是薛定谔的装配——你不知道这台机器到底加载了哪一份。为什么单测抓不到单元测试用的是SpringBootTest它往往直接指定了主配置类或测试配置绕过了完整的AutoConfigurationImportSelector扫描流程。而且我们的单测只断言了数据源存在没断言租户路由存在所以漏掉的那一半配置在测试里毫无存在感。更要命的是那 2 台异常机器重启后偶尔又能正常——因为类加载器缓存命中的顺序随机变化让装配结果在几次重启间漂移。这种非确定性 bug最耗费人。两个修复路径的对比账方案做法结果风险双文件并存原始旧 factories 新 imports 都留部分机器装配不全内容必须完全一致极易漏配只留新 imports删掉 spring.factories 的 EnableAutoConfiguration稳定一致要求 Boot ≥ 2.7只留旧 factories删掉新 imports 文件2.7 能用但 3.0 会失效不可持续升级到 3.0 必炸我们选了只留新 imports彻底删掉旧 factories 里的EnableAutoConfiguration。因为目标版本就是 2.7而且 3.0 会完全移除spring.factories的EnableAutoConfiguration支持留着就是给未来埋雷。// 新文件META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports com.xxx.starter.datasource.TenantDataSourceAutoConfiguration com.xxx.starter.tenant.TenantRouteAutoConfiguration复盘数字受影响机器灰度 10 台中 2 台表现为只有数据源自动配置生效、租户路由缺失。排查耗时约 2 个工作日前期在错误方向环境/配置中心上耗了一天半。触发条件Spring Boot 2.7 过渡期双清单文件内容不一致。修复后删除旧spring.factories的自动配置条目所有机器行为一致单测补充租户路由 bean 必须存在的断言。我的取舍我不建议在过渡版本里新旧两套都留着图省事。自动配置的清单本质是 Spring 在启动早期扫描的两份清单一旦内容不齐得到的候选集就依赖类加载顺序这种不该被依赖的随机因素。迁移到新imports文件时必须保证旧spring.factories里的EnableAutoConfiguration条目被完整删除而不是并存等以后再说。我后来给团队的规范加了两条第一任何 starter 的自动配置清单CI 里加一条检查脚本禁止spring.factories与imports同时声明EnableAutoConfiguration第二单测不仅要断言配置类加载了还要断言它产生的 bean 真的存在且行为正确把漏了一半这种事挡在测试阶段。给迁移上的一道保险CI 静态检查光靠人记得删旧文件迟早会再出事。我们在 starter 的构建流水线里加了一段静态检查打包完成后扫 jar 内部禁止两个清单同时声明自动配置# 在 maven/gradle 构建后执行 if unzip -l target/*.jar | grep -q spring.factories \ unzip -l target/*.jar | grep -q AutoConfiguration.imports; then echo ERROR: 同时存在 spring.factories 与新 imports禁止发布 exit 1 fi这段脚本的逻辑很简单只要 jar 里同时出现了旧spring.factories和新imports两个文件就直接让构建失败把问题挡在发布之前而不是等到灰度机行为异常再回头查。它解决的不是怎么写对而是怎么不让错误版本流出去——对自动配置这种启动早期、难以单测覆盖的环节靠流程兜底比靠评审更稳。思考题如果同一个自动配置类既被spring.factories声明、又被imports声明Spring 去重后它只会被实例化一次吗去重依据的是类名还是类对象在多 ClassLoader 隔离的场景比如某些插件化容器下这个去重还可靠吗
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门