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 是三个注解的组合别名,展开后等价于:

等价写法(示意)Java
// @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 的以下文件中读取候选类名:

META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.importsProperties
# 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 等。两个文件职责不交叉,不要混淆。

@EnableAuto Configuration @Import AutoConfiguration ImportSelector DeferredImportSelector 读取 AutoConfiguration .imports 文件 每行一个全限定类名 返回 候选自动 配置类列表
图 3.1自动配置发现链:@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 的风格:

MyDataSourceAutoConfiguration.javaJava
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 对照

@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 赢"的效果——不是魔法,是 排序 + 条件求值时机 的精确配合。

时间 用户 @Configuration 解析 + 注册 BeanDef DeferredImportSelector 触发,读 .imports OnBeanCondition 求值 (REGISTER_BEAN 阶段) 看到用户 BeanDef → 退让 自动配置 @Bean 跳过 ① 先注册 ② 最后执行 ③ 关键时机 ④ back off 两个关键点: ① DeferredImportSelector 保证自动配置在用户配置之后处理 ② OnBeanCondition 在 REGISTER_BEAN 阶段求值,此时用户 BeanDefinition 已在注册表中
图 3.2解析顺序时间轴:用户配置先注册 BeanDefinition → DeferredImportSelector 最后触发读取自动配置候选 → OnBeanCondition 在 REGISTER_BEAN 阶段求值时看到用户 Bean → 自动配置 @Bean 退让。注意:OnBeanCondition 是 ConfigurationCondition,选择 REGISTER_BEAN 阶段正是这一切成立的技术基础。
.imports 原始候选列表 140+ 全限定类名 AutoConfigurationImportFilter ImportFilter 快速剪枝 OnClassCondition / OnWebApp ConfigurationClassPostProcessor @ConditionalOnXxx 逐类求值 (含 REGISTER_BEAN 阶段) OnBeanCondition 在此退让 最终装配的 Bean 注册进单例池
图 3.3候选自动配置类的筛选漏斗:原始列表 → 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

先合上教程,把答案写下来再展开对照。

  1. META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 取代了 spring.factories 的哪部分?spring.factories 现在还在用吗?
  2. @ConditionalOnMissingBean 为什么能让用户定义的 Bean 覆盖自动配置的默认 Bean?请回答到机制层面(不能只说"因为它检查 bean 存不存在")。
  3. 两个自动配置类 A 和 B,A 里有 @Bean @ConditionalOnMissingBean 依赖先看到 B 注册的某个 Bean 才能退让——如果不做任何额外声明,这个行为可靠吗?如何修复?
答案(先做完再展开)
  1. .imports 文件只取代了旧 spring.factories 中 org.springframework.boot.autoconfigure.EnableAutoConfiguration 键对应的自动配置类列表(2.7 起迁移,Boot 3.x 两者兼容,Boot 4.0 仅读 .imports)。spring.factories 仍用于注册 EnvironmentPostProcessor、FailureAnalyzer、AutoConfigurationImportFilter 等扩展点,两个文件职责不交叉。
  2. 原因是两个相互配合的机制:① AutoConfigurationImportSelector 是 DeferredImportSelector,在 ConfigurationClassPostProcessor 同轮次中最后被处理,确保用户 @Configuration 的 BeanDefinition 先于所有自动配置类注册进容器;② OnBeanCondition 实现了 ConfigurationCondition,选择在 REGISTER_BEAN 阶段求值——此时 BeanDefinition 注册表中已有用户的 Bean,条件求值结果为"目标类型已存在",自动配置的 @Bean 方法直接跳过。两者缺一不可。
  3. 不可靠。两个自动配置类之间的相对顺序默认未定义,取决于 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 的依赖(避免传递给用户工程)。