引言
Spring Boot 3 对自动装配机制做了一次"静默但彻底"的重构。说它静默,是因为升级后旧方式不会报错,只会静默失效------配置类根本不加载,功能凭空消失,排查成本极高。说它彻底,是因为注册文件、加载入口、条件评估策略全部换了一遍。
本文先对比 Boot 2 与 Boot 3 的核心差异,再拆解两种配置加载机制(扫描包下 vs 扫描包外),接着深入父子容器与条件评估的关系,最后完整梳理 Boot 3 的自动装配流程与迁移注意事项。
一、Spring Boot 2 vs 3:核心差异一览
| 对比维度 | Spring Boot 2.x | Spring Boot 3.x |
|---|---|---|
| 注册文件 | META-INF/spring.factories |
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports |
| 文件格式 | Key=Value,逗号分隔 |
纯文本列表,每行一个类名 |
| 推荐注解 | @Configuration |
@AutoConfiguration |
| 加载入口 | SpringFactoriesLoader |
ImportCandidates(延迟加载) |
@ConditionalOnMissingBean 默认范围 |
ALL(含父容器) |
CURRENT(仅当前容器) |
这些差异背后,是 Spring Boot 对自动装配机制的一次系统性重新设计。下面逐项展开。
二、两种配置加载机制:扫描包下 vs 扫描包外
在深入自动装配细节之前,必须先厘清一个容易混淆的问题:一个 @Configuration 类到底是怎么被 Spring 发现的? 答案取决于它是否在入口类的扫描包下------这是两套完全不同的机制。
2.1 在扫描包下:靠 @ComponentScan 加载
@SpringBootApplication 本身是一个组合注解,其中包含 @ComponentScan。默认情况下,它会扫描入口类所在包及其所有子包。
java
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
如果你通过 scanBasePackages 显式指定扫描范围:
java
@SpringBootApplication(scanBasePackages = "com.example")
public class Application { }
那么 com.example 包及其子包下的 @Configuration、@Service、@Controller、@Component 等都会被注册到容器。
关键特征 :这是"暴力全量扫描",没有条件判断。只要类在包下且带有受支持的注解,就会被加载。它适合项目内部的业务配置。
2.2 在扫描包外:靠 AutoConfiguration.imports 加载
不在扫描包下的配置类,无法被 @ComponentScan 发现。如果希望它被 Spring Boot 自动装配,就必须写入指定文件:
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
每行一个类的全限定名:
com.example.starter.FooAutoConfiguration
com.example.starter.BarAutoConfiguration
通过 @EnableAutoConfiguration 机制,Spring Boot 会读取这个文件,加载其中登记的类,并经过 @Conditional 条件判断后决定是否生效。
关键特征:这是"按需加载",每个类都要通过条件评估。它适合第三方 Starter、跨模块复用的配置。
2.3 两条路径对比
| 维度 | 扫描包下 | 扫描包外 |
|---|---|---|
| 发现机制 | @ComponentScan 包扫描 |
读取 .imports 文件 |
| 加载方式 | 全量加载,无条件判断 | 按需加载,条件评估后生效 |
| 适合场景 | 项目内部业务配置 | 第三方 Starter、跨模块复用 |
| 是否需额外登记 | 不需要,在包下即可 | 必须写入 .imports 文件 |
| 受 Boot 3 变更影响 | 不受影响 | 受影响,注册文件和注解都变了 |
一句话总结 :在扫描包下靠 @ComponentScan 扫描加载;不在包下靠 .imports 文件登记后由自动配置机制按条件加载。两者是互补关系,不是同一套机制。
三、Spring Boot 2.x:基于 spring.factories
3.1 注册方式
在 Boot 2.x 中,自动配置类通过在 META-INF/spring.factories 文件中声明 org.springframework.boot.autoconfigure.EnableAutoConfiguration 这个 Key 来注册:
properties
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.starter.FooAutoConfiguration,\
com.example.starter.BarAutoConfiguration
启动时,SpringFactoriesLoader 会扫描类路径下所有 jar 包的该文件,合并读取所有配置类。
3.2 存在的问题
spring.factories 是一个"多功能混用"的文件。除了自动配置,它还承载监听器、初始化器、失败分析器等各类扩展点:
properties
org.springframework.context.ApplicationListener=\
com.example.MyListener
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.FooAutoConfiguration
这导致两个问题:
- 文件臃肿:所有扩展点挤在一个文件里,职责不清。
- 解析效率低:每次启动都要解析全部 Key,即使只关心自动配置。
3.3 加载入口
Boot 2.x 通过 @Import(AutoConfigurationImportSelector.class) 触发自动配置加载。AutoConfigurationImportSelector 实现了 DeferredImportSelector 接口,但早期的加载时机和条件评估策略相对粗放。
四、Spring Boot 3.x:基于 .imports 的 SPI 规范化
4.1 注册方式
Boot 3 将自动配置的注册从 spring.factories 中彻底剥离,改用独立的文件:
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
文件格式变得极其简洁------纯类名列表,一行一个,不需要 Key,不需要逗号:
com.example.starter.FooAutoConfiguration
com.example.starter.BarAutoConfiguration
这种设计让文件职责单一:只服务自动配置,不再混杂其他扩展点。
4.2 底层加载逻辑
Boot 3 使用 ImportCandidates 类来读取 .imports 文件,替代了 Boot 2 的 SpringFactoriesLoader。核心变化是引入了延迟加载策略:
自动配置类的筛选和分组被推迟到所有用户自定义配置类解析完成之后才执行。
这个时机变化的意义在于:避免了因用户配置尚未就绪导致 @ConditionalOnMissingBean 等条件注解误判。在 Boot 2 中,如果自动配置类加载过早,可能看不到用户后定义的 Bean,从而错误地创建了本应被用户覆盖的 Bean。
4.3 专用注解 @AutoConfiguration
Boot 3 为自动配置类引入了专属注解 @AutoConfiguration。它本质上是一个"元注解",底层包含了 @Configuration,但额外提供了两个能力:
- 排序声明 :通过
after、before属性替代旧版的@AutoConfigureAfter/@AutoConfigureBefore。 - 强制
proxyBeanMethods = false:避免自动配置类中@Bean方法间的代理调用开销。
java
@AutoConfiguration(after = DataSourceAutoConfiguration.class)
public class MyAutoConfiguration {
// ...
}
五、父子容器:理解 @ConditionalOnMissingBean 范围变化的关键
5.1 什么是父子容器?
父子容器是一种层级化的 Bean 管理机制 ,目的是实现关注点分离 和模块隔离。
最经典的例子是传统的 Spring MVC 应用:
- 父容器(Root Context) :通常由
ContextLoaderListener创建,负责管理服务层(Service)、**数据层(Repository)**等全局共享的 Bean。 - 子容器(Servlet Context) :由
DispatcherServlet创建,负责管理控制器(Controller) 、视图解析器等 Web 层组件。
这种设计让 Web 组件不会污染全局服务,也让服务层可以被多个 Web 子模块共享。
在 Spring Cloud 或 Spring Boot 3 的一些场景中,也会用到父子容器。例如,Spring Cloud 的 Bootstrap 上下文就是主应用容器的父容器,用于从配置中心加载远程配置。
5.2 核心区别与可见性
| 维度 | 父容器 (Parent) | 子容器 (Child / 当前容器) |
|---|---|---|
| 定位 | 全局共享,存放基础服务 | 模块专属,存放 Web/业务组件 |
| Bean 可见性 | 看不到子容器的 Bean | 能看到父容器的 Bean |
| Bean 覆盖 | 被子容器同名 Bean 覆盖 | 可以覆盖父容器的同名 Bean |
| 生命周期 | 独立,通常先于子容器启动 | 独立,可以单独刷新或关闭 |
核心规则:子容器能"看见"父容器的 Bean,但父容器看不见子容器的 Bean。
5.3 这对 @ConditionalOnMissingBean 意味着什么?
这正是 search 属性发挥作用的地方。它决定了条件检查时,是只查"自己家"(当前容器),还是把"父母家"(父容器)也翻一遍。
SearchStrategy.CURRENT:只查当前容器。这是 Spring Boot 3 的默认值。如果某个 Bean 只存在于父容器中,这个条件会认为"缺失",从而触发自动配置。SearchStrategy.ALL:遍历整个层级(当前容器 + 所有祖先容器)。只要父容器或任何祖先容器里有这个 Bean,条件就会认为"已存在",从而跳过自动配置。这是 Spring Boot 2 的默认值。
5.4 为什么 Boot 3 要改成 CURRENT?
这个改动是为了避免误判,让父子容器的边界更清晰。
假设一个场景:你的应用(子容器)需要自动配置一个 MyService,但父容器(比如 Spring Cloud 的 Bootstrap 上下文)里恰好也有一个同类型的 MyService。
- 如果搜索策略是
ALL,自动配置会误以为"用户已经提供了这个 Bean",从而静默跳过,导致子容器中缺少必要的配置,功能异常。 - 改成
CURRENT后,自动配置只关心当前应用自己有没有定义这个 Bean,不会被父容器的干扰所影响,行为更符合直觉。
如果你确实需要让自动配置"尊重"父容器里已有的 Bean,就需要显式声明:
java
@ConditionalOnMissingBean(search = SearchStrategy.ALL)
六、Spring Boot 3 自动装配完整流程
下面按请求流转顺序,逐步拆解 Boot 3 的自动装配过程。
第一步:入口触发
@SpringBootApplication 是一个组合注解,其中包含 @EnableAutoConfiguration:
java
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
@EnableAutoConfiguration 内部通过 @Import(AutoConfigurationImportSelector.class) 导入自动配置选择器。
第二步:读取候选列表
AutoConfigurationImportSelector 底层调用 ImportCandidates.load(AutoConfiguration.class, ...),扫描类路径下所有 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件,得到候选自动配置类的全限定名列表。
第三步:过滤与去重
得到候选列表后,进行两轮处理:
- 排除 :移除被
@EnableAutoConfiguration的exclude属性或spring.autoconfigure.exclude配置排除的类。 - 去重:处理多个 jar 包中重复声明的类名。
第四步:条件匹配
对每个候选类逐一评估 @Conditional 系列注解。这是自动装配的核心决策环节,常见注解包括:
| 注解 | 作用 |
|---|---|
@ConditionalOnClass |
类路径存在指定类时生效 |
@ConditionalOnMissingBean |
容器中尚无该 Bean 时生效(允许用户覆盖) |
@ConditionalOnProperty |
指定配置属性满足条件时生效 |
@ConditionalOnWebApplication |
当前是 Web 应用时生效 |
Boot 3 对 @ConditionalOnClass 做了优化:改用 ClassLoader.loadClass,不再触发类的静态初始化块,避免加载类时产生意外副作用。
同时,@ConditionalOnMissingBean 的默认搜索策略从 ALL(包含父容器)收紧为 CURRENT(仅当前容器),父子上下文隔离更彻底。
第五步:注册 Bean
条件匹配通过的自动配置类被解析,其中的 @Bean 方法注册到 Spring 容器中,完成自动装配。
流程总结图
@SpringBootApplication
↓
@EnableAutoConfiguration
↓
@Import(AutoConfigurationImportSelector)
↓
ImportCandidates.load() 读取 .imports 文件
↓
过滤排除 + 去重
↓
@Conditional 条件匹配(默认只搜当前容器)
↓
注册 @Bean 到容器
七、迁移时的关键注意事项
7.1 自定义 Starter 必须迁移注册文件
如果你的 Starter 还在 spring.factories 中注册自动配置,升级到 Boot 3 后这些配置完全不会加载。需要:
- 新建
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件。 - 将原
spring.factories中EnableAutoConfiguration下的类名逐行复制过去。 - 注意:
spring.factories中的其他扩展点(监听器、初始化器等)仍需保留在原文件中,Boot 3 并未移除对它的支持,只是不再用它注册自动配置。
7.2 用 @AutoConfiguration 替代排序注解
将自动配置类上的 @Configuration 替换为 @AutoConfiguration,排序声明改用注解属性:
java
// Boot 2 写法
@Configuration
@AutoConfigureAfter(DataSourceAutoConfiguration.class)
public class MyAutoConfiguration { }
// Boot 3 写法
@AutoConfiguration(after = DataSourceAutoConfiguration.class)
public class MyAutoConfiguration { }
7.3 注意 @ConditionalOnMissingBean 的范围变化
默认只搜索当前容器。如果需要跨父子上下文查找,必须显式指定:
java
@ConditionalOnMissingBean(search = SearchStrategy.ALL)
7.4 扫描包下的配置类不受影响
需要区分两套机制:
- 在扫描包下 :靠
@ComponentScan扫描加载,与 Boot 版本无关。 - 不在扫描包下 :靠
.imports文件登记后由自动配置机制按条件加载。
只有后者受本次变更影响。
八、总结
Spring Boot 3 的自动装配重构,核心可以概括为四点:
- 注册文件独立化 :从混用的
spring.factories剥离为专用的.imports文件,职责单一、格式简洁。 - 加载时机延迟化 :
ImportCandidates延迟到用户配置解析完成后再评估,条件判断更准确。 - 条件评估严格化 :
@ConditionalOnClass不触发静态初始化,@ConditionalOnMissingBean默认只搜当前容器。 - 两套加载机制并行 :扫描包下靠
@ComponentScan全量加载,扫描包外靠.imports按需加载,各司其职。
对于普通业务项目,这些变化大多由 Spring Boot 自身处理,感知不强。但对于自定义 Starter 的开发者,迁移是必须的------而且由于旧方式静默失效,务必在升级后验证自动配置类是否真正生效。
参考方向 :Spring Boot 官方文档 Auto-configuration 章节、spring-boot-autoconfigure 源码中 ImportCandidates 与 AutoConfigurationImportSelector 的实现。