1. 自动配置加载机制:从 spring.factories 说起
接触过 Spring Boot 的同学应该都知道,Spring Boot 最让人省心的就是“自动配置”。也就是说,你引入一个spring-boot-starter-data-redis依赖,RedisTemplate 就能直接注入使用了。不用你手动写 @Bean,也不用你去记繁琐的配置类。这个魔法背后,核心就是服务发现机制,也就是 SPI 思想的体现。
在 Spring Boot 2.7 之前,这个机制是通过META-INF/spring.factories文件实现的。这个文件本质上是一种属性配置文件,里面通过键值对的方式声明了各种需要被框架加载的类。其中最关键的一组配置是这样的:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.example.demo.autoconfigure.CustomAutoConfiguration当 Spring Boot 启动时,EnableAutoConfiguration处理器会去扫描所有 jar 包里的spring.factories文件,把org.springframework.boot.autoconfigure.EnableAutoConfiguration这个 key 对应的所有 value 类全部实例化、解析条件注解、注入容器。这就是“自动配置”的原始实现路径。
不过这种配置方式有一个很明显的问题:key 和 value 都塞在同一个文件里,类名必须写完整路径,代码量一大文件就变得非常臃肿,而且因为是字符串解析,IDE 无法给你跳转,拼错一个字符只能等启动时报错才能发现。更麻烦的是,它其实没有区分“自动配置类”和“其他SPI扩展点”,框架内部像EnvironmentPostProcessor、ApplicationContextInitializer、FailureAnalyzer这类扩展也全都挤在一个文件里,时间久了维护体验确实不怎么样。
我在实际开发中第一次深入看这个文件,是因为排查一个“为什么第三方 starter 没有生效”的问题。当时打开依赖 jar 翻 spring.factories,才发现里面有几十行配置,各种 key 混在一起,阅读体验真的很糟糕。这也是 Spring Boot 官方后来下决心重做这套机制的直接原因之一。
2. 为什么会出现 AutoConfiguration.imports 这个新文件
2.1 老机制的核心痛点
spring.factories 的方式虽然能工作,但它的缺陷在 Spring Boot 2.x 时代越来越明显。
第一,所有自动配置类都必须写在同一个 key 下面。如果某个 starter 提供了多个自动配置类,要么用逗号分隔写在同一个 key 里,要么用多个 key 同时对应不同的自动配置组。不管哪种写法,可读性都很差,特别是大型开源组件,比如 mybatis-spring-boot-starter、spring-cloud 相关组件,配置内容非常长,排查问题的时候眼睛都快看花了。
第二,加载顺序的控制比较隐晦。虽然可以通过@AutoConfigureBefore、@AutoConfigureAfter注解控制自动配置类之间的先后顺序,但前提是你得知道别人 starter 里的自动配置类叫什么名字。如果互相引用的 jar 版本变了,类名变了,这个控制就会变得很脆弱。配合 spring.factories 的扁平化结构,几乎无法从文件层面直观看出依赖关系。
第三,Spring Boot 官方在 2.7 版本开始为 JDK 9 以上版本引入的模块化系统做准备,需要一种更规范的配置声明方式。spring.factories 是很多框架共用的约定,但“只声明自动配置类”这种语义,需要一个独立且更明确的文件来承载。于是org.springframework.boot.autoconfigure.AutoConfiguration.imports就登场了。
2.2 新机制到底新在哪里
这个新文件的最大变化,不是文件名变长这么简单,而是它把“自动配置类的声明”和“其他扩展点的声明”彻底分开了。
旧的 spring.factories 里,我们是这样写的:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.example.custom.CustomAutoConfiguration新的 AutoConfiguration.imports 文件,我们不需要 key,也不需要=符号,直接每行写一个自动配置类的全限定名:
com.example.custom.CustomAutoConfiguration com.example.custom.AnotherAutoConfiguration文件放置的路径也有讲究,必须是META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports。注意看位置,它是在META-INF/spring/这个新目录下,文件名就是org.springframework.boot.autoconfigure.AutoConfiguration.imports。这个命名规则非常 Java SPI 风格,接口全限定名作为文件名前缀。
Spring Boot 读取这个文件的时候,内部逻辑就用SpringFactoriesLoader的替代类AutoConfigurationImportSelector去加载。这个类在 2.7 版本中做了重大调整,新的getCandidateConfigurations方法实现里,会同时读取两个来源:老的 spring.factories 中EnableAutoConfiguration的配置和新的 AutoConfiguration.imports 文件里的配置。这也意味着 2.7 是一个兼容过渡版本,两种方式并存,你用自己的老 starter 也不会直接挂掉。
我用一个简单对比表整理了下两种方式的主要差别:
| 对比项 | spring.factories | AutoConfiguration.imports |
|---|---|---|
| 出现版本 | Spring Boot 1.0 开始 | Spring Boot 2.7 开始 |
| 默认启用版本 | 2.7 之前 | 3.0 起完全启用 |
| 文件位置 | META-INF/spring.factories | META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports |
| 配置格式 | key=value 属性格式 | 每行一个全限定类名 |
| 加载语义 | 可承载多种SPI扩展点 | 专门声明自动配置类 |
| 2.7 兼容性 | 被继续支持 | 被支持 |
| 3.0 支持情况 | 不再支持自动配置声明 | 强制使用新文件 |
3. 从源码层面看加载机制的前世今生
3.1 老机制加载链路
要真正掌握这个机制,光看文档不够,我建议直接跟一遍源码。在 Spring Boot 2.6 中,EnableAutoConfigurationImportSelector最终走到SpringFactoriesLoader.loadFactoryNames()方法,这个方法会扫描 classpath 下所有 jar 包里的META-INF/spring.factories文件,然后根据指定的 factoryTypeName,也就是 key,取出对应的类名列表。
public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) { ClassLoader classLoaderToUse = (classLoader != null ? classLoader : SpringFactoriesLoader.class.getClassLoader()); Map<String, List<String>> result = loadSpringFactories(classLoaderToUse); return result.getOrDefault(factoryType.getName(), Collections.emptyList()); }这里的loadSpringFactories方法会缓存所有扫描到的配置映射,避免每次调用都重新扫一遍所有 jar。这也是为什么老机制虽然文件难看,但性能上并不差的原因。
3.2 新机制加载链路
到了 Spring Boot 2.7 和 3.x,AutoConfigurationImportSelector的getCandidateConfigurations()方法逻辑变了,它不再只调用SpringFactoriesLoader.loadFactoryNames()了,而是通过一个新的getAutoConfigurationEntry()方法走。
新的核心加载代码近似如下:
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) { List<String> configurations = new ArrayList<>( SpringFactoriesLoader.loadFactoryNames(EnableAutoConfiguration.class, this.beanClassLoader)); configurations.addAll( AutoConfigurationImportSelector.this.importCandidates.loadCandidates()); return configurations; }注意里面的importCandidates,它的全名是AutoConfigurationImportSelector.ImportCandidates,这个内部类专门用来加载META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件。它的加载逻辑会基于 classpath 下所有 jar 的META-INF/spring目录查找符合命名规则的文件。
也就是说,2.7 版本中两种来源的结果是合并在一起的,甚至可能出现重复类名——官方也在文档中提示过,2.7 升级时最好做一次重复检查。到了 3.0 版本,SpringFactoriesLoader.loadFactoryNames(EnableAutoConfiguration.class...)这条路径被彻底移除,只保留importCandidates.loadCandidates()这一条路径。这就是为什么很多人在升级 Spring Boot 3.0 之后发现自动配置全部失效,基本都是因为只改了 spring.factories,没有添加新的 imports 文件。
从源码层面理解之后,你就知道为什么官方反复强调“同一个自动配置类不能同时出现在两个文件里”。两个方法在 2.7 版本中都会读到,如果同时存在,同一个配置类会被注册两次,轻则出现BeanDefinitionOverrideException,重则容器里出现两个相同类型的 Bean,导致注入混乱。
我在本地写演示项目时特意踩过这个坑,加了一个自定义自动配置类,2.7 环境下 spring.factories 没删,又加了 imports 文件,结果启动就报“Invalid bean definition with name 'xxx' defined in class path resource ... ”,排查半天才意识到是两个文件重复了。这个问题在 3.0 中反而不会出现,因为老文件不读了,只在 2.7 迁移期需要特别小心。
4. 项目实操:改造一个自定义 Starter
4.1 改造前的老写法
假设我们之前写了一个自定义 starter,项目结构大致如下:
my-starter/ ├── src/main/java/ │ └── com/example/custom/ │ ├── CustomProperties.java │ ├── CustomService.java │ └── CustomAutoConfiguration.java └── src/main/resources/ └── META-INF/ └── spring.factoriesCustomAutoConfiguration的内容大概是:
@Configuration @ConditionalOnClass(CustomService.class) @EnableConfigurationProperties(CustomProperties.class) public class CustomAutoConfiguration { @Bean @ConditionalOnMissingBean public CustomService customService() { return new CustomService(); } }spring.factories 里配置:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.example.custom.CustomAutoConfiguration再配合spring-autoconfigure-metadata.properties做条件过滤优化,这个 starter 在 Spring Boot 2.6 之前运行得很正常。
4.2 升级转换步骤
要把这个 starter 升级到兼容 Spring Boot 2.7 和 3.x,我按以下步骤操作:
第一步,新建一个目录META-INF/spring/,在里面创建文件org.springframework.boot.autoconfigure.AutoConfiguration.imports。
第二步,把 spring.factories 里的自动配置类那一行复制过去,去掉 key 前缀:
com.example.custom.CustomAutoConfiguration第三步,把 spring.factories 里除了EnableAutoConfiguration之外的内容保留下来。因为这个文件在 2.7 和 3.0 中仍然承担其他扩展点声明,不能完全删除。
例如如果你的 starter 里还有ApplicationContextInitializer或EnvironmentPostProcessor,这些还是要继续写在 spring.factories 里。
第四步,检查CustomAutoConfiguration的注解。建议把@Configuration注解换成@AutoConfiguration。这个注解是 2.7 新增的,语义上更清晰,它内部也是组合了@Configuration(proxyBeanMethods = false),能够让容器里配置类的代理方式更轻量。
@AutoConfiguration @ConditionalOnClass(CustomService.class) @EnableConfigurationProperties(CustomProperties.class) public class CustomAutoConfiguration { // ... }注意@AutoConfiguration在 2.7 版本中还支持通过after、before属性声明顺序,比原来分开写@AutoConfigureAfter、@AutoConfigureBefore更集中,推荐迁移时顺手一起整理了。
4.3 验证加载是否成功
改造完成后,最直接的验证方式就是在启动类上加一个 debug 断点,或者直接看自动配置报告。在 spring.factories 时代我们习惯用debug=true在application.properties里面开启自动配置报告。新机制下这个依然有效,启动日志会输出一个CONDITIONS EVALUATION REPORT,里面能明显看到你的CustomAutoConfiguration是 positive match 还是 negative match。
如果你不想开 debug,也可以通过ApplicationRunner打印所有自动配置类来判断:
@SpringBootApplication public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } @Bean public ApplicationRunner configChecker(ApplicationContext context) { return args -> { String[] names = context.getBeanDefinitionNames(); Arrays.stream(names).filter(name -> name.contains("custom")) .forEach(System.out::println); }; } }如果启动后能看到customService的 Bean 名称被打印出来,说明加载链路没问题。
5. 常见问题与排查技巧实录
这节我整理了自己折腾这套迁移时遇到的高频问题,每一项都是实际踩过坑之后的总结,希望对正在迁移的你有帮助。
第一个坑:升级 3.0 后自动配置静默失效
典型现象是引入 starter 后,代码里注入自定义 Bean 时直接NoSuchBeanDefinitionException。原因就是上面说的,老 starter 只有 spring.factories,没有 AutoConfiguration.imports。3.0 完全不读 spring.factories 里的EnableAutoConfigurationkey 了,所以你的配置类根本没有被加载。
排查思路很简单,先看 jar 包里的META-INF/spring目录是否存在对应 imports 文件。如果不存在,升级没商量。如果 starter 是第三方维护的,就直接查它的版本兼容性说明。
第二个坑:自动配置类的条件注解判断不通过
用@ConditionalOnClass时有个比较隐蔽的坑,当配置类被@AutoConfiguration标注后,类的加载时机提前了,条件注解判断时某些类还没进入 classpath 的加载范围。这时候如果条件判断用的是@ConditionalOnClass(name = "com.example.SomeClass")这种字符串方式,要注意字符串别写错,否则条件永远不成立,但也不会报错,就是非常难查。
第三个坑:文件编码问题
AutoConfiguration.imports 文件如果用了非 UTF-8 编码,类名末尾可能出现不可见字符,启动时就会报ClassNotFoundException或者IllegalArgumentException,关键是那个类名看着完全正常,复制到 IDE 里跳转也正常。这个坑我遇到过一次,是某次在 Windows 机器上手动创建文件,编辑器默认编码不是 UTF-8。建议用 IDE 新建文件,并且确认右下角编码是 UTF-8。
第四个坑:多个 jar 包都声明了同名自动配置类
在 Spring Boot 3 之后,AutoConfiguration.imports 的处理逻辑会自动去重加载候选,但如果有两个不同 jar 里的全限定类名恰好一样,也就是类名冲突了,大概率是某个类被复制到了两个模块里。这个问题在 2.7 迁移期更容易出现,因为两个来源都会加载同一个类名。你用mvn dependency:tree排查重复依赖时,重点关注 starter 之间的传递依赖。
第五个坑:条件报告里出现“negative match”但不清楚原因
这个很普遍。自动配置报告里会列出匹配条件和实际匹配值。比如@ConditionalOnProperty配置了custom.enabled=true,但你的配置文件里没写或者写错了,就会导致 negative。我建议排查这类问题时,直接看启动日志里Negative matches区块中的内容,它会精确到哪个属性不满足。比瞎猜代码快得多。
下面给一个简单对照表,方便遇到问题时快速定位:
| 问题现象 | 可能原因 | 处理建议 |
|---|---|---|
| 升级 3.0 后自定义 starter 失效 | 缺少 AutoConfiguration.imports 文件 | 新建文件,迁移自动配置声明 |
| Bean 重复定义 | 两个文件同时声明同一个类 | 移除其中一个来源 |
| 条件判断未通过 | 属性配置错误或类不存在 | 查看启动报告 Negative 明细 |
| 启动报类找不到 | 类名拼写错误或编码异常 | 复制全限定名到 IDE 核对 |
| 顺序关系失效 | spring.factories 删除后 @AutoConfigureAfter 被绕过 | 改用 @AutoConfiguration(after = ...) 注解属性 |
6. 关于迁移时机的务实建议
如果你维护的是一个内部中间件 starter,强烈建议直接按新版本规范改造。Spring Boot 2.7 是一个很好的落地窗口,因为老文件新文件同时生效,你能在生产环境灰度验证。如果项目还在用 2.6 甚至 2.3,我个人建议也不要有太大心理负担,做好兼容思路就行。
我在给团队做升级时采用的方案是“双文件并存”。也就是在 2.6 版本的项目里不会去新增 imports 文件,因为 2.6 的加载源码根本不会读它,加了反而白加。真正需要改动的是在启动类的@SpringBootApplication上临时排除受影响的自动配置类,再配合新版 starter 上线。当基础版本升到 2.7 后,再把 spring.factories 里的自动配置声明切换到 imports 文件,一个版本周期内完成所有迁移。
这里还有个小技巧:如果担心 starter 在某些老项目里仍被引用,可以在新 jar 里同时保留两个文件,但一定要保证不要重复声明同一个类。比如自动配置类 A 只在 imports 文件里声明,另一个老扩展点比如FailureAnalyzer继续放在 spring.factories 里。这样老项目升到 2.7 后不会挂,新项目 3.0 也能正常用。
如果你用 Maven 管理项目,建议在插件配置里添加spring-boot-configuration-processor和spring-boot-autoconfigure-processor。第二个处理器会在编译期帮你生成META-INF/spring-autoconfigure-metadata.properties以及校验自动配置类注解使用的正确性,很多低级笔误在编译阶段就能暴露出来,不用等到运行时启动才排查。
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-autoconfigure-processor</artifactId> <optional>true</optional> </dependency>说实话,我从 spring.factories 一路踩到 AutoConfiguration.imports,最大的感受是:Spring Boot 官方在一点点把“约定优于配置”这件事做得更精细了。老的 SPI 机制很灵活,但太灵活就会导致混乱。新机制让自动配置声明位置更聚焦、内容更清晰、IDEA 的支持也更友好,类名直接能跳转。如果你正在搭建自己的 starter,或者正处于升 3.x 的路上,建议尽早切换到新机制,越晚迁移,老 jar 积累越多,成本越高。