主题
自动装配原理:@EnableAutoConfiguration 是怎么工作的
本文是 Spring 系统学习系列的 L2 核心篇。前置:容器启动与 BeanDefinition、Bean 生命周期扩展点。 学完可以配合面试题食用:08. Spring Boot 自动装配与 spring.factories
引入依赖就能用,是谁在背后布置 Bean
写 Spring Boot 应用时,pom.xml 里加了 spring-boot-starter-data-jpa,没写一行 XML 也没贴一个 @Bean,DataSource、JdbcTemplate、事务管理器就全都有了。这个现象背后是一条确定的执行链:@EnableAutoConfiguration 注解生效时,AutoConfigurationImportSelector 会去读固定路径的配置文件,把里面列出的上百个配置类逐个做条件评估,通过的才注册成 BeanDefinition。上一篇讲过容器启动的通用流程,这一篇把"配置类从哪来、谁放行、谁拦截"这一段拆开看。
加载链:从注解到 .imports 文件
@SpringBootApplication 是个组合注解,其中 @EnableAutoConfiguration 通过 @Import(AutoConfigurationImportSelector.class) 引入了一个 ImportSelector。容器刷新阶段处理配置类的 import 时,会调用它的 selectImports(),链路如下:
mermaid
sequenceDiagram
participant C as 配置类解析器
participant S as AutoConfigurationImportSelector
participant L as AutoConfigurationMetadata
participant AC as 条件评估
C->>S: selectImports()
S->>S: getCandidateConfigurations()
Note over S: SpringFactoriesLoader.load()<br/>读 META-INF/spring/<br/>org.springframework.boot.autoconfigure.AutoConfiguration.imports
S->>L: 去重 + 排除 exclude 属性指定的类
S->>AC: AutoConfigurationSorter 排序后逐个评估 @Conditional
AC-->>S: 不满足的整组丢弃
S-->>C: 返回通过的配置类全限定名要点有三个。第一,候选列表来自 SpringFactoriesLoader.load(),它扫描 classpath 上所有 jar 的 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件,每行一个配置类全限定名,Boot 官方 starter 和第三方 starter 都走这条路。第二,@EnableAutoConfiguration 的 exclude 属性在这里生效,直接从候选集里剔除。第三,排序不只是为了可读性:AutoConfigurationSorter 按字母序、@AutoConfigureOrder、@AutoConfigureBefore、@AutoConfigureAfter 确定评估顺序,因为一个配置类里 @ConditionalOnBean(DataSource.class) 是否成立,取决于 DataSource 的配置类有没有先被处理。
spring.factories 到 .imports 的演进
Boot 2.7 之前,自动配置类注册在 META-INF/spring.factories 里,key 是 org.springframework.boot.autoconfigure.EnableAutoConfiguration。2.7 开始迁到独立的 .imports 文件,spring.factories 中的相关条目在 Boot 3.0 彻底删除。为什么要拆?因为 spring.factories 是个大杂烩文件,内部监听器、环境后处理器、自动配置类混在一起,框架每次启动都要全量加载解析,而其中自动配置类占了绝大多数行数,既拖慢启动又没法单独裁剪。拆成纯文本的 .imports(一行一个类名)后,格式足够简单,给后续优化留了空间,加载语义也清晰了。写第三方 starter 时注意:给 Boot 3.x 用就必须用新路径,spring.factories 里的 EnableAutoConfiguration 条目不会再被读取。
条件注解族:评估时机与短路顺序
配置类进了候选集不代表会被注册,每个类上的 @Conditional 派生注解决定去留。评估发生在配置类解析阶段,也就是 BeanDefinition 注册之前,所以条件注解操纵的是"这个 Bean 定义进不进容器",而不是"这个 Bean 实例化不实例化"。常见的三员:
@ConditionalOnClass:classpath 上存在某个类才生效。判断的是字节码是否存在,不需要真的加载初始化它。典型用法:DataSourceAutoConfiguration上有@ConditionalOnClass({DataSource.class, EmbeddedDatabaseType.class }),没引 JDBC 相关依赖时整个类直接出局,连"找不到驱动类"的报错都不会有。@ConditionalOnMissingBean:容器里没有指定类型的 Bean 才注册自己这个。这是"用户配置优先"的实现机制, starter 定义的只是兜底默认值。@ConditionalOnProperty:Environment 里存在某属性且值匹配才生效,matchIfMissing = true表示属性没配也当作匹配。各种xxx.auto-enable开关就是它。
注意短路顺序:类级别的 @ConditionalOnClass 不满足时,类体内所有 @Bean 方法和注解属性都不再处理,方法级别的条件注解也不会评估。另外 @ConditionalOnMissingBean 的判断结果依赖 Bean 的注册顺序,只对自动配置类之间的顺序有保障(靠上面说的 Sorter),用户自己 @Configuration 里定义的 Bean 一定先于自动配置被处理,所以用户的 Bean 总能赢。
走一遍 DataSourceAutoConfiguration
以"引了 JDBC starter、配了数据源、又自己定义了一个 DataSource"为例,把整条逻辑串起来:
- classpath 有
DataSource.class(jdbc starter 传递引入),@ConditionalOnClass通过,配置类进入解析。 - 类上还有
@ConditionalOnMissingBean(type = "io.r2dbc.spi.ConnectionFactory")、@AutoConfigureBefore(SqlInitializationAutoConfiguration.class)等约束,逐一评估。 - 内部嵌套了
EmbeddedDatabaseCondition、PooledDataSourceCondition,根据是否配置了连接池相关属性,决定走内嵌库还是池化数据源。 - 关键在
@ConditionalOnMissingBean(DataSource.class):用户自己声明了一个DataSource@Bean,条件不成立,Boot 的默认数据源整组跳过。日志里那句DataSourceAutoConfiguration.DataSourceBeanMetadata...相关条目会出现在 Negative matches 里。
"有配置类则生效,用户 Bean 优先"就是这套条件组合的结果,不是什么额外魔法。
动手实操:--debug 报告
条件评估的结果不靠猜,Boot 提供了现成的报告。写一个最简应用:
java
// src/main/java/demo/Application.java
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}xml
<!-- pom.xml,只引 web,故意不引 jdbc -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>3.2.0</version>
</dependency>启动时加 --debug 参数:
bash
mvn spring-boot:run -Dspring-boot.run.arguments=--debug启动日志会打印 CONDITIONS EVALUATION REPORT,分四段。挑三行看:
text
Positive matches:
-----------------
DispatcherServletAutoConfiguration matched:
@ConditionalOnClass found required class 'org.springframework.web.servlet.DispatcherServlet'; @ConditionalOnMissingBean (types: org.springframework.web.servlet.DispatcherServlet; SearchStrategy: all) did not find any beans
Negative matches:
-----------------
DataSourceAutoConfiguration:
Did not match:
@ConditionalOnClass did not find required class 'javax.sql.DataSource'第一段是放行的配置类及命中的条件;第二段是被拦下的及具体原因(DataSourceAutoConfiguration 的 @ConditionalOnClass 未通过,报告展示的是 classpath 上缺失的特定类名)。报告能回答大部分"为什么我的 Bean 没生效"类问题:先看目标配置类在不在 Negative matches,再看挂在它下面的不匹配原因,基本能定位到缺依赖、缺属性或被用户 Bean 顶掉这三类。
常见误区与小结
- 把自动装配理解成"classpath 扫描",实际是读固定路径文件 + 条件评估,不会加载和扫描任意类。类路径上多一个 jar 不会带来额外开销,除非它注册了自动配置类且条件全部通过。
- 以为
@ConditionalOnMissingBean在任何场景都保用户 Bean 优先。它依赖处理顺序,用户@Configuration先于自动配置处理没问题,但两个自动配置类之间得靠@AutoConfigureBefore/After显式声明。 - 在普通
@Configuration(非自动配置类)上用@ConditionalOnClass期望它正常工作。条件注解的顺序保障只存在于自动配置体系内,普通配置类的解析顺序不受 Sorter 管,可能因编译顺序不同产生不一致行为,starter 的条件逻辑应放在自动配置类里。 - 排查 Bean 没生效时先翻代码猜条件。先跑
--debug看 Negative matches,比读源码快得多,原因就写在报告里。 - Boot 3 项目还在往 spring.factories 里注册自动配置类。那条通道已经关闭,必须用
META-INF/spring/...AutoConfiguration.imports。
小结:自动装配 = ImportSelector 加载 .imports 文件 + 条件注解逐个把关 + Sorter 保证评估顺序。它建立在容器启动和 BeanDefinition 注册这套底层机制之上,也是理解 starter 设计的前提。下一篇讲生产环境排障,会用到这里说的 --debug 报告。
参考
- Spring Boot 官方文档 "Auto-configuration" 章节(4.1.1 Conditional Annotations、4.1.2 Test Auto-configurations)
org.springframework.boot.autoconfigure.AutoConfigurationImportSelector源码(spring-boot-autoconfigure 模块)- Spring Boot 2.7 Release Notes 中 "Changes to Spring Autoconfiguration" 一节