1. 自动配置注册这件事,Spring Boot 为什么越改越“绕”
我最早被 spring.factories 坑到,是在给公司封装一个内部基础组件的时候。明明写好了一个配置类,也在META-INF/spring.factories里声明了,结果接入方启动以后,配置死活不生效。排查了大半天,最后发现是我把EnableAutoConfiguration键写成了AutoConfiguration,差一个单词,整个自动配置就没被加载。后来 Spring Boot 2.7 开始推广META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports这个新文件,老同事的第一反应普遍是:又要学一套新规范?
其实这个变化并不突兀。如果你经历过 Spring Boot 1.x 到 2.x 再到 3.x 的演进,会发现整个核心思路一直在往“显式、可控、无魔法”的方向走。spring.factories最初的设计非常灵活,Spring 生态里大量组件都靠它做 SPI 扩展,但灵活过头就容易引入很多隐性问题。官方后来单拎出自动配置这一块,用一个新的 imports 文件单独管理,就是为了把“自动配置”从通用 SPI 机制里拆出来,明确边界,减少误用。
这篇文章不是简单的概念讲解,我会带着你从加载原理、文件格式、迁移步骤到排查技巧完整过一遍。不管你是刚接触 Spring Boot 的新手,还是已经在老项目里维护了多年自动配置的老手,读完后都应该能对这两个文件有清晰判断:该用哪个、什么时候迁移、出了问题去哪查。
2. spring.factories:老一代自动配置注册机制的完整拆解
2.1 spring.factories 的格式与加载原理
spring.factories是一个标准的 Properties 风格文件,位于META-INF/spring.factories。它最核心的功能就是给 Spring Boot 提供 SPI 扩展入口,你可以在里面配置一大堆 key-value,每个 key 对应一个扩展点,多个实现类用逗号分隔。
自动配置相关的写法长这样:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.example.starter.MyAutoConfiguration,\ com.example.starter.AnotherAutoConfiguration加载这个文件的逻辑在 Spring Boot 2.x 里主要由SpringFactoriesLoader.loadFactoryNames()完成。它会在 classpath 下扫描所有依赖 Jar 包里的META-INF/spring.factories,按 key 分组后获取对应的实现类列表。
有个细节值得注意:SpringFactoriesLoader本身是 Spring Framework 的东西,不是 Spring Boot 独有的。Spring 的spring-web、spring-test等模块也用它做内部扩展。自动配置只是复用了这套机制,把 key 规定成了org.springframework.boot.autoconfigure.EnableAutoConfiguration。
加载时机也很关键。Spring Boot 在启动阶段会先收集所有自动配置类,然后对这些类做条件评估,比如@ConditionalOnClass、@ConditionalOnMissingBean、@ConditionalOnProperty等。换句话说,spring.factories里写了哪个类,不代表一定会加载,还要经过条件注解这层过滤。当时我遇到配置不生效,就是条件注解也占了很大嫌疑——你把类写进文件了,但类上面一个@ConditionalOnProperty没有匹配上,启动时静默跳过,根本不会报错。
2.2 为什么官方最终决定让它“退役”
Spring Boot 2.7 发布时,官方在 Deprecation 说明里点得很清楚:spring.factories这个机制太通用了,导致很多第三方组件什么都往里塞——监听器、初始化器、环境后置处理器、自动配置类,全挤在同一个文件里。为了避免大项目里 JAR 冲突、资源重复加载、加载顺序不可控的问题,官方把自动配置这块单独抽了个新文件。
我把新旧机制的核心差异整理成了表格,方便对照:
| 对比维度 | spring.factories | AutoConfiguration.imports |
|---|---|---|
| 文件路径 | META-INF/spring.factories | META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports |
| 文件格式 | Properties 键值对 | 每行一个类全限定名 |
| 注册目标 | 通过指定 key 绑定自动配置 | 文件本身就是自动配置专属入口 |
| 适用版本 | Spring Boot 2.7 之前 | Spring Boot 2.7+ / 3.x 推荐 |
| 是否支持其他扩展点 | 支持(监听器、初始化器等) | 仅支持自动配置 |
有人觉得这是官方在“收紧生态”,我倒觉得这是一次很好的“职责划分”。以前你写一个 Starter,要把自动配置、监听器、ApplicationContextInitializer 全部声明在一个文件里,维护成本高,排查问题也费劲。现在自动配置单独一个文件,打开就知道这个 Jar 提供了哪些自动装配组件,配合 IDE 跳转也方便。
官方还特意做了兼容性设计:Spring Boot 2.7 / 2.8 过渡期内,两个文件都同时加载,spring.factories里的自动配置仍然有效,只是会在启动日志里打一条警告,提示你尽快迁移。到了 Spring Boot 3.0,spring.factories里的自动配置就不会再被扫描了。
2.3 我在老项目中用 spring.factories 踩过的坑
第一个坑是文件重复加载。项目里有两个模块都打出了META-INF/spring.factories,如果恰好还有依赖覆盖,比如 B 版本覆盖了 A 版本,SpringFactoriesLoader 读取到的内容可能不是你期望的那份。对自动配置来说,后果就是有些类被丢了,应用在启动时出现莫名其妙的 Bean 缺失。
第二个坑是注释和格式问题。spring.factories是 Properties 格式,行尾多了空格、反斜杠转义出错,或者逗号后面少个反斜杠导致换行被吞,都可能导致解析失败。这类问题不是报错,而是自动配置类完全不加载,排查起来非常费时间。
第三个坑是我一直建议团队戒掉的:把业务扩展类也塞进 spring.factories。比如有人为了方便,把某个ApplicationListener直接放在EnableAutoConfiguration同一个文件里,但这会导致自动配置装配 Bean 时,监听器还没注册,某些事件没有被处理。虽然可以通过@Order临时解决,但本质上违反了“扩展点各归其位”的原则。新机制把自动配置隔离出来,反而是对这类误用的一种纠正。
3. AutoConfiguration.imports:新机制的设计思路与最佳实践
3.1 新文件的命名规则与加载机制
新文件的完整路径是META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports。
文件名本身就包含了完整信息:org.springframework.boot.autoconfigure.AutoConfiguration是扩展点接口,.imports是扩展文件后缀。Spring Boot 在启动时会用ImportCandidates.load()方法去META-INF/spring目录下查找以AutoConfiguration.imports结尾的文件,然后按顺序读取每一行,去空格、去注释、过滤空内容,剩下的每个类全限定名都作为自动配置类注册。
这个文件只做一件事:声明自动配置类。如果你想注册其他类型的 SPI,比如ApplicationListener或ApplicationContextInitializer,不要放在这里,继续用spring.factories或者其他专门的机制。
举个例子,一个典型的新文件内容:
com.example.starter.MyAutoConfiguration com.example.starter.SupportAutoConfiguration注意,这里没有 key,没有等号,不需要逗号分隔,一个类名占一行。格式简化了,解析逻辑也简单了,对编译器、IDE、静态扫描工具都更友好。
加载顺序上,Spring Boot 会按照 classpath 下 Jar 的扫描顺序,还有文件名排序来决定先后。如果你需要严格控制多个自动配置类的加载顺序,可以配合@AutoConfigureBefore、@AutoConfigureAfter、@AutoConfigureOrder使用,这些注解在 imports 文件模式下依然有效。
3.2 新老机制如何共存
很多人担心迁移之后,老代码就立刻不能用了。其实 Spring Boot 2.7 的设计是“双轨并行”,官方也明确给出了兼容方案。
在 Spring Boot 2.7 和 2.8 版本里,spring.factories里的自动配置仍然会被加载,只是启动时会有一条 WARN 日志,类似:
The following auto-configuration classes are registered using the deprecated spring.factories mechanism: com.example.starter.MyAutoConfiguration Please migrate them to the new AutoConfiguration.imports file.这时候你可以两个文件同时存在:老项目里还没迁完的自动配置类留在spring.factories,新增的自动配置类直接写到 imports 文件。对新加的类来说,优先使用 imports;对旧类来说,可以等整体改造时再一起迁移。
到了 Spring Boot 3.0,情况就变了。spring.factories里的自动配置不再被识别,所有自动配置类必须放到 imports 文件里,或者使用@ImportAutoConfiguration显式导入。如果你是有第三方 Starter 依赖需要兼容老版本,可以在你的 Jar 里同时保留两种文件,通过判断 Spring Boot 版本来决定是哪一份被加载——不过这种兼容写法会增加复杂度,如果不是要发开源组件给大量用户用,我不建议在自己的业务项目里做这种兼容层。
3.3 怎么把老项目平滑迁移到新文件
第一步,先在src/main/resources/META-INF/spring/目录下新建org.springframework.boot.autoconfigure.AutoConfiguration.imports文件。
第二步,打开老项目里的spring.factories,找到org.springframework.boot.autoconfigure.EnableAutoConfiguration这个 key,把 value 部分的所有类名复制到 imports 文件里,每行一个。
第三步,把spring.factories里的自动配置 key 整个拿掉,只留下其他扩展点 key。如果某个 JAR 里只剩自动配置这一项,文件可以直接删除。
第四步,重新构建项目并启动,重点观察启动日志。2.7/2.8 版本下,如果迁移成功,WARN 日志会消失;低版本下,如果还有类似自动配置类重复注册的报错,说明spring.factories和 imports 文件同时存在且内容重复了,需要手动清理旧文件。
我的经验是,千万不要在一个版本里同时把所有模块都改完。“先迁,再删,再回归”三步走,分批处理,每一个模块升级后都要跑一遍启动自测,确认没有 Bean 报错再继续。毕竟自动配置出问题,表现通常不是“红色异常”,而是某个场景下 Bean 缺失导致的运行时错误,这类问题最隐蔽。
4. 动手实操:自己写一个自动配置 Starter
4.1 从零搭建自动配置模块
很多时候我们会把“自动配置”和“Starter”混为一谈,其实严格来说,Starter 是面向使用方的依赖入口,自动配置模块才是真正干活的 Jar。最佳实践是拆成两个模块:xxx-spring-boot-starter和xxx-spring-boot-autoconfigure,starter 只负责引入依赖,autoconfigure 包含配置类和自动配置声明。
我这边用一个简单的“短信发送器”组件做例子。目录结构如下:
sms-spring-boot-autoconfigure/ ├── src/main/java/ │ └── com/example/sms/ │ ├── SmsAutoConfiguration.java │ └── SmsProperties.java └── src/main/resources/ └── META-INF/ └── spring/ └── org.springframework.boot.autoconfigure.AutoConfiguration.imports在SmsAutoConfiguration里,我一般会使用@AutoConfiguration注解来标注这是自动配置类,同时配合@EnableConfigurationProperties绑定配置项:
@AutoConfiguration @EnableConfigurationProperties(SmsProperties.class) @ConditionalOnClass(SmsSender.class) public class SmsAutoConfiguration { @Bean @ConditionalOnMissingBean public SmsSender smsSender(SmsProperties properties) { return new SmsSender(properties.getEndpoint(), properties.getAccessKey()); } }这里的关键点是@ConditionalOnClass(SmsSender.class)。这个条件保证了一个场景:即使自动配置模块被引入了,但使用方项目里没有引入真正发短信的 SDK,配置类也不会生效,就不会白白创建无用的 Bean。
4.2 用 imports 文件注册配置类
在 resources 目录下创建org.springframework.boot.autoconfigure.AutoConfiguration.imports,内容如下:
com.example.sms.SmsAutoConfiguration就这么一行,没有其他修饰。这个文件不需要写版本号、条件、顺序,条件逻辑全部由配置类上的注解负责。
如果想控制多个自动配置类之间的先后顺序,可以使用注解,不依赖文件里行的顺序:
@AutoConfiguration(after = DataSourceAutoConfiguration.class) public class SmsAutoConfiguration { // ... }@AutoConfiguration注解还支持before属性,配合使用可以精确控制顺序。这个方法比在 imports 文件里手动调行更可靠,因为 imports 文件的行顺序受多个因素影响,比如 classpath 扫描顺序、构建工具的重命名规则,不太稳定。
注意一点:@AutoConfiguration注解是 Spring Boot 2.7 才引入的。如果你还在用 2.6 及以下的老版本,不能用这个注解,只能用@Configuration搭配 imports 文件的手段,不过那样就不是“官方支持”的新机制了,所以我建议用了 imports 文件就尽量把 Boot 升级到 2.7+,统一体验。
4.3 配置元数据与条件注解的配合
新机制虽然把声明文件简化了,但不代表条件注解的作用减弱了。恰恰相反,正因为 imports 文件里没有条件逻辑,所有“什么时候该加载”的判断都要靠配置类里的注解来把关。
常用的条件注解有这些:
| 注解 | 作用 | 使用场景 |
|---|---|---|
@ConditionalOnClass | classpath 存在某个类时才生效 | 判断可选依赖是否引入 |
@ConditionalOnMissingClass | classpath 不存在某个类时生效 | 预留降级方案 |
@ConditionalOnProperty | 配置项满足指定值时生效 | 按开关启用功能 |
@ConditionalOnBean | 容器中存在某个 Bean 时才生效 | 依赖其他组件 |
@ConditionalOnMissingBean | 容器中不存在某个 Bean 时生效 | 防止重复定义,支持用户自定义覆盖 |
@ConditionalOnWebApplication | 当前应用是 Web 应用时生效 | Web 场景专属配置 |
我在设计 Starter 时,习惯遵循几个原则。第一,所有条件注解尽量放在方法或类上,而不是放在 imports 文件里做判断——因为后者根本做不到。第二,@ConditionalOnMissingBean要放在@Bean方法上,给使用方留出覆盖入口。第三,配置属性必须有合理默认值,否则用户只引入依赖但没写配置时,启动就会因为绑定失败而报错。
另外,如果你的组件需要提供 IDE 配置提示,可以在META-INF/下加一个spring-configuration-metadata.json,把SmsProperties字段的说明、默认值、类型都描述清楚。这样用户在application.yml里写配置时会有自动补全,鼠标悬停还能看到说明。
5. 常见问题排查与避坑指南
5.1 明明配置了却未生效,排查思路
遇到“配置类没生效”这种问题,我建议按顺序排查。先确认是否把类写对了文件:检查自动配置类是不是真的在AutoConfiguration.imports中,注意路径大小写、中划线等细节。然后看 Spring Boot 版本:如果是 3.x,spring.factories里的自动配置不会再被加载;如果是 2.7/2.8,可能还有兼容处理后遗症。再看条件注解:在启动日志中搜索 “ConditionEvaluationReport”,或者把debug=true打开,自动配置报告中会详细列出哪些类被匹配、哪些被拒绝,以及被拒绝的具体原因。最后确认依赖是否真的被引入了:在gradle dependencies或mvn dependency:tree里检查 Starter 是不是真的进入了使用方项目的 classpath。
这里有个我屡试不爽的小技巧:在自动配置类里临时加一个静态初始化日志,比如static { System.out.println("autoconfig loaded"); },如果启动时没有打印,说明自动配置类根本没有被加载,问题在注册层面;如果打印了但 Bean 没注入,问题在条件注解或 Bean 定义层面。
5.2 新旧文件同时存在时的优先级与冲突
我见过不少团队在迁移过程中,新旧文件同时存在,导致自动配置类被重复注册。在 Spring Boot 2.7 / 2.8 版本下,同一个类如果既出现在spring.factories又出现在 imports 文件里,Spring Boot 会尝试去重。大部分情况下不会报错,但会有一个隐患:两个来源的加载顺序不同,可能导致某个自动配置类在另一个 Bean 还没准备好时就被执行了,从而触发NoSuchBeanDefinitionException。
如果出现这种问题,最快的解决方式是把旧文件里的自动配置 key 彻底删掉。不要把spring.factories和 imports 文件里的自动配置混着写,要么全在新文件,要么全在旧文件。
顺便提一个容易忽略的点:spring.factories里除了EnableAutoConfiguration之外,还有很多 key,比如ApplicationContextInitializer、EnvironmentPostProcessor等。这些 key 在新机制下不会被替换。也就是说,即使你切换到 imports 文件,老文件里其他扩展点也别忘了保留。
5.3 一些容易忽略的细节
- imports 文件末尾建议保留一个空行,有些文件解析器和版本控制工具对“最后一行无换行符”会打警告。
- 类名必须是完整的全限定名,比如
com.example.sms.SmsAutoConfiguration,不能写SmsAutoConfiguration或com.example.sms.*。 - 文件中可以写注释吗?可以,
#开头行会被忽略,但建议少用,毕竟这个文件越简洁越不容易出错。 - 自动配置类是懒加载还是立即加载?Spring Boot 2.2 之后默认启用了延迟初始化,自动配置类的实例化可能比想象中晚,但这和注册机制无关,不必担心。
- 如果使用 Spring Native 或 GraalVM 等场景,imports 文件比
spring.factories更容易被静态分析识别,这也是新机制的另一个优势。
6. 从 spring.factories 到 imports 文件的完整迁移清单
为了照顾那些懒得从头读的读者,我把整个迁移过程整理成了一份可直接照做的清单。
| 步骤 | 操作 | 说明 |
|---|---|---|
| 1 | 升级 Spring Boot 到 2.7+ | 低于 2.7 无法使用新文件 |
| 2 | 新建 imports 文件 | 路径为META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports |
| 3 | 将spring.factories的自动配置类复制到新文件 | 每行一个类全限定名 |
| 4 | 删除spring.factories中的自动配置 key | 保留其他扩展点 key |
| 5 | 检查自动配置类注解 | 使用@AutoConfiguration替代@Configuration,如需排序使用before/after属性 |
| 6 | 开启 debug 日志回归 | 确认启动日志中不再出现 deprecated 提示 |
| 7 | 多模块并行迁移 | 业务模块逐一改造,不一次性全量切换 |
按这个清单走,一个中等规模的 Starter 改造基本在一个小时内能完成。当然,如果项目的启动链路上还有自定义SpringFactoriesLoader逻辑或古老的@EnableAutoConfiguration排除逻辑,就要额外小心,这些逻辑默认不兼容新文件。
7. 我对这个变化的几点真实感受
从最开始的spring.factories到现在的AutoConfiguration.imports,我最大的感受是 Spring Boot 正在把“约定优于配置”这个口号贯彻得更彻底。老机制虽然灵活,但灵活不等于清晰——你可以在一个文件里声明几乎所有扩展点,但代价是加载逻辑黑了,排错靠猜。新机制把自动配置单独摘出来,文件路径、文件命名、文件格式都“一眼可见”,这是减少隐式魔法的关键一步。
对我自己而言,写组件时也变得更加克制了。以前一个spring.factories里什么都能写,我可能顺手就把配置类、监听器都塞进去,现在分文件、分职责,类结构自然就清晰了。如果你的项目正在升级 Spring Boot 3.x,或者你要发一个开源 Starter,我的建议是尽早完成迁移,别等到官方彻底不支持再动手。迁移本身不复杂,真正费时间的永远是那些被隐藏了的问题——与其在升级时被兼容性坑得焦头烂额,不如现在花半小时把这一步做干净。