Spring Boot · 第 03 章
自动配置——"Boot"之所以是 Boot
上一章的 refresh() 在 invokeBeanFactoryPostProcessors 阶段加载所有 BeanDefinition——这一章下探:其中那批"自动配置"的 BeanDefinition 是怎么被发现和筛选的。
本章你将建立的 schema
- starter = 精选传递依赖(不含自动配置代码),与自动配置类是配套的两半
@SpringBootApplication= 三注解;AutoConfigurationImportSelector是DeferredImportSelector,最后执行- 自动配置类登记在
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports,经@ConditionalOnXxx逐层筛选 - "用户优先"= 排序 +
ConfigurationCondition在REGISTER_BEAN阶段求值,不是魔法
3.1starter 是什么
starter 是一组精选的传递依赖描述符,把一个功能切片需要的 jar 一次拉齐。
引入一个功能往往涉及多个 jar 版本协调(HTTP 客户端 + 序列化库 + 连接池……),版本矩阵靠工程师手动维护容易踩漏。starter 把选型决策固化成一个坐标,工程师声明意图、Boot 承担版本协调。
以 spring-boot-starter-web 为例:它拉入 spring-webmvc、内嵌 Tomcat、Jackson 等,classpath 上有了这些类,自动配置里的 @ConditionalOnClass 才能命中相应的装配逻辑。starter 和自动配置是配套的两半:starter 负责把类放到 classpath,自动配置据此决策是否装配。
starter 本身通常只有一个 pom.xml,不含任何 Java 代码——装配代码在 spring-boot-autoconfigure 里,与 starter 分离正是为了让工程师能在不用 starter 的情况下单独声明依赖,而自动配置仍然生效。
3.2@SpringBootApplication 展开
@SpringBootApplication 是三个注解的组合别名,展开后等价于:
// @SpringBootApplication 等价于同时标注以下三个注解:
@SpringBootConfiguration // @Configuration 的别名,声明主类为配置类
@EnableAutoConfiguration // 触发自动配置发现流程(见 3.3)
@ComponentScan // 以主类所在包为根包扫描组件
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
}
@ComponentScan 的扫描根包是主类所在包——这是"主类要放在最外层包"的根本原因:若主类嵌套在子包中,同级或上层的 @Component 就不在扫描范围内。
@EnableAutoConfiguration 通过 @Import(AutoConfigurationImportSelector.class) 挂载导入选择器,这是整个自动配置发现机制的入口,第 3.3 节将沿这条链深入。
3.3自动配置如何被发现(机制核心)
AutoConfigurationImportSelector 实现的是 DeferredImportSelector,而非普通 ImportSelector。这一细节至关重要:ConfigurationClassPostProcessor(第 1 章的 BFPP)在处理所有 @Configuration 类时,会把 DeferredImportSelector 的调用推迟到当前轮次所有普通配置类都处理完之后才执行——这正是"用户配置先注册、自动配置后处理"的机制起点(3.5 节进一步展开)。
AutoConfigurationImportSelector.getCandidateConfigurations() 内部调用 ImportCandidates.load(AutoConfiguration.class, classLoader),从 classpath 上所有 jar 的以下文件中读取候选类名:
# Spring Boot 4.0.x — 自动配置类注册文件
# 2.7 起取代旧的 spring.factories 中的 EnableAutoConfiguration 键
# 每行一个全限定类名
org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration
org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration
# ... 共约 140+ 行
.imports 文件仅存放自动配置类(标注了 @AutoConfiguration 的类)。spring.factories 文件仍然使用,但负责其他扩展点:EnvironmentPostProcessor(如加载 application.yml 的 ConfigDataEnvironmentPostProcessor)、FailureAnalyzer、AutoConfigurationImportFilter 等。两个文件职责不交叉,不要混淆。
@EnableAutoConfiguration 挂载 AutoConfigurationImportSelector,后者读取 .imports 文件返回候选类名列表。注意:AutoConfigurationImportSelector 是 DeferredImportSelector,整个调用链在同轮次所有普通 @Configuration 处理完毕后才触发。
3.4@Conditional 筛选
候选类名列表在真正解析之前还要经过两轮筛选:
第一轮(快速剪枝):AutoConfigurationImportFilter 对候选列表做 classpath 级快速扫描。其中 OnClassCondition、OnBeanCondition、OnWebApplicationCondition 三个实现会检查 .imports 文件同目录的 ConditionalOnClass.index 等 AOT 索引文件,把 classpath 上明显缺失依赖的候选类一次性排除,避免后续解析大量必然不命中的配置类。
第二轮(逐类求值):通过过滤的候选类被 ConfigurationClassPostProcessor 正式解析,此时注解上的 @ConditionalOnClass、@ConditionalOnMissingBean、@ConditionalOnProperty 等按 Condition 接口逐个求值,不满足的类整体跳过。
以下是一个简化的自动配置类结构,模仿 DataSourceAutoConfiguration 的风格:
package com.example.autoconfigure;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import javax.sql.DataSource;
@AutoConfiguration // 声明为自动配置类(包含 @Configuration)
@ConditionalOnClass(DataSource.class) // classpath 上有 DataSource 才生效
public class MyDataSourceAutoConfiguration {
@Bean
@ConditionalOnMissingBean // 用户已定义 DataSource Bean 时退让
public DataSource dataSource() {
// 示意:返回一个默认 DataSource 实现
return buildDefaultDataSource();
}
private DataSource buildDefaultDataSource() {
// ...
return null;
}
}
如果工程师在 @SpringBootApplication 主类旁边写了一个 @Bean DataSource myDs(),上面这个自动配置的 dataSource() 方法还会被调用吗?为什么?
展开答案(先停 10 秒)
不会被调用。@ConditionalOnMissingBean 由 OnBeanCondition 求值,而 OnBeanCondition 是 ConfigurationCondition,在 REGISTER_BEAN 阶段执行。此时用户的 DataSource BeanDefinition 已经注册(因为用户配置先于自动配置被处理),条件求值结果为"bean 已存在",整个 dataSource() 方法的 BeanDefinition 被跳过。这是 3.5 节的核心机制——背后靠的是排序,而非运行时检测。
常用 @ConditionalOnXxx 对照
| 注解 | 检查目标 | 典型用途 | 命中条件 |
|---|---|---|---|
@ConditionalOnMissingBean |
BeanDefinition 注册表 | 用户覆盖默认 Bean | 指定类型/名称的 Bean 尚未注册 |
@ConditionalOnClass |
classpath | 依赖 jar 存在才装配 | 指定类可被 classloader 加载 |
@ConditionalOnMissingClass |
classpath | 排除某依赖时的回退 | 指定类不可被加载 |
@ConditionalOnProperty |
Environment 属性 |
配置开关控制装配 | 属性存在且值符合预期(默认 havingValue 检查非 "false") |
@ConditionalOnWebApplication |
应用类型 | 仅在 Web 环境生效 | 上下文是 WebApplicationContext(Servlet 或 Reactive) |
@ConditionalOnBean |
BeanDefinition 注册表 | 依赖另一个 Bean 存在才装配 | 指定类型/名称的 Bean 已注册 |
@ConditionalOnSingleCandidate |
BeanDefinition 注册表 | 唯一候选或 Primary Bean | 指定类型恰好一个 Bean,或有 @Primary |
3.5用户优先 = 排序(揭穿机制)
"用户 Bean 总赢"并非 @ConditionalOnMissingBean 拥有什么特殊的"用户优先"语义——背后是严格的时序保证,可以分解为两个关键事实:
事实一:自动配置类被 AutoConfigurationSorter 排到最后解析。 DeferredImportSelector 的回调在同一轮 ConfigurationClassPostProcessor 处理中最后执行,这保证工程师写的 @Configuration 类(及其 @Bean 方法)对应的 BeanDefinition 先于任何自动配置类注册进容器。
事实二:OnBeanCondition 是 ConfigurationCondition,在 REGISTER_BEAN 阶段求值。 @Conditional 有两个求值时机:PARSE_CONFIGURATION(解析 @Configuration 结构时)和 REGISTER_BEAN(向容器注册 BeanDefinition 时)。OnBeanCondition 选择 REGISTER_BEAN 阶段,此时用户的 BeanDefinition 已在注册表中。自动配置类的 @Bean @ConditionalOnMissingBean 方法在求值时看到"该类型已有注册",条件不成立,直接跳过。
这两个事实叠加,产生了"用户 Bean 赢"的效果——不是魔法,是 排序 + 条件求值时机 的精确配合。
DeferredImportSelector 最后触发读取自动配置候选 → OnBeanCondition 在 REGISTER_BEAN 阶段求值时看到用户 Bean → 自动配置 @Bean 退让。注意:OnBeanCondition 是 ConfigurationCondition,选择 REGISTER_BEAN 阶段正是这一切成立的技术基础。
ImportFilter classpath 快剪 → @ConditionalOnXxx 逐类求值(含 REGISTER_BEAN 阶段的 OnBeanCondition) → 最终装配。注意:漏斗最窄处(朱红)是本章核心——OnBeanCondition 在 REGISTER_BEAN 阶段的退让行为决定了"用户优先"的实现方式。
"用户优先"依赖 DeferredImportSelector 把用户配置排在前、所有自动配置排在后。但两个自动配置类彼此之间的相对顺序未定义——若自动配置 A 的 @ConditionalOnMissingBean 依赖先看到自动配置 B 注册的 Bean,结果取决于字典序或加载顺序,行为不稳定。
正确的做法是在自动配置类上显式声明顺序依赖:
@AutoConfigureBefore(BAutoConfiguration.class)——让当前类在 B 之前处理@AutoConfigureAfter(BAutoConfiguration.class)——让当前类在 B 之后处理@AutoConfigureOrder(Ordered.LOWEST_PRECEDENCE)——数字越大越晚
这三个注解由 AutoConfigurationSorter 在排序阶段读取,不影响用户配置与自动配置的主顺序,只调整自动配置内部的相对次序。
§本章 self-check
先合上教程,把答案写下来再展开对照。
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports取代了spring.factories的哪部分?spring.factories现在还在用吗?@ConditionalOnMissingBean为什么能让用户定义的 Bean 覆盖自动配置的默认 Bean?请回答到机制层面(不能只说"因为它检查 bean 存不存在")。- 两个自动配置类 A 和 B,A 里有
@Bean @ConditionalOnMissingBean依赖先看到 B 注册的某个 Bean 才能退让——如果不做任何额外声明,这个行为可靠吗?如何修复?
答案(先做完再展开)
.imports文件只取代了旧spring.factories中org.springframework.boot.autoconfigure.EnableAutoConfiguration键对应的自动配置类列表(2.7 起迁移,Boot 3.x 两者兼容,Boot 4.0 仅读.imports)。spring.factories仍用于注册EnvironmentPostProcessor、FailureAnalyzer、AutoConfigurationImportFilter等扩展点,两个文件职责不交叉。- 原因是两个相互配合的机制:①
AutoConfigurationImportSelector是DeferredImportSelector,在ConfigurationClassPostProcessor同轮次中最后被处理,确保用户@Configuration的 BeanDefinition 先于所有自动配置类注册进容器;②OnBeanCondition实现了ConfigurationCondition,选择在REGISTER_BEAN阶段求值——此时 BeanDefinition 注册表中已有用户的 Bean,条件求值结果为"目标类型已存在",自动配置的@Bean方法直接跳过。两者缺一不可。 - 不可靠。两个自动配置类之间的相对顺序默认未定义,取决于 classpath 扫描顺序或类名字典序,行为在不同环境可能不同。修复方法:在 A 上标注
@AutoConfigureAfter(BAutoConfiguration.class),让AutoConfigurationSorter保证 B 先处理、B 的 BeanDefinition 注册后 A 才求值@ConditionalOnMissingBean。
实现一个自定义 starter 的自动配置
为一个名为 my-metrics 的功能写自动配置,要求:工程师设置 my.metrics.enabled=false 可以完全关掉该功能;若工程师自己定义了同类型的 Bean,自动配置退让;该自动配置类必须在 MeterRegistryAutoConfiguration 之后处理(依赖 Micrometer 的 MeterRegistry 已装配)。
需要在自动配置类上叠哪几个注解、在哪个文件中登记这个类,以及 starter 的 pom.xml 要声明哪个依赖?
提示(卡住再展开)
注解组合:@AutoConfiguration(包含 @Configuration,且会被 ImportCandidates 扫到)+ @ConditionalOnProperty(prefix="my.metrics", name="enabled", matchIfMissing=true)(缺省视为开启)+ @ConditionalOnClass(MeterRegistry.class)(有 Micrometer 才生效)+ @AutoConfigureAfter(MeterRegistryAutoConfiguration.class)(顺序保证);Bean 方法加 @ConditionalOnMissingBean 实现退让。登记位置:在 starter 的 jar 里创建 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports,写入该类的全限定名。starter 的 pom.xml 通过 optional 或 provided 范围声明对 spring-boot-autoconfigure 的依赖(避免传递给用户工程)。