接手老项目把 Spring Boot 从 2.3 升到 3.0,编译一切顺利,启动后却发现自定义的自动配置全部失效,日志里连尝试加载的痕迹都没有。当时第一反应是条件注解没满足,排查了大半天,最后才意识到是自动配置的"登记表"变了——项目里还在用旧的META-INF/spring.factories,而 Spring Boot 3.x 已经换成了META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports。这类问题在升级中特别容易踩,而且往往不动神色,今天把这两套机制的来龙去脉、迁移方法和避坑经验一次讲清楚。
这篇内容适合谁?如果你要维护或升级 Spring Boot 2.7+ 的老项目,要自己写 starter 给团队用,或者一直对"自动配置到底怎么被 Spring 发现的"这件事有点模糊,那这篇文章值得读完。
1. 先搞清楚:自动配置到底是怎么被发现的
1.1 老路子:spring.factories 的工作原理
在 Spring Boot 2.7 之前的很长一段时间里,所有需要被自动装配的类,都靠一个叫spring.factories的文件来"登记"。这个文件放在META-INF目录下,本质是个 Java properties 格式的文件,内容大概长这样:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.example.starter.DemoAutoConfiguration,\ com.example.starter.AuditAutoConfiguration启动时,SpringFactoriesLoader会去 classpath 下所有 jar 包的META-INF/spring.factories文件里,读取org.springframework.boot.autoconfigure.EnableAutoConfiguration这个 key 对应的配置类列表,然后挨个尝试匹配条件注解,符合条件的就会被加载成 bean。
这里有一个容易被忽视的点:spring.factories不止用于自动配置。它还承担ApplicationContextInitializer、ApplicationListener、EnvironmentPostProcessor等扩展点的注册。那时候开发者戏称它是"Spring Boot 的万能插销板",只要你需要一个实现类的集合,都可以塞进去。但也正因为什么都能往里塞,这个文件越来越臃肿,还带来了性能和安全性上的隐患——Spring Boot 官方在 2.7 版本中正式标注了spring.factories里自动配置条目为废弃状态,给出的理由很实在:"这个文件太通用,导致 JVM 启动时无法对里面的实现类做预判优化,也无法在 GraalVM 原生镜像环境下做静态分析。"
1.2 新机制:AutoConfiguration.imports 文件到底怎么工作
org.springframework.boot.autoconfigure.AutoConfiguration.imports这个文件名很长,第一次看到的时候很容易写错。它的正确路径是META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports,注意是放在spring子目录下,不是直接放META-INF。
文件内容不需要 key,直接一行一个自动配置类全限定名:
com.example.starter.DemoAutoConfiguration com.example.starter.AuditAutoConfigurationSpring Boot 启动时会通过自己的配置类加载器去找到这个固定文件名的资源,把每一行解析成一个类名。为什么要换这个格式?关键动机是"专一"。这个文件只做一件事:声明自动配置类。加载器从通用的SpringFactoriesLoader换成了AutoConfigurationImportSelector内部专用的读取逻辑,这样一来,JVM 在启动时对自动配置类型有更精确的预期,对 GraalVM 原生化也更友好。官方在 2.7 版本的更新说明里甚至建议,所有提供自动配置的库,都应该尽快切换到这个新文件上。
实际上,把自动配置独立成一个文件,还有一个文档层面的好处:任何人打开一个 starter 的 jar 包,一眼就能看到这个项目注册了哪些自动配置类,不再需要在巨大的spring.factories里找那一行键值对。
1.3 官方为什么坚持移除旧机制
很多人问,一个 properties 文件而已,多兼容一个格式有那么难吗?为什么 Spring Boot 3 里直接不认spring.factories里的自动配置了?
原因是多方面的。第一是性能。spring.factories加载所有 key 的所有值,即使只在用EnableAutoConfiguration,也会把文件里其他扩展点配置同时加载,启动扫描成本变高。第二是原生镜像。Spring Framework 6 和 Spring Boot 3 的主要目标之一是"全场景支持 GraalVM 原生编译",原生编译需要在构建期就对所有资源做静态分析,如果文件内容还需要通过 key 动态解释,编译器就没法提前生成 hints。换成固定文件名的imports文件后,构建插件可以直接遍历每个 jar 包内的这个文件。
第三,也是最现实的原因:spring.factories里面不同条目之间的顺序和重复性很难管理。两个 jar 包如果同时定义了一样的ApplicationListener,最终顺序会因为 classpath 扫描顺序不同而变得不可控。自动配置单独拆出来后,自己拥有独立的排序和去重逻辑,行为更稳定。
所以,这并不只是换了个文件名,而是一次基础设施级别的重构。理解了这一点,后续排查问题会顺很多。
2. 两套机制并存期,要怎么迁移和兼容
2.1 从 Spring Boot 2.7 开始的标准做法
如果你维护的库需要同时兼容 Spring Boot 2.6 和 2.7,甚至 3.x,最稳妥的做法是在自动配置模块里同时保留两个文件。等你把项目升级到 2.7 以上版本,就可以开始做迁移:先把类从META-INF/spring.factories的EnableAutoConfiguration条目下移到org.springframework.boot.autoconfigure.AutoConfiguration.imports文件。
需要注意的是,新文件必须使用AutoConfiguration.imports这个名字,不能自己发明类似的兼容名。这个文件名在 Spring Boot 中是硬编码的,它在SpringFactoriesLoader之外单独查找。
实际操作时,我习惯在每个 starter 项目的:
src/main/resources/META-INF/目录下新建spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件。如果之前spring.factories里只有自动配置,没有其他扩展点,可以直接把旧的EnableAutoConfiguration那一段删除。但如果里面还注册了ApplicationListener、EnvironmentPostProcessor之类的内容,不能一并删除,只能单独删掉自动配置那一行。
2.2 为什么要保留 spring.factories 的场景
一个常见的误区是:Spring Boot 3 删了spring.factories里的自动配置支持,是不是意味着整个文件都没用了?不是。spring.factories里其他 key 依然有效,比如spring.factories中org.springframework.context.ApplicationContextInitializer和org.springframework.boot.env.EnvironmentPostProcessor仍然会被加载。
我自己在升级中就踩过这个坑。当时有一个 starter 既提供自动配置,又注册了一个EnvironmentPostProcessor,我图省事把整个spring.factories文件删了,结果启动时环境变量的默认值没有被设置,排查了很久才发现是删过头了。正确做法:只删EnableAutoConfiguration这一行,保留其他条目的注册。
所以,建议迁移时先列一个清单,看清楚这个spring.factories文件里除了自动配置之外还有什么。哪些能归到新的机制里去,哪些必须留。
2.3 Spring Boot 3 里如果不迁移,会发生什么
一句话:自动配置类根本没有机会被加载,你的 bean 也不会创建,而且不会报任何错。Spring Boot 3 启动时根本不去读spring.factories里的自动配置条目,所以你看到的现象就是"什么都没发生",日志里也没有 ERROR。
这里有个很关键的调试入口:开启调试日志后观察是否存在形如下面的输出:
Negative matches: DemoAutoConfiguration如果连出现在Negative matches里都没有,那大概率是自动配置类根本没被读取进来。就是文件路径或文件名的问题。如果它能出现在Negative matches里,说明被识别到了,只是条件不满足。
在 Spring Boot 2.7 和 2.8 之间,有一个小坑:2.7 中如果你同时写了spring.factories和新的imports文件,两个里面的配置类都会加载,可能导致同一个自动配置类被评估两次,从而产生重复 bean。如果你在迁移中遇到"bean 重复定义"异常,先看看是不是迁移过程中两边都保留了一份,删掉旧的一边即可。
3. 写一个最简单的自动配置 starter,实测两类文件
3.1 搭建最小自动配置模块
为了验证这套机制,我自己建过一个学习用的 demo starter。结构如下:
my-spring-boot-starter/ └── src/main/java/com/example/starter/ ├── DemoProperties.java ├── DemoService.java └── DemoAutoConfiguration.java └── src/main/resources/META-INF/ └── spring/ └── org.springframework.boot.autoconfigure.AutoConfiguration.importsDemoAutoConfiguration代码很简单:
package com.example.starter; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; @AutoConfiguration @ConditionalOnProperty(prefix = "demo", name = "enabled", havingValue = "true", matchIfMissing = true) @EnableConfigurationProperties(DemoProperties.class) public class DemoAutoConfiguration { @Bean @ConditionalOnMissingBean public DemoService demoService(DemoProperties properties) { return new DemoService(properties.getPrefix()); } }注意,从 Spring Boot 2.7 开始,官方建议在自动配置类上标注@AutoConfiguration,而不是直接用@Configuration。因为AutoConfigurationImportSelector在加载 imports 文件时能识别这个注解,并在排序时把配置类的顺序统一管理。用@Configuration也能跑通,但会失去一些排序上的便利。
imports文件内容就一行:
com.example.starter.DemoAutoConfiguration这个文件末尾建议保留换行符,有个别构建工具在打包时会忽略最后一行文本,如果遇到"类没被加载"的情况,先检查文件末尾有没有换行,这种细节最容易被人忽略。
3.2 为什么自动配置里不能直接用 @ComponentScan
自动配置的定位是"给使用者提供可选的默认装配",所以它对使用者现有的配置是无感知的。如果我在自动配置类上直接加@ComponentScan,那会扫描到使用者自己的包,容易把一些不该创建的对象也注册进来,还会跟使用者的扫描范围和装配顺序冲突。想通的代码,只能靠条件注解来控制,比如@ConditionalOnClass、@ConditionalOnProperty、@ConditionalOnMissingBean。
这三个条件注解的使用场景差别很大。我常遇到的情况是:自己写的 starter 里依赖了一个第三方库,只有 classpath 里有这个库时才创建对应的服务 bean,就用@ConditionalOnClass;如果允许使用者通过配置文件开关这个功能,就用@ConditionalOnProperty;如果使用者自己已经定义了一个更定制化的 bean,我这边就不要重复创建了,这是@ConditionalOnMissingBean的职责。把这三个条件组合起来,自动配置才能真正做到"默认可用、按需覆盖"。
3.3 配置顺序:AutoConfiguration.imports 也支持排序
你可能没注意到,AutoConfiguration.imports文件虽然不像 spring.factories 那样有复杂的 key-value,但是 Spring Boot 仍然允许你通过AutoConfigurationImportSelector和@AutoConfigureBefore、@AutoConfigureAfter这两个注解手动排序。如果一个 starter 包含多个自动配置类,比如先配数据源,再配基于数据源的缓存策略,那我就必须在 imports 文件和注解上明确顺序。
一种常见写法是:
@AutoConfiguration @AutoConfigureAfter(DataSourceAutoConfiguration.class) public class CacheAutoConfiguration { }这里要注意,@AutoConfigureAfter的参数是自动配置类的 class 对象,如果被依赖的配置类没有在自动配置清单里,这个顺序注解会被直接忽略,而且不会报错。排查问题时如果发现顺序没生效,先确认DataSourceAutoConfiguration是否真的能被加载。
验证加载情况的另一个实用技巧是,在主程序里加这段配置:
debug=true启动时控制台会打印所有自动配置条件的匹配结果,包括Positive matches和Negative matches。如果DemoAutoConfiguration出现在Positive matches,说明加载链路完全正常;如果是Negative matches,那就要看具体是哪条条件注解挡住了。这个方法对排查"为什么我的配置没生效"特别有用。
4. 升级过程中的常见问题与排查技巧实录
4.1 "自动配置没生效"的标准排查路径
这个现象我见过太多了,尤其是团队里多人同时升级时,经常有人喊着"我的 starter 坏了"。但实际上,90% 的情况都不是代码坏了,而是自动配置文件名写错了。我把自己的排查顺序整理成了一张表,供你直接对照:
| 检查项 | 判断标准 | 处理方式 |
|---|---|---|
| 文件路径 | 必须在META-INF/spring/下 | 确认没有放到META-INF根目录 |
| 文件名 | 必须是全名org.springframework.boot.autoconfigure.AutoConfiguration.imports | 复制粘贴,不要手敲 |
| 文件编码 | 必须是 UTF-8,不能带 BOM | 用 IDE 重新保存或把 BOM 去点 |
| 类是否公开 | 自动配置类必须是public | 如果是 package-private,加载时直接失败 |
| 类构造器 | 不能有带参构造器 | 自动配置类要用无参构造器 |
| 条件注解 | 使用者的配置文件里是否禁用了相关属性 | 看Negative matches判断具体是哪条条件 |
| debug 开关 | 是否开启了debug=true | 开启后看匹配结果 |
如果每一步都查完,还是没有出现在日志里,那就再确认一下打出来的 jar 包本身。有些时候,IDE 里的项目能跑通,但打包发的 jar 里因为 resource 过滤或 maven-resources-plugin 配置问题,imports 文件没有被打进去。可以执行jar tf命令查看:
jar tf your-starter.jar | grep AutoConfiguration.imports没有输出的话,去检查构建配置。这个问题在 maven 配置了非默认的 resource 目录时特别容易出现。
4.2 同时保留新旧文件的重复加载问题
前面提到过 Spring Boot 2.7 会兼容加载新旧两套文件里的自动配置,很多人没意识到这会导致同一个自动配置类自己被评估两次。如果一个自动配置类里用了@ConditionalOnMissingBean和自己定义成对使用,倒还好。但如果里面用了@ConditionalOnClass,并且这种情况下你还注册了同名 bean,就会看到 "BeanDefinitionStoreException" 或者启动时报 "conflicting bean definition found"。
这里我给的实操建议是:**如果你已经决定用新文件,就把 spring.factories 里的自动配置那一行删掉,千万不要两边都留。**兼容期不是让你双写的,而是给使用者一个平滑过渡的周期。
另外,注意有些 IDE 缓存会导致旧文件"看起来"还在生效,其实根本原因是刚改完文件没重新编译。遇到诡异现象时先mvn clean package一次,再观察结果。
4.3 顺序覆盖问题的处理细节
我遇到过一种更隐蔽的情况:在 2.7 中,spring.factories里的自动配置和imports文件里的自动配置同时存在时,前者的加载优先级高于后者,导致某些依赖顺序的类加载行为不一致。当你把所有类都迁到新文件后,这个顺序可能会变,类似"之前能启动,迁移后启动报循环依赖"。
这在数据源和事务管理器、缓存和敏感配置之间比较常见。解决方法是利用@AutoConfigureBefore、@AutoConfigureAfter明确指定顺序,并且在高需求场景下减少条件注解的"碰运气"式装配。还有一个更直接的调试方式:在application.properties里加:
spring.autoconfigure.exclude=com.example.DeprecatedAutoConfiguration如果你怀疑某个自动配置类是循环依赖的诱因,可以先排除它,观察启动是否恢复正常。如果恢复正常,说明这个配置类与自己的其他配置有顺序冲突。
4.4 升级经验的一些补充
最后分享几个代码审计之外的细节。
第一,旧项目中如果自定义了META-INF/spring.factories,迁移时不要只改文件内容,还要检查测试代码。很多测试类里写死了SpringBootTest(classes = {DemoAutoConfiguration.class}),不会走到实际的自动配置加载流程,所以测试通过不代表生产环境的加载没问题。最好加一个冒烟测试,用ApplicationContextRunner来专门验证自动配置加载场景,这类测试在升级中价值极高。示例:
@Test void autoConfigurationShouldLoad() { new ApplicationContextRunner() .withConfiguration(AutoConfigurations.of(DemoAutoConfiguration.class)) .withPropertyValues("demo.enabled=true") .run(context -> { assertThat(context).hasSingleBean(DemoService.class); }); }第二,如果你的项目打算使用 Spring Boot 3 的 AOT 或原生编译,尽量把自由发挥的地方收敛一点,比如不要动态生成过滤器注册等。那类操作在新的自动配置机制下也能工作,但需要额外的官方 hints,排查起来比较费力。
第三,养成打开debug=true看启动日志的习惯,特别是升级版本后第一次启动。自动配置匹配结果里包含了大量运行环境信息,比任何代码分析工具都直观。日志里面不仅能看到哪些配置被加载、哪些没被加载,还能看到每个条件注解的判断理由,排查问题时几乎可以替代"断点 + 猜测"的低效循环。
最后再补充一个小技巧:如果在多个 jar 包中,有两个org.springframework.boot.autoconfigure.AutoConfiguration.imports文件,Spring Boot 会合并中间的行,所以不用担心多个依赖互相覆盖。这一点看起来简单,但真的解决了老spring.factories时代同名 key 覆盖顺序不可控的遗留问题。要写自动配置的人,记住了:新文件名就是自动配置的身份证,路径错、名字错、编码错,结果都是三无——无日志、无异常、无 Bean。希望这篇内容能在你升级或写 starter 时少走一些弯路。