自动配置原理:从 spring.factories 到 AutoConfiguration.imports

bee2026-10-0871 分钟0 次阅读
为什么引入一个 starter 就自动有了数据源和事务管理器?拆解 AutoConfigurationImportSelector 的加载链路、@AutoConfiguration 的排序艺术与条件装配的组合拳。
1 / 141
小节
〇、30 秒看懂
2 / 141

前两篇你已经会「跑起来」和「拆开那个三合一注解」了。这一篇回答最容易卡住的那个问题:为什么我只加了一行依赖,数据源、事务管理器、JSON 转换器就全都有了?答案不是魔法,而是一条很朴素的流水线:jar 包里预先放着一份候选清单,启动时 Boot 把清单读出来,逐个问几个「你配了吗 / 你有这个类吗」的问题,只把通过的那些注册进容器。整篇文章就是把这条流水线拆成四段看清楚,再教你一件事——它没生效时,怎么让 Boot 自己开口告诉你为什么。

3 / 141
类比

精装房的水电家具都是提前配好的,但你一旦把自己的沙发摆进客厅,装修方就不会再往那儿放它的沙发。自动配置就是这套「先看你有没有安排,没有才补」的规矩:@ConditionalOnMissingBean——客人没带伞,店里才借一把;你自己带了伞,店员看一眼就把伞架收回去。所以下面第二节才会反复强调「延迟导入」:Boot 一定要等你把话说完,才决定要不要替你配。

4 / 141
架构图
图 · 两条进容器的路:扫描 vs 清单登记
图 · 两条进容器的路:扫描 vs 清单登记
5 / 141

学完这一篇,你应该能回答三个问题:

6 / 141
  • 候选清单从哪个文件读来?spring.factories 和 AutoConfiguration.imports 差在哪个版本?
  • 一个自动配置类要满足哪些条件才会真的生效?被跳过时我能从哪里看到原因?
  • 我想换掉 Boot 配好的 ObjectMapper,正确做法是什么?为什么不该打开覆盖开关?
7 / 141
小节
一、先看现象:引入一个 starter,凭什么都齐了
8 / 141

新建一个 Spring Boot 项目,在 pom.xml 里只加了一行依赖:

9 / 141
xml
<dependency>    <groupId>org.springframework.boot</groupId>    <artifactId>spring-boot-starter-data-jpa</artifactId></dependency>
10 / 141

application.yml 里只写了三四行数据库地址,什么 DataSource、EntityManagerFactory、PlatformTransactionManager 一律没配。可是启动之后,这些 Bean 全都好好地在容器里待着——@Autowired 一个 DataSource 就能直接用。

11 / 141

这里有个很值得追问的点:Spring 的 IoC 容器只会造「你告诉它的东西」,它不会凭空变出一个连接池。那这些 Bean 是谁注册进去的?

12 / 141

答案就是自动配置。自动配置不是魔法,它是一批被提前写进 jar 包里、只有满足条件才会生效的配置类。本文要做的,就是把「谁读了哪个文件、谁判断了条件、谁最后把 Bean 塞进容器」这条链路完整拆一遍。

13 / 141

「一行依赖换来一屋子 Bean」这件事,最好自己勾一遍看。生成器与课文同源:先只勾 Data JPA,看它一个人就拖来了 spring-orm、hibernate-core、spring-boot-starter-jdbc;再把 MySQL 驱动 与 H2 叠上去,注意两个驱动同时在 classpath 时会发生什么——那正是第十三节沙盘与第十四节那段报错的现场;最后加 RabbitMQ,勾完就把它取消,你会在第 ② 步的实验里看到它被哪一道闸砍掉。

14 / 141
生成器
生成器一行依赖凭什么换来一屋子 Beanpom.xml1 / 10
每勾一项,被评估的自动配置就多一批。勾完再取消一次,对照第十三节:谁在正选、谁在落选
产物
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.3.4</version> <!-- 版本由 BOM 统管,子依赖不写 version -->
        <relativePath/>
    </parent>

    <groupId>com.example</groupId>
    <artifactId>demo-service</artifactId>
    <version>0.0.1-SNAPSHOT</version>

    <properties>
        <java.version>17</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>
勾了这些,代价与理由在这里
parent继承 3.3.4 的 starter-parent 之后,所有 spring-boot-starter-* 都不用写版本号;一旦有人手写给某个 starter 加 version,就以那条为准——这是依赖版本漂移最常见的原因。
Data JPA包含 spring-orm + Hibernate;写 Repository 接口就有实现,不用自己拼 SQL。
15 / 141
小节
二、加载链路全拆:从开关到注册
16 / 141
架构图
图 1 · 自动配置加载链路
图 1 · 自动配置加载链路
17 / 141

一切的起点,是上一篇里 @EnableAutoConfiguration 上的那一行 @Import:

18 / 141
java
@Import(AutoConfigurationImportSelector.class)public @interface EnableAutoConfiguration { }
19 / 141

真正干活的是 AutoConfigurationImportSelector。它的骨架大致长这样:

20 / 141
代码对照
代码java
public class AutoConfigurationImportSelector        implements DeferredImportSelector, BeanClassLoaderAware,                   ResourceLoaderAware, BeanFactoryAware, EnvironmentAware,                   Ordered {    @Override    public String[] selectImports(AnnotationMetadata annotationMetadata) {        if (!isEnabled(annotationMetadata)) {            return NO_IMPORTS;          // spring.boot.enableautoconfiguration=false 时直接返回空        }        AutoConfigurationEntry entry = getAutoConfigurationEntry(annotationMetadata);        return StringUtils.toStringArray(entry.getConfigurations());    }    @Override    public Class<? extends Group> getImportGroup() {        return AutoConfigurationGroup.class;   // 分组=延迟导入,保证排在用户配置之后    }}
解读
  • DeferredImportSelector:注意「Deferred(延迟)」。普通 ImportSelector 立刻被处理,而它被推迟到所有用户自己的 @Configuration 都处理完之后才执行。这一步至关重要,第七节会看到它换来了什么
  • selectImports:整个流程的入口。先判断开关是否打开,再向 getAutoConfigurationEntry 要候选清单
  • getAutoConfigurationEntry:内部依次做「取候选 → 去重 → 过滤排除 → 条件裁剪」,最终返回一份「该导入的配置类」数组
21 / 141

把 getAutoConfigurationEntry 展开,一次完整的加载是这样一个序列:

22 / 141
  1. getCandidateConfigurations() 拿到全部候选自动配置类(就是 jar 里清单文件的那一份)
  2. 去掉重复项(多个 jar 可能重复声明),并剔除用户通过 exclude / excludeName / spring.autoconfigure.exclude 指定的项
  3. AutoConfigurationImportFilter 先做一轮快速筛选(例如 OnClassCondition 直接看 classpath,能一次性踢掉大批不相关的配置类,省下解析开销)
  4. 剩下的类交给 ConfigurationClassParser 逐个解析
  5. 解析时,类上和方法上的 @Conditional 条件被 ConditionEvaluator 逐条求值
  6. 通过者注册为 BeanDefinition;未通过者被记进 ConditionEvaluationReport
  7. 容器 refresh(),从这些 BeanDefinition 实例化出真正的 Bean
23 / 141
提示

注意链路里有两道筛选。第一道是 AutoConfigurationImportFilter,它不解析类、只看 classpath,作用是「快速砍掉」,属于性能优化;第二道是条件求值,它才会真正读注解、判断 Bean 是否存在。理解这两层的分工,你就能明白为什么一个「类路径没有 RabbitMQ」的项目,启动时不会为 Rabbit 相关配置类付出解析代价。

24 / 141

这两道闸长得不一样,能力也不一样,这张图把它们并排放:

25 / 141
架构图
图 · 链路上的两道闸:粗筛与细筛
图 · 链路上的两道闸:粗筛与细筛
26 / 141

看右边那一列的最后一行——只有第二道闸会留下「为什么」。所以「为什么不生效」这个问题,永远只能在 --debug 报告里找,而不是在候选清单阶段找;第一道闸砍掉的东西连名字都不会出现在报告里,这是新手最常见的「报告里搜不到它」的真正原因(第十三节第四格会再遇到一次)。

27 / 141
小节
三、新老两种机制:spring.factories 与 AutoConfiguration.imports
28 / 141

「候选清单」到底从哪来?这里有一个版本分水岭。Spring Boot 2.7 之前用 spring.factories,2.7 起引入新写法,3.0 彻底移除旧机制。

29 / 141
对照表
维度spring.factories(≤ 2.7)AutoConfiguration.imports(2.7+)
文件路径META-INF/spring.factoriesMETA-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
文件格式properties:一个 key 映射一串类名纯文本:一行一个类名
定位方式靠 key org.springframework.boot.autoconfigure.EnableAutoConfiguration文件名本身就是 key,不需要额外声明
加载 APISpringFactoriesLoader.loadFactoryNamesImportCandidates.load(AutoConfiguration.class, ...)
现状2.7 起弃用,3.0 起失效现行标准
30 / 141

旧的写法长这样,注意那串靠反斜杠续行、逗号分隔的类名:

31 / 141
properties
# META-INF/spring.factories(旧)org.springframework.boot.autoconfigure.EnableAutoConfiguration=\com.example.demo.autoconfigure.MyServiceAutoConfiguration,\com.example.demo.autoconfigure.OtherAutoConfiguration
32 / 141

新的写法则干净得多,一行一个类,没有再声明 key 的必要:

33 / 141
代码对照
代码text
# META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports(新)com.example.demo.autoconfigure.MyServiceAutoConfigurationcom.example.demo.autoconfigure.OtherAutoConfiguration
解读

坑:从 2.7 升级到 3.0 时,如果你的自定义 starter 还在用 spring.factories 注册自动配置,升级后会整批静默失效——不报错,只是那些自动配置类再也不会被加载。升级清单里务必搜一遍 spring.factories。顺带一提,spring.factories 在 3.0 里并没有被完全删除,它仍然承载「监听器、初始化器」等其它扩展点,被移走的只是自动配置这一项。

34 / 141
小节
四、走进一个真实的自动配置类:DataSourceAutoConfiguration
35 / 141

链路讲完,来看一个真实选手。Spring Boot 自带的 DataSourceAutoConfiguration(做了必要精简)几乎用全了自动配置的每个关键注解:

36 / 141
java
@AutoConfiguration(before = HibernateJpaAutoConfiguration.class)@ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class })@ConditionalOnMissingBean(type = "io.r2dbc.spi.ConnectionFactory")@EnableConfigurationProperties(DataSourceProperties.class)@Import({ DataSourcePoolMetadataProvidersConfiguration.class,          DataSourceCheckpointRestoreConfiguration.class })public class DataSourceAutoConfiguration {    @Configuration(proxyBeanMethods = false)    @Conditional(PooledDataSourceCondition.class)    @ConditionalOnMissingBean({ DataSource.class, XADataSource.class })    @Import({ DataSourceConfiguration.Hikari.class,              DataSourceConfiguration.Tomcat.class })    protected static class PooledDataSourceConfiguration {    }}
37 / 141

逐个注解解释,这才是读自动配置源码的正确姿势:

38 / 141
  • @AutoConfiguration:新式自动配置标记。它等价于 @Configuration(proxyBeanMethods = false),但额外获得排序能力(before / after)。老的自动配置类用的是 @Configuration,新版应该统一换成它
  • before = HibernateJpaAutoConfiguration.class:声明「我必须排在 JPA 自动配置之前」。因为 JPA 需要 DataSource,顺序错了 JPA 就找不到数据源——这正是第五节的主题
  • @ConditionalOnClass({ DataSource.class, ... }):类路径上必须存在这些类。没有 spring-jdbc 时,整个配置类直接跳过,容器里也就不会有 DataSource
  • @ConditionalOnMissingBean(type = "io.r2dbc.spi.ConnectionFactory"):如果项目用的是响应式 R2DBC,就让 R2DBC 那边负责,这里主动让路
  • @EnableConfigurationProperties(DataSourceProperties.class):把 spring.datasource.* 前缀的配置项绑定成一个 Java 对象,供下面的 @Bean 方法使用
  • @Import(...):继续导入更细粒度的配置。所以自动配置是分层嵌套的:外层判断「要不要配数据源」,内层判断「用哪种连接池」
39 / 141
小节
五、@AutoConfiguration 排序:为什么顺序真的重要
40 / 141

自动配置类之间不是平级的——它们有依赖关系。最典型的一对:DataSourceAutoConfiguration 必须在 HibernateJpaAutoConfiguration 之前。如果 JPA 先跑,它去容器里找 DataSource 时还没被注册,就会报「找不到数据源」启动失败。

41 / 141

Spring Boot 提供了三种表达顺序的方式:

42 / 141
代码对照
代码java
// 方式一:@AutoConfiguration 上的 before / after(最常用,成对声明依赖)@AutoConfiguration(before = HibernateJpaAutoConfiguration.class)public class DataSourceAutoConfiguration { }// 方式二:类的 after —— 等对方先跑完@AutoConfiguration(after = DataSourceAutoConfiguration.class)@ConditionalOnClass({ LocalContainerEntityManagerFactoryBean.class, EntityManager.class })public class HibernateJpaAutoConfiguration extends JpaBaseConfiguration {    // 到这里可以安全假设 DataSource 已经就绪}// 方式三:@AutoConfigureOrder —— 给一个绝对数字,越小越靠前@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)public class MyEarlyAutoConfiguration { }
解读
  • before / after 是相对声明,表达「我和谁有依赖」。它们不允许成环,成环会在启动时报错
  • @AutoConfigureOrder 是绝对声明,用一个数字定顺序。它更「强硬」但也更容易写出难以维护的魔法数字,官方推荐优先用 before/after
  • 兜底规则:AutoConfigurationSorter 在处理完所有 before/after 之后,会把剩下没有任何依赖关系的类按类名字母序(A→Z)排列。这一步保证排序是确定性的——同样的清单每次启动顺序一致,不会因为 JVM 差异而抖动

要点:面试问「自动配置类的顺序是怎么定的」,标准答案是三层——先按 before/after 解出依赖顺序,没有依赖关系的用 @AutoConfigureOrder 的数字,最后剩下的按字母序兜底。三句话答全,基本就满分了。

43 / 141

这三层不是三条并列规则,而是一条按顺序落水的流水线:前一层能定的,后一层就不再有发言权。点着走一遍,重点看第 ④ 格——它决定「同样的清单每次启动顺序一致」这件事到底靠什么:

44 / 141
交互图解
流程自动配置的排序是怎么定下来的(点着看)1 / 5
从 ① 点到 ⑤。第 ③ 格最容易被忽略:真正保证可复现的,是那个看起来最土的字母序兜底
→
→
→
→
① 收齐候选,先不看顺序
AutoConfigurationImportFilter 粗筛之后剩下的类进入排序器。此刻它们是一份无序集合——来自不同 jar 的清单合并结果,谁也不认识谁。
全部看懂了一句话:依赖拓扑优先,档位数字次之,字母序兜底;三层都走完,注册顺序才是可复现的。
45 / 141
小节
六、条件装配矩阵:自动配置的「大脑」
46 / 141

自动配置类之所以能「智能地」判断该不该生效,全靠条件注解。下面这张表把最常用的一批集中列出,下一篇会逐个讲透:

47 / 141
对照表
条件注解判定依据典型用途
@ConditionalOnClass类路径存在指定类有依赖才装配(如检测到 HikariCP)
@ConditionalOnMissingClass类路径不存在指定类排除某个实现
@ConditionalOnBean容器内已有指定类型的 Bean依赖另一个 Bean 才能装配
@ConditionalOnMissingBean容器内没有指定类型的 Bean用户没配时的兜底
@ConditionalOnSingleCandidate该类型只有一个候选 Bean唯一数据源、唯一事务管理器
@ConditionalOnProperty配置项等于 / 不等于某值功能开关(如 spring.aop.auto)
@ConditionalOnWebApplication当前是 Web 应用Web 相关的自动配置
@ConditionalOnExpressionSpEL 表达式为真复杂的组合条件
48 / 141
提示

这张表先当「字典」用。真正难的是它们之间的求值顺序——比如 @ConditionalOnBean 依赖「别人已经注册好了」,那它凭什么保证别人先注册?这正是下一篇要回答的核心问题。

49 / 141
小节
七、调试利器:--debug 与条件评估报告
50 / 141

自动配置「没生效」时,最不该做的就是瞎猜。Spring Boot 内置了一份条件评估报告,会把你项目里每个自动配置类的判定结果和原因全部打印出来。开启方式三选一:

51 / 141
bash
# 方式一:启动参数java -jar app.jar --debug
52 / 141
yaml
# 方式二:配置文件debug: true# 方式三:只打开自动配置包的日志logging:  level:    org.springframework.boot.autoconfigure: DEBUG
53 / 141

打开后,控制台会有一段报告,核心是两个小节——「谁生效了」和「谁被跳过了、为什么」:

54 / 141
代码对照
代码text
============================CONDITIONS EVALUATION REPORT============================Positive matches:-----------------   DataSourceAutoConfiguration matched:      - @ConditionalOnClass found required classes 'javax.sql.DataSource',        'org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType' (OnClassCondition)      - @ConditionalOnMissingBean (types: io.r2dbc.spi.ConnectionFactory) did not find any beans (OnBeanCondition)      - @AutoConfigureBefore matched: HibernateJpaAutoConfiguration (AutoConfigureOrder)Negative matches:-----------------   RabbitAutoConfiguration matched:      - @ConditionalOnClass did not find required class 'com.rabbitmq.client.Channel' (OnClassCondition)Exclusions:-----------   DataSourceAutoConfiguration (excluded by @SpringBootApplication exclude)
解读
  • Positive matches:这些配置类通过了条件,真的生效了。逐条列出它通过了哪些判断,等于告诉你「这些 Bean 是它给的」
  • Negative matches:这些被跳过,原因写在括号里。排查自动配置问题,80% 的时间是在看这一段——比如你发现 RabbitAutoConfiguration 因为「找不到 com.rabbitmq.client.Channel」被跳过,那根因就是依赖没加全
  • Exclusions:被你主动排除的条目,会在这里单独列出,确认你的 exclude 生效了

说明:报告的源头是 ConditionEvaluationReport 对象。它记录了每一次条件求值的结果,--debug 只是把这份对象渲染成可读文本。理解这一点,你就知道这份报告不是「日志」,而是容器内部决策过程的快照。

55 / 141
原理动画
动图 · 从 starter 到生效 Bean
动图 · 从 starter 到生效 Bean
56 / 141

三种开启方式里,第一种(--debug 启动参数)只适合本地临时看一次;真正排查线上问题时你需要的是能按环境切换的第二、第三种。所以下面这份 yml 请这样勾:先开「日志」,把自动配置包的 DEBUG 单独调出来——这比整体 debug: true 温和得多,也不会把别的信息淹掉;再叠「数据源」,你会看到那三行 url/username/driver 正是第八节和第十四节报错的解药;最后加「Profile」,把上面两组搬进分文档,得到「开发环境吵、生产环境安静」的那份配置。

57 / 141
生成器
生成器把调试开关与数据源写进同一份 ymlapplication.yml1 / 5
先勾「日志」看只打开自动配置包这一档;再叠「数据源」与「Profile」,对照上面三种开启方式,想清楚哪一条能按环境切换
产物
server:
  port: 8080

spring:
  application:
    name: demo-service

logging:
  level:
    root: INFO
    com.example.demoservice: DEBUG
    org.springframework.jdbc.core.JdbcTemplate: DEBUG   # 打 SQL 与参数
  file:
    name: logs/app.log
  logback:
    rollingpolicy: { max-file-size: 50MB, max-history: 14 }
勾了这些,代价与理由在这里
logging级别可按包精细控制;logging.level.root=DEBUG 会把三方库全打爆,别在生产这么干。
58 / 141
小节
八、坑:覆盖自动配置的正确姿势
59 / 141

自动配置是「你没配时兜底」,那你想自己接管的正确做法是什么?先看一个错误的示范:有人为了让自己的 Bean 生效,去打开这个开关——

60 / 141
properties
# ❌ 不推荐:打开 BeanDefinition 覆盖spring.main.allow-bean-definition-overriding=true
61 / 141

Spring Boot 2.1 起默认 不允许 覆盖(false)。打开它看似能「覆盖一切」,实则把「最终谁生效」变成了一个取决于注册顺序的问题:自动配置类和你的类谁先注册,是不确定的,于是你可能得到自己的实现,也可能得到 Boot 的默认实现——一个极难复现的隐性 bug。

62 / 141

正确姿势是利用自动配置主动让路的机制。因为绝大多数自动配置类都带着 @ConditionalOnMissingBean:

63 / 141
java
@Configurationpublic class MyDataSourceConfig {    // 只要你定义了 DataSource,DataSourceAutoConfiguration 就会因为    // @ConditionalOnMissingBean 而主动退让,不会和你的 Bean 冲突    @Bean    @ConfigurationProperties("spring.datasource.hikari")    public DataSource dataSource(DataSourceProperties props) {        return props.initializeDataSourceBuilder()                    .type(HikariDataSource.class)                    .build();    }}
64 / 141

为什么这样就能生效?关键还在第二节埋的那个伏笔——AutoConfigurationImportSelector 是 DeferredImportSelector,它被推迟到所有用户配置都处理完之后才执行。于是当自动配置类求值 @ConditionalOnMissingBean 时,你定义的那个 DataSource 早已注册完毕,条件自然判定为「已存在」,自动配置随即让路。延迟导入,就是「用户优先」的技术保障。

65 / 141
坑

allow-bean-definition-overriding=true 是一种「掩盖冲突」而非「解决冲突」。它让你以为覆盖成功,实际上埋下了顺序依赖的雷。自定义 Bean 要覆盖自动配置,请优先依赖 @ConditionalOnMissingBean 的让路机制;只有在不方便定义同类型 Bean 时(例如想改的是 Builder 而不是最终对象),才改用官方提供的 Customizer 扩展点。

66 / 141

「延迟导入 → 你先注册 → 它才求值 → 它让路」这条因果链,就是下面这六帧:

67 / 141
原理动画
动图 · 让路机制:你的 Bean 为什么能赢
动图 · 让路机制:你的 Bean 为什么能赢
68 / 141

再把它摊成一次单步执行。左边是那六行真正发生的事情,右边同步刷新「此刻容器里有没有 dataSource」和「谁在求值」——重点在第 ③ 步与第 ④ 步:那半秒钟的先后关系,就是「用户优先」的全部技术含量:

69 / 141
单步调试台
单步台逐帧走一遍:你写的那个 @Bean 是怎么把自动配置挤出去的1 / 6
按下一步走六拍,盯右侧「容器里已有的 DataSource 定义」这一格——它的变化时间决定了后面所有条件的判定
被调试的代码
1// 容器解析你的 MyDataSourceConfig(普通 @Configuration,不延迟)
2// @Bean dataSource 的 BeanDefinition 注册完毕,此刻对象还没造
3// DeferredImportSelector 终于被执行:自动配置候选清单进场
4// DataSourceAutoConfiguration 求值 @ConditionalOnMissingBean(DataSource.class)
5// 条件命中「已经有一个了」→ false,整个内层配置不注册
6// refresh() 实例化:容器里唯一的 DataSource 是你那个 HikariDataSource
此刻的变量
正在处理MyDataSourceConfig
属于哪一批用户自己的配置
延迟队列还没启动
调用栈
1ConfigurationClassParser.parse
2普通 @Configuration 分支
1第一步的关键不在你的类做了什么,而在它**排在前面**。ConfigurationClassParser 处理用户配置时,导入选择器(ImportSelector)里被标成 Deferred 的那些会被单独收走,攒着不执行。
70 / 141
小节
九、动手体验:自动配置条件求值现场
71 / 141

下面这个演示把自动配置的条件求值过程做成可切换的。试着切换三个条件开关,观察每一个自动配置类对应的 Bean,是在什么条件下被创建、又在什么条件下被整批跳过:

72 / 141
内核实验
73 / 141

按钮按完,换成命令行——本篇的核心问题「它到底生没生效、为什么没生效」在这台容器里正好有三个命令能直接回答:conditions 打印条件求值记录,cond 手动扳动某个条件开关,restart 让整条装配链重新走一遍。

74 / 141

按这个顺序敲,第 4 步与第 5 步的差别就是「第一道闸」和「第二道闸」的差别:

75 / 141
  1. boot —— 建容器,看装配日志
  2. conditions —— 谁在正选、谁在落选,落选原因写在括号里
  3. cond jdbcOnClasspath false —— 把 JDBC 从 classpath 上「拿掉」
  4. restart 之后再 conditions —— 找 DataSourceAutoConfiguration:它这次还在报告里吗?
  5. cond userDataSource true —— 模拟你自己定义了一个 DataSource,再 restart
  6. lab autoconf filter 与 lab autoconf report —— 粗筛那一步砍掉多少,报告里又留下多少
76 / 141
内核控制台
77 / 141
说明

第 3 步与第 5 步的现象很像,答案却完全不同。jdbcOnClasspath false 让配置类从粗筛阶段就消失,所以报告里搜不到它的名字;userDataSource true 时它照样出现在落选段,括号里写着 OnBeanCondition——这正是第二节那张双闸图给出的两个可验证预言。

78 / 141
小节
十、决策:需要覆盖自动配置时,到底该怎么做
79 / 141
决策
决策项目里想给 Spring Boot 自动配好的 `ObjectMapper` 换一套自定义风格(改日期格式、序列化时忽略 null)。下面哪种做法最该选?
80 / 141
小节
十一、动手体验一:四段流水线,一段一段看
81 / 141

第二节的七步序列是文字版。下面这个实验把它变成可以按段推进的画面——autoconf 场景模拟的就是自动配置装配链,五个按钮正好对应「清单 → 过滤 → 排序 → 生效 → 报告」:

82 / 141
  • 候选清单从哪来(imports):扫描 classpath 上所有 jar 的 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports,把里面的全限定类名合并成一份大清单。这一步只做 IO,不看任何条件,所以数量会很吓人(内置就有 130+ 条)
  • 条件过滤(filter):AutoConfigurationImportFilter 先粗筛一轮。你会看到大批配置类因为 classpath 上缺类而被一次性踢掉——这就是第七节说的「快刀」,它不解析注解
  • 排序与分组(sort):剩下的交给 AutoConfigurationSorter:先按 before/after 解依赖,再按 @AutoConfigureOrder,最后字母序兜底。这一段的输出顺序,就是你以后在报告里看到的顺序
  • 真正生效(apply):逐个做完整的条件求值,通过者注册成 BeanDefinition;未通过者写进报告
  • 装配报告(report):把上面的结果渲染成正选 / 落选 / Exclusions / Unconditional 四段——和你在控制台加 --debug 看到的一模一样
83 / 141
内核实验
TeaVM自动配置装配链:清单 → 过滤 → 排序 → 生效 → 报告未启动
按 imports → filter → sort → apply → report 的顺序点,每段记下候选数量的变化
场景参数
点「运行演示」,在浏览器内真实执行 Java 编译出的内核算法,逐步看它怎么跑。
84 / 141
要点

候选数在 filter 那一步掉得最狠,而真正决定「有没有这个 Bean」的是 apply。这两个数字的落差,正是「为什么加了依赖还是没生效」这个问题的答案所在——依赖影响的是 classpath,classpath 影响的是条件求值。

85 / 141
小节
十二、动手体验二:三种「让它生效 / 让它闭嘴」的手段
86 / 141

流水线的每一段都能被你手动干预。下面三个实验分别练三件事,最后一个还给出第十节决策的实操版。

87 / 141

第一个专练条件注解。cond 的 missing 参数讲的就是那条借伞规矩:客人没带伞(容器里没有同类型 Bean),店里才借一把(自动配置才补上);切到 report 能看到每个条件的判定原文,格式与 --debug 一致。先把四个条件各试一遍,你就再也不需要靠猜来判断「为什么不生效」:

88 / 141
内核实验
TeaVM四种条件各自盘问什么:onclass / onbean / onprop / missing未启动
重点看 missing:先造一个自己的 Bean,再看它如何把自动配置挡在门外
场景参数
点「运行演示」,在浏览器内真实执行 Java 编译出的内核算法,逐步看它怎么跑。
89 / 141

第二个练「反向操作」:自己写一个 starter,把这条链路端到端跑通一次。starter 的四个参数依次是 meta(写清单登记自动配置类)、props(用 @ConfigurationProperties 绑前缀属性)、bean(条件满足后生成 Bean)、off(用 spring.xxx.enabled=false 关掉它)。做完这一段,第八节那段「让路机制」就不再是抽象说法,而是你自己写过四行代码的东西:

90 / 141
内核实验
TeaVM亲手装一条自动配置:清单 → 属性 → Bean → 关掉它未启动
走完 meta/props/bean 后一定要点 off,看 exclude 与开关属性的差别
场景参数
点「运行演示」,在浏览器内真实执行 Java 编译出的内核算法,逐步看它怎么跑。
91 / 141

第三个练「同一个属性被多处声明时谁赢」。自动配置大量依赖配置项来决定开合,所以你必须知道优先级:命令行 > JNDI > 系统属性 > application-{profile}.yml > application.yml > 默认值。prop 场景会当场演示覆盖过程:

92 / 141
内核实验
TeaVM配置项谁盖谁:自动配置的开关也是被这样决定的未启动
看完 order 再切 profile,理解同一份自动配置如何在多环境里表现不同
场景参数
点「运行演示」,在浏览器内真实执行 Java 编译出的内核算法,逐步看它怎么跑。
93 / 141

第四个实验把本篇和上一篇缝回一张图:一个对象进容器一共四条路,beanin 的 auto 档专门演示自动配置这条路——不经过扫描、不看包名,只认 jar 里那份清单。切到 scan 再对比一次,你就把「走扫描的门」和「走清单的门」这两种命运同时看完了,也正是本节末尾那张对比图想说的话:

94 / 141
内核实验
TeaVM进容器的四条路里,自动配置是最后那一条未启动
先切 scan 看扫描那条路怎么只认包路径,再切 auto 看清单这条路为什么与包名无关
场景参数
点「运行演示」,在浏览器内真实执行 Java 编译出的内核算法,逐步看它怎么跑。
95 / 141

顺手记住这张对比图——上一篇「Bean 找不到」和本篇「自动配置不生效」其实是同一枚硬币的两面:你的类走扫描的门,框架的类走清单的门。走错门,就永远不会出现:

96 / 141
架构图
图 · 两条进容器的路:扫描 vs 清单登记
图 · 两条进容器的路:扫描 vs 清单登记
97 / 141

本篇讲到现在,你手上其实已经有了六把扳手,但它们的作用位置各不相同:有的删候选、有的改条件、有的只管注册之后的事。把「扳手 ↔ 它作用在哪一环」配一遍,下次排查就不会再靠试:

98 / 141
配对闯关
闯关六把扳手各拧在哪一环已配对 0/6 · 配错 0
左边是你写在代码或配置里的东西,右边是它真正生效的那个环节——配错了会告诉你两者差在哪一步
先点左边一个
99 / 141
小节
十三、沙盘:同一句「没生效」,先看这四格
100 / 141

--debug 打印的报告很长,但真正要看的只有四格。先看这条动画——它讲清这份报告是怎么被写出来的(理解了这一点,你就明白为什么它是「决策快照」而不是日志),再用左边换场景、右边直接告诉你该读哪一格:

101 / 141
原理动画
动图 · --debug 报告是怎么长出来的
动图 · --debug 报告是怎么长出来的
102 / 141
沙盘
沙盘自动配置没生效?先看这四格
运行结果
Negative matches:
DataSourceAutoConfiguration:
- @ConditionalOnClass did not find required class 'org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType' (OnClassCondition)
#去 Negative matches 这一格:括号里的 OnClassCondition 已经点名了缺失的类
典型是依赖没加全:只引了 spring-jdbc,却没引带连接池的 starter。补 spring-boot-starter-data-jpa 或 HikariCP 再看一次。
103 / 141
说明

四格的读法顺序是有讲究的——先确认它在不在正选(在,就去查你的属性和配置;不在),再去落选看原因(括号里的 Condition 类名就是根因),最后看 Exclusions 确认你没手滑 exclude 掉。这套顺序能替你省掉大部分「凭感觉改改看」。

104 / 141
小节
十四、常见报错速查
105 / 141

报错原文都可以整段粘进搜索框:

106 / 141
对照表
报错原文(片段)真实原因30 秒自救深挖看第几篇
java.lang.NoClassDefFoundError: org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseType自动配置类的 @ConditionalOnClass 需要的类不在 classpath——依赖少引了一个开 --debug,在 Negative matches 里搜这个类名,它会告诉你哪个自动配置因此被跳过;再按该类所属 artifact 补依赖本篇第七节 · 第十三节沙盘
ClassNotFoundException: com.rabbitmq.client.Channel 且 RabbitAutoConfiguration 全程静默同上:类路径缺类,条件不满足,整个配置类被跳过。Boot 不会为「没启用」的功能报错这正是 --debug 报告里 - @ConditionalOnClass did not find required class 'com.rabbitmq.client.Channel' (OnClassCondition) 那一行;加 spring-boot-starter-amqp本篇第六节条件矩阵
自定义 starter 升到 Spring Boot 3 后「整批静默失效」,没有任何报错还在用旧机制:自动配置项写在 META-INF/spring.factories 里,而 3.0 已移除该入口把类名逐行搬到 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports;注意 spring.factories 仍服务于监听器、初始化器等其它扩展点,别整删文件本篇第三节版本对照表
Field dataSource in ... required a bean of type 'javax.sql.DataSource' that could not be found数据源相关自动配置被跳过或被 exclude;也可能是 spring.datasource.url 缺失导致条件不成立先看 Exclusions 有没有它,再看 Negative matches 的原因;两处都没有就说明根本没引入 starter第 17 篇第四节 · 第 28 篇连接池
Parameter 0 of method jpaVendorAdapter in ...HibernateJpaAutoConfiguration required a bean of type '...' that could not be found自动配置类之间的顺序出了问题:本该先注册的 Bean 还没到位用 @AutoConfiguration(before/after) 显式声明依赖关系,不要靠 @AutoConfigureOrder 的魔法数字调本篇第五节排序三层
The bean 'objectMapper', defined in class path resource [...] could not be registered. A bean with that name has already been defined [...] and overriding is disabled.同名 Bean 冲突,而 Boot 2.1 起默认禁止覆盖别急着打开 allow-bean-definition-overriding;改名或干脆删掉你自己的定义,让自动配置的 @ConditionalOnMissingBean 让路机制工作本篇第八节 · 第十五节自测第二题
spring.autoconfigure.exclude 写了类名却报 No auto-configuration classes found属性里写的类名拼错、或那个类根本不在当前 classpath(例如 starter 没引)从 IDE 里复制自动配置类的全限定名粘贴过去,别手打;顺带确认对应 starter 已在 pom 里第 17 篇第六节三种排除手段
107 / 141
坑

自动配置「不生效」几乎从不抛异常——它只是安静地不出现在容器里。所以排查第一步永远是 --debug,而不是改代码试试。看不见 ≠ 出错,看不见 = 条件没满足。

108 / 141

上面那条规律有唯一一个常见反例:自动配置生效了、但配不出可用的 Bean。这时候它不但会报错,还会把「我是谁配的」直接写在栈里。下面这段就是新手第一次引了 spring-boot-starter-data-jpa 却忘写连接信息时的现场,先别看解析,点出你认为的凶手行:

109 / 141
报错急救
报错急救DataSourceBeanCreationException: Failed to determine a suitable driver class
自动配置生效了,却没配出可用的 DataSource:报错里写着它是谁

新建项目只勾了 Data JPA 与 H2,application.yml 一个字没写。启动即失败,屏幕中段打出 Reason: failed to configure a DataSource。

org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'userController' defined in file [D:\demo\target\classes\com\example\demo\web\UserController.class]: Unsatisfied dependency expressed through constructor parameter 0; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'dataSource' defined in class path resource [org/springframework/boot/autoconfigure/jdbc/DataSourceConfiguration$Hikari.class]: Failed to instantiate [com.zaxxer.hikari.HikariDataSource]: Factory method 'dataSource' threw exception with message: Failed to determine a suitable driver class
at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:801)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1212)
at org.springframework.boot.autoconfigure.jdbc.DataSourceConfiguration$Hikari.dataSource(DataSourceConfiguration.java:49)
at org.springframework.boot.autoconfigure.jdbc.DataSourceProperties.determineDriverClassName(DataSourceProperties.java:186)
Caused by: org.springframework.boot.autoconfigure.jdbc.DataSourceProperties$DataSourceBeanCreationException: Failed to determine a suitable driver class
点你认为的「凶手行」(可反复试)
不会也没关系:先猜异常名,再猜哪一行在做决定。
110 / 141
小节
十五、随堂自测
111 / 141
随堂自测
随堂自测项目升级到 Spring Boot 3.0 后,你自己写的 starter 里的自动配置全部不生效,而且启动没有任何报错。最可能的原因是?
先自己选一个,选中立刻告诉你对不对
112 / 141
随堂自测
随堂自测你想换掉 Boot 自动配好的 ObjectMapper,最不该做的是哪一项?
先自己选一个,选中立刻告诉你对不对
113 / 141
小节
十六、动手练习
114 / 141
小节
第一档 · 照做
115 / 141

目标:开 --debug 看清一条真实的自动配置链路,并在报告里找出五个 Bean 的出处。

116 / 141

pom.xml:

117 / 141
xml
<?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0"         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">    <modelVersion>4.0.0</modelVersion>    <parent>        <groupId>org.springframework.boot</groupId>        <artifactId>spring-boot-starter-parent</artifactId>        <version>3.2.5</version>        <relativePath/>    </parent>    <groupId>com.example</groupId>    <artifactId>autolab</artifactId>    <version>0.0.1-SNAPSHOT</version>    <properties>        <java.version>17</java.version>    </properties>    <dependencies>        <dependency>            <groupId>org.springframework.boot</groupId>            <artifactId>spring-boot-starter-web</artifactId>        </dependency>        <!-- H2 会自动触发 DataSourceAutoConfiguration,无需任何 datasource 配置 -->        <dependency>            <groupId>com.h2database</groupId>            <artifactId>h2</artifactId>            <scope>runtime</scope>        </dependency>    </dependencies>    <build>        <plugins>            <plugin>                <groupId>org.springframework.boot</groupId>                <artifactId>spring-boot-maven-plugin</artifactId>            </plugin>        </plugins>    </build></project>
118 / 141

主类(src/main/java/com/example/autolab/AutolabApplication.java,注意根包):

119 / 141
java
package com.example.autolab;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;@SpringBootApplicationpublic class AutolabApplication {    public static void main(String[] args) {        SpringApplication.run(AutolabApplication.class, args);    }}
120 / 141

src/main/resources/application.properties 只需要一行:

121 / 141
properties
debug=true
122 / 141

运行 mvn spring-boot:run,预期启动日志的关键片段(顺序与内容会随依赖略有出入):

123 / 141
text
 :: Spring Boot ::                (v3.2.5)============================CONDITIONS EVALUATION REPORT============================Positive matches:-----------------   DataSourceAutoConfiguration matched:      - @ConditionalOnClass found required classes 'javax.sql.DataSource', 'org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType' (OnClassCondition)      - @ConditionalOnMissingBean (types: io.r2dbc.spi.ConnectionFactory) did not find any beans (OnBeanCondition)   DataSourceAutoConfiguration$EmbeddedDataSourceConfiguration matched:      - @ConditionalOnMissingBean (types: javax.sql.DataSource,javax.sql.XADataSource) did not find any beans (OnBeanCondition)      - EmbeddedDatabaseCondition and PooledDataSourceCondition both matched (OnClassCondition)   DispatcherServletAutoConfiguration matched:      - @ConditionalOnClass found required class 'org.springframework.web.servlet.DispatcherServlet' (OnClassCondition)      - found 'session' scope (OnWebApplicationCondition)Negative matches:-----------------   RabbitAutoConfiguration:      - @ConditionalOnClass did not find required classes 'org.springframework.amqp.rabbit.core.RabbitTemplate', 'com.rabbitmq.client.Channel' (OnClassCondition)   QuartzAutoConfiguration:      - @ConditionalOnClass did not find required class 'org.quartz.Scheduler' (OnClassCondition)Exclusions:-----------    None.Unconditional:--------------    None.... Tomcat started on port 8080 (http) with context path ''... Started AutolabApplication in 2.1 seconds
124 / 141

现在做三次「找东西」,每次都在报告里写下你那一行的原文:

125 / 141
  1. 在正选里搜 DispatcherServletAutoConfiguration,说出 DispatcherServlet 这个 Bean 是谁给的
  2. 在落选里随便挑三条,把括号里的 Condition 类名抄下来,并说出各自缺的是哪个类
  3. 把 debug=true 删掉、改成命令行 --debug 启动,确认报告一模一样(顺便体会第十三节说的「开启方式三选一」)
126 / 141

验收清单:① 你能不看资料解释正选与落选的差别;② 至少一条落选记录里,你能指出「补哪个依赖它就会翻到正选」;③ 说清 Exclusions: None. 这行为什么值得单独看一眼。

127 / 141
小节
第二档 · 变体
128 / 141

每个变体只动一处:

129 / 141
  1. 让一条落选翻成正选:在上一步的基础上加 spring-boot-starter-amqp,重启。你会观察到 RabbitAutoConfiguration 从 Negative matches 移到 Positive matches——这是最直观的「条件由 classpath 决定」的证明。
  2. 亲眼看看让路机制:新建 MyJsonConfig,里面写一个 @Bean ObjectMapper(随便设个日期格式),然后请求一个返回对象的接口。你会观察到 JacksonAutoConfiguration 的那条 @ConditionalOnMissingBean 判定发生变化,而你自己的格式化规则生效了。这正是第十节决策的 B 选项。
  3. 故意制造冲突:在第 2 步基础上再打开 spring.main.allow-bean-definition-overriding=true,并把你的 Bean 命名成 objectMapper。你会观察到 启动不再提示冲突、但生效的是谁取决于注册顺序——把这条和上一轮实验的日志对比着记进笔记,你会永久记住为什么不该这么干。
  4. 用属性关掉一项自动配置:在 application.properties 写 spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration,删掉 H2 依赖前的顺序请留意。你会观察到 报告的 Exclusions 段出现它,同时正选里的 EmbeddedDataSourceConfiguration 一起消失。
130 / 141
小节
第三档 · 造一个
131 / 141

给自己做一个最小 starter greeting-spring-boot-starter,把本篇整条链路走通一遍。

132 / 141
  • 两个 Maven 模块:greeting-spring-boot-starter(只放依赖聚合)与 greeting-spring-boot-autoconfigure(放代码)
  • 自动配置类要求:@AutoConfiguration + @ConditionalOnClass(GreetingService.class) + @ConditionalOnMissingBean,并用 @EnableConfigurationProperties(GreetingProperties.class) 绑定前缀 acme.greeting
  • GreetingProperties 至少两个字段:prefix(默认 [hello])和 enabled(默认 true);@Bean 方法上再加一个 @ConditionalOnProperty(prefix = "acme.greeting", name = "enabled", havingValue = "true", matchIfMissing = true)
  • 在 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 里登记这个类
  • 另一个 demo 工程只依赖这个 starter,不写任何 @ComponentScan 相关配置,直接注入 GreetingService 并暴露 GET /greet
133 / 141

验收清单:① demo 里不加任何配置也能启动成功,/greet 返回带默认前缀的文本;② 改 acme.greeting.prefix 立刻生效;③ 设 acme.greeting.enabled=false 后启动失败并报「required a bean of type GreetingService」,且 --debug 落选段能指出是哪条属性条件拦住的;④ 把 optional 依赖去掉,观察 @ConditionalOnClass 让整个配置类消失;⑤ README 里画出「清单 → 过滤 → 排序 → 生效」四段,标注你这份文件分别落在哪一段。

134 / 141
小节
十七、要点自查
135 / 141
自检

不看上文,按顺序说出自动配置的四段流水线,以及每段大致做了什么。

136 / 141
自检

AutoConfigurationImportFilter 和 ConditionEvaluator 的区别是什么?为什么说前者是「快刀」、后者是「细筛」?

137 / 141
自检

spring.factories 与 AutoConfiguration.imports 的分水岭在哪个版本?升到 3.0 时哪种 starter 会静默失效?

138 / 141
自检

自动配置类的顺序由哪三层决定?为什么官方推荐 before/after 而不是 @AutoConfigureOrder?

139 / 141
自检

想覆盖一个自动配置好的 Bean,正确姿势是什么?用「精装房」和「借伞」两个类比各说一遍。

140 / 141
口诀

清单定候选、条件定生死、顺序定先后、延迟定谁让——四段流水线一句收。

141 / 141
总结

自动配置的完整链路是 @EnableAutoConfiguration → AutoConfigurationImportSelector(延迟导入)→ 读取 AutoConfiguration.imports 清单 → 快速筛选 + 条件求值 → 注册 BeanDefinition → refresh() 实例化。清单格式在 3.0 从 spring.factories 迁移到了 AutoConfiguration.imports;顺序靠 before/after、@AutoConfigureOrder、字母序三层确定;能否生效全看条件注解。最后记住两条实战铁律:排查自动配置先开 --debug 看条件报告,覆盖自动配置靠「自己定义 Bean + @ConditionalOnMissingBean 让路」而不是打开覆盖开关。