1. 自动装配到底解决了什么问题
1.1 从手动配置的痛点说起
做过传统 Spring 项目的人应该都有体会:一个 Web 项目要跑起来,光是写配置就能耗掉大半天。数据源要配、事务管理器要配、MVC 要配、视图解析器要配,如果用 MyBatis 还得配 SqlSessionFactory、MapperScannerConfigurer。这些配置本身没什么技术含量,但少一个就启动不了,写错了还得排查半天。
我刚入行那会儿维护过一个老项目,光是 XML 配置文件就有十几个,每次加一个新模块,都要在对应的 XML 里加一堆 bean 定义。最头疼的是版本升级,Spring 从 4 升到 5 的时候,有些类的包路径变了,得一个个文件去改全限定名。那时候就想,能不能让框架自己知道我需要什么,自动帮我配好?
SpringBoot 的自动装配就是干这个的。它的核心思路是:约定优于配置。你引入一个 starter 依赖,它就默认帮你把相关的 bean 都注册好,你只需要在 application.properties 里改几个关键参数就行。比如引入 spring-boot-starter-web,它自动配好 DispatcherServlet、Jackson 消息转换器、内嵌 Tomcat;引入 spring-boot-starter-data-redis,它自动配好 RedisTemplate、连接工厂。这套机制让一个 Web 项目从零到能跑,压缩到了几分钟。
但自动装配不是魔法,它背后是一整套精心设计的机制:条件注解决定"要不要配",ImportSelector 决定"配哪些",SPI 机制决定"从哪里找配置"。把这套机制吃透,你不仅能看懂 SpringBoot 源码,还能自己写 starter,把团队里重复的配置逻辑封装起来。
1.2 自动装配的适用场景与学习价值
自动装配这套机制,表面上是给业务开发者用的,实际上它的价值在三个层面:
第一层,日常开发。你天天用的@SpringBootApplication、各种 starter,背后都是自动装配。理解它,你才能知道为什么有些 bean 莫名其妙就注入了,为什么有些配置改了不生效,为什么排除某个自动配置类能解决冲突。
第二层,中间件封装。团队里如果有自研的组件(比如公司内部的 RPC 框架、日志组件、监控 SDK),想让它接入 SpringBoot 项目时"引入即用",就得写自己的 starter。这时候你必须懂AutoConfigurationImportSelector和条件注解,否则写出来的 starter 要么不生效,要么到处冲突。
第三层,源码阅读能力。SpringBoot 的自动装配是"注解驱动开发"的集大成者,它把@Conditional、@Import、SPI、反射这些技术串成了一条完整的链路。把这条链路走通,你对 Spring 整个体系的理解会上一个台阶。
这篇文章我会从源码层面把这条链路拆开,重点讲清楚三件事:@EnableAutoConfiguration是怎么触发自动装配的,AutoConfigurationImportSelector是怎么筛选配置类的,条件注解是怎么决定一个配置类生不生效的。每一部分我都会配上源码片段和实际调试经验,尽量让你看完能自己动手写一个 starter。
2. 自动装配的整体设计与核心链路
2.1 从 @SpringBootApplication 到自动装配入口
一切从启动类的那个注解开始。你写一个 SpringBoot 项目,主类上通常只有一个@SpringBootApplication,但这个注解是个"复合注解",它内部包含了三个关键注解:
@SpringBootConfiguration @EnableAutoConfiguration @ComponentScan(excludeFilters = { ... }) public @interface SpringBootApplication { }@SpringBootConfiguration本质上就是@Configuration,标记这是一个配置类。@ComponentScan负责扫描当前包及子包下的@Component、@Service等注解。@EnableAutoConfiguration才是自动装配的总开关。
所以自动装配的入口非常明确,就是@EnableAutoConfiguration。点进去看它的定义:
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @AutoConfigurationPackage @Import(AutoConfigurationImportSelector.class) public @interface EnableAutoConfiguration { String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration"; Class<?>[] exclude() default {}; String[] excludeName() default {}; }这里有两个关键点。第一个是@AutoConfigurationPackage,它通过@Import(AutoConfigurationPackages.Registrar.class)把主类所在的包名注册到一个叫AutoConfigurationPackages的容器里,后面 JPA、MyBatis 这些需要扫描实体或 Mapper 的组件会用到这个包名。第二个是@Import(AutoConfigurationImportSelector.class),这才是自动装配真正干活的地方。
@Import是 Spring 提供的一个注解,用来把一个类导入到容器里。它支持三种类型:普通配置类、ImportSelector实现类、ImportBeanDefinitionRegistrar实现类。这里用的是ImportSelector,因为自动装配需要"动态决定导入哪些类",而不是写死一个类。
2.2 AutoConfigurationImportSelector 的定位与职责
AutoConfigurationImportSelector实现了ImportSelector接口,这个接口只有一个方法:
public interface ImportSelector { String[] selectImports(AnnotationMetadata importingClassMetadata); }Spring 在处理@Import注解时,如果发现导入的是ImportSelector的实现类,就会调用它的selectImports方法,拿到返回的类名数组,然后把这些类注册成 bean 定义。所以AutoConfigurationImportSelector的核心任务就是:返回所有需要自动装配的配置类全限定名。
但它的实现比接口定义复杂得多。它继承了一个叫AutoConfigurationImportSelector的父类体系,实际干活的方法在getAutoConfigurationEntry里。这个方法做了几件事:加载候选配置、去重、排除、过滤条件、触发事件。我把它简化成一条链路:
selectImports -> getAutoConfigurationEntry -> getCandidateConfigurations // 从 META-INF 加载候选 -> removeDuplicates // 去重 -> getExclusions // 处理 exclude -> fireAutoConfigurationImportEvents // 触发监听器 -> filter // 条件过滤 -> 返回最终类名数组这条链路里,getCandidateConfigurations和filter是两个最关键的环节。前者决定了"候选池"从哪来,后者决定了"候选池"里哪些能留下。下面分别拆开讲。
2.3 候选配置的加载机制:SPI 思想的落地
getCandidateConfigurations的源码大致是这样:
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) { List<String> configurations = SpringFactoriesLoader.loadFactoryNames( getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader()); Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories..."); return configurations; }关键在SpringFactoriesLoader.loadFactoryNames。这个方法会去扫描 classpath 下所有 jar 包里的META-INF/spring.factories文件,找到 key 为org.springframework.boot.autoconfigure.EnableAutoConfiguration的那一行,把对应的类名列表读出来。
spring.factories是一个 properties 格式的文件,内容长这样:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration,\ org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration,\ org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration这就是典型的 SPI(Service Provider Interface)思想:框架定义接口和加载规则,各个模块提供实现并声明在约定文件里,框架运行时统一加载。JDK 自带的ServiceLoader也是这个思路,只不过 Spring 自己实现了一套更灵活的版本,支持一个 key 对应多个实现,也支持排序。
注意:SpringBoot 2.7 之后引入了新的
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件来替代spring.factories中的自动配置声明,但为了兼容,spring.factories的方式仍然保留。如果你在写新 starter,建议用新的 imports 文件。
这里有个容易踩的坑:spring.factories里的类名如果写错了,或者对应的类不存在,启动时会直接报错,而且报错信息不一定直观。我遇到过好几次因为依赖版本不匹配,某个自动配置类被移除了,但spring.factories里还留着,结果启动就炸。排查的时候要仔细看异常栈里提到的类名。
3. 条件注解的实现机制
3.1 @Conditional 的底层原理
候选配置加载出来之后,并不是全部都会生效。比如你项目里没引入 Redis 依赖,RedisAutoConfiguration就不应该生效,否则会因为找不到RedisConnectionFactory类而报错。这个"要不要生效"的判断,就是条件注解干的活。
条件注解的根是@Conditional:
@Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Conditional { Class<? extends Condition>[] value(); }它接收一个Condition接口的实现类数组。Condition接口定义很简单:
@FunctionalInterface public interface Condition { boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata); }matches返回 true 就表示条件满足,配置类或 bean 生效;返回 false 就跳过。ConditionContext提供了访问BeanFactory、Environment、ResourceLoader、ClassLoader的能力,所以你可以基于"类是否存在""配置项是什么值""某个 bean 是否已注册"等任意条件来判断。
Spring 处理@Conditional的时机是在ConfigurationClassPostProcessor解析配置类的时候。它会遍历配置类上的所有条件注解,找到对应的Condition实现,逐个调用matches,全部返回 true 才继续处理这个配置类。
3.2 SpringBoot 扩展的条件注解家族
SpringBoot 在@Conditional基础上封装了一大批语义化的条件注解,常用的有这些:
| 注解 | 作用 | 典型场景 |
|---|---|---|
@ConditionalOnClass | 类路径存在指定类时生效 | 判断依赖是否引入 |
@ConditionalOnMissingClass | 类路径不存在指定类时生效 | 兜底配置 |
@ConditionalOnBean | 容器中存在指定 bean 时生效 | 依赖其他配置 |
@ConditionalOnMissingBean | 容器中不存在指定 bean 时生效 | 允许用户覆盖默认配置 |
@ConditionalOnProperty | 配置项满足条件时生效 | 开关式配置 |
@ConditionalOnResource | 资源文件存在时生效 | 配置文件驱动 |
@ConditionalOnWebApplication | 是 Web 应用时生效 | Web 相关配置 |
@ConditionalOnExpression | SpEL 表达式为 true 时生效 | 复杂条件 |
这些注解背后都对应一个Condition实现类,比如@ConditionalOnClass对应OnClassCondition,@ConditionalOnMissingBean对应OnBeanCondition。它们都继承自SpringBootCondition,这个抽象类实现了Condition接口,并提供了日志记录和报告机制。
SpringBootCondition的matches方法大致是这样:
public final boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { String classOrMethodName = getClassOrMethodName(metadata); ConditionOutcome outcome = getMatchOutcome(context, metadata); logOutcome(classOrMethodName, outcome); recordEvaluation(context, classOrMethodName, outcome); return outcome.isMatch(); }子类只需要实现getMatchOutcome方法,返回一个ConditionOutcome对象,里面包含是否匹配以及原因。这个设计很巧妙:把判断逻辑和日志记录解耦,子类专注判断,父类负责记录。调试的时候打开ConditionEvaluationReport的日志,就能看到每个条件为什么匹配或不匹配,非常有用。
3.3 @ConditionalOnMissingBean 的覆盖机制
@ConditionalOnMissingBean是自动装配里最重要的注解之一,它实现了"默认配置可被用户覆盖"的能力。举个例子,DataSourceAutoConfiguration里注册数据源时会加这个注解:
@Bean @ConditionalOnMissingBean public DataSource dataSource() { return DataSourceBuilder.create().build(); }意思是:如果容器里已经有一个DataSourcebean(比如用户自己用@Bean定义了一个),那这个默认的就不注册了。这样用户想自定义数据源,直接写一个@Bean方法就行,不用去排除自动配置类。
但这个注解有个顺序陷阱:它只能判断"当前已经注册的 bean",如果用户的 bean 定义在自动配置之后才被解析,那@ConditionalOnMissingBean就会误判。所以 SpringBoot 规定,自动配置类必须通过@AutoConfigureAfter、@AutoConfigureBefore控制顺序,确保用户配置先于自动配置处理。这也是为什么自定义 starter 时,顺序注解不能乱写。
实操心得:如果你写的自动配置类里用了
@ConditionalOnMissingBean,但发现用户的自定义 bean 没生效,八成是顺序问题。检查一下有没有加@AutoConfigureAfter,或者用户配置类是不是被@ComponentScan扫到了。
4. 手写一个 Starter 的完整实操
4.1 Starter 的模块划分与命名规范
理解了原理,最好的验证方式就是自己写一个 starter。我以"请求日志组件"为例,做一个引入即用的 starter,它能自动记录每个 HTTP 请求的耗时和路径。
一个标准的 starter 通常拆成两个模块:
xxx-spring-boot-starter:空 jar,只负责声明依赖,把 autoconfigure 模块和第三方依赖引进来。xxx-spring-boot-autoconfigure:真正包含自动配置逻辑的模块。
为什么要拆两个?因为有些项目只想用你的自动配置,不想引入 starter 里的其他依赖,拆开更灵活。当然小项目合成一个也行,但命名上建议遵循xxx-spring-boot-starter的约定,这是社区惯例。
目录结构大致这样:
request-log-spring-boot-starter/ pom.xml request-log-spring-boot-autoconfigure/ src/main/java/com/example/log/ RequestLogAutoConfiguration.java RequestLogProperties.java RequestLogInterceptor.java src/main/resources/META-INF/ spring.factories additional-spring-configuration-metadata.json4.2 配置属性类的绑定
先写配置属性类,让用户能在application.properties里配置:
@ConfigurationProperties(prefix = "request.log") public class RequestLogProperties { private boolean enabled = true; private String prefix = "[REQ]"; private boolean includeHeaders = false; // getter/setter 省略 }@ConfigurationProperties会把request.log.enabled、request.log.prefix这些配置项绑定到字段上。注意字段要有 getter/setter,否则绑定不上。如果想在 IDE 里有自动补全提示,还需要在META-INF下放一个additional-spring-configuration-metadata.json,声明每个属性的类型和描述。
4.3 自动配置类的编写与条件控制
核心的自动配置类:
@Configuration @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) @ConditionalOnProperty(prefix = "request.log", name = "enabled", havingValue = "true", matchIfMissing = true) @EnableConfigurationProperties(RequestLogProperties.class) public class RequestLogAutoConfiguration { @Bean @ConditionalOnMissingBean public RequestLogInterceptor requestLogInterceptor(RequestLogProperties properties) { return new RequestLogInterceptor(properties); } @Bean public WebMvcConfigurer requestLogWebMvcConfigurer(RequestLogInterceptor interceptor) { return new WebMvcConfigurer() { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(interceptor).addPathPatterns("/**"); } }; } }这里几个注解各有分工:
@ConditionalOnWebApplication保证只在 Servlet Web 应用里生效,普通应用不加载。@ConditionalOnProperty提供开关,用户设置request.log.enabled=false就能关掉。@EnableConfigurationProperties把属性类注册成 bean,这样拦截器能注入它。@ConditionalOnMissingBean允许用户自己定义拦截器覆盖默认的。
拦截器本身很简单,实现HandlerInterceptor,在preHandle记录开始时间,afterCompletion计算耗时并打印。
4.4 注册自动配置类与调试验证
最后一步,在META-INF/spring.factories里声明:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.example.log.RequestLogAutoConfiguration如果用 SpringBoot 2.7+,也可以放在META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports里,一行一个类名。
写完打包安装到本地仓库,在测试项目里引入依赖,启动后访问任意接口,控制台就会打印请求日志。验证的时候有几个检查点:
- 启动日志里搜索
RequestLogAutoConfiguration,看它有没有被加载。 - 打开
debug=true,看ConditionEvaluationReport里这个配置类的匹配结果。 - 如果没生效,先检查
spring.factories路径和类名有没有写错,再看条件注解是不是把配置类排除了。
我踩过的一个坑是:spring.factories文件放在了src/main/resources/META-INF/下,但打包时被 maven 的 resource 过滤规则排除了,导致 jar 里根本没有这个文件。排查了半天才发现是 pom 里<resources>配置的问题。所以打包后一定要unzip -l看一下 jar 里的文件结构。
5. 常见问题与排查技巧实录
5.1 自动配置不生效的排查思路
自动配置不生效是最常见的问题,排查可以按这个顺序来:
| 排查点 | 检查方法 | 常见原因 |
|---|---|---|
| 配置类是否被加载 | 启动日志搜类名 | spring.factories 路径错、类名错 |
| 条件是否匹配 | 开启 debug 看报告 | 条件注解判断失败 |
| 顺序是否正确 | 看 @AutoConfigureAfter | 被其他配置覆盖 |
| 是否被排除 | 检查 exclude 配置 | 用户手动排除了 |
| 依赖是否引入 | mvn dependency:tree | starter 依赖没传递进来 |
开启 debug 的方式是在application.properties里加debug=true,启动后控制台会打印一份ConditionEvaluationReport,里面列出了所有自动配置类的匹配情况,包括"Positive matches"和"Negative matches",Negative 里还会写明不匹配的原因。这份报告是排查自动配置问题的第一手资料,比瞎猜高效得多。
5.2 条件注解的常见误用
条件注解用错了,会导致配置时灵时不灵。几个典型误用:
误用一:@ConditionalOnClass写在@Bean方法上。这个注解在类级别和方法级别都能用,但方法级别判断的是"方法返回类型对应的类是否存在",而不是方法体里用到的类。如果你在方法体里 new 了一个可能不存在的类,光加@ConditionalOnClass是不够的,因为类加载时方法体里的引用可能已经触发NoClassDefFoundError。正确做法是把整个配置类用@ConditionalOnClass包起来。
误用二:@ConditionalOnMissingBean判断的 bean 类型太宽泛。比如你写@ConditionalOnMissingBean(Object.class),那永远不匹配,因为容器里肯定有 Object 类型的 bean。要精确到具体类型。
误用三:@ConditionalOnProperty的havingValue和matchIfMissing搞混。havingValue是"配置值等于什么才匹配",matchIfMissing是"配置项不存在时是否匹配"。两个一起用时,如果配置项不存在,走matchIfMissing;存在则比较havingValue。
5.3 自动配置的调试技巧
除了debug=true,还有几个实用技巧:
- 用
ConditionEvaluationReport的 API:在测试里注入ApplicationContext,拿到ConditionEvaluationReportbean,可以编程式地查询某个配置类的匹配结果。 - 断点打在
AutoConfigurationImportSelector.getAutoConfigurationEntry:能看到候选配置的完整列表和过滤后的结果,对比一下就知道哪些被过滤了。 - 用
@ImportAutoConfiguration在测试里精确控制:写单元测试时,不想加载全部自动配置,可以用这个注解只导入需要的。 - 排除法定位:如果怀疑某个自动配置导致问题,用
@SpringBootApplication(exclude = XxxAutoConfiguration.class)排除掉,看问题是否消失。
实操心得:我习惯在写 starter 的时候,专门写一个测试类,用
ApplicationContextRunner来验证各种条件下的行为。这个工具能模拟不同的类路径和配置,非常轻量,比启动整个应用快得多。
6. 从源码看自动装配的设计取舍
6.1 为什么用 ImportSelector 而不是 ImportBeanDefinitionRegistrar
@Import支持两种动态导入方式:ImportSelector和ImportBeanDefinitionRegistrar。自动装配选了前者,原因是ImportSelector只需要返回类名数组,Spring 会走正常的配置类解析流程,条件注解、属性绑定、bean 注册这些机制都能正常用。而ImportBeanDefinitionRegistrar需要手动注册BeanDefinition,绕过了配置类解析,条件注解就不生效了。自动装配的核心诉求是"条件化地导入配置类",所以ImportSelector是更合适的选择。
6.2 条件注解的性能考量
条件注解在启动时会执行大量判断,尤其是@ConditionalOnClass需要加载类、@ConditionalOnBean需要查询容器。SpringBoot 对此做了优化:OnClassCondition会缓存类加载结果,OnBeanCondition会缓存 bean 查询结果。另外,条件判断的结果会被记录到ConditionEvaluationReport,这个报告本身也有开销,所以生产环境建议关掉 debug。
还有一个细节:@ConditionalOnClass在类级别和方法级别的处理方式不同。类级别用 ASM 字节码读取,避免真正加载类;方法级别才用反射。这是为了避免"类不存在时加载配置类本身失败"的问题。理解这一点,你就能明白为什么有些配置类要拆成嵌套类来写。
6.3 自动装配的演进方向
从 SpringBoot 1.x 到 3.x,自动装配机制一直在演进。1.x 用spring.factories,2.7 引入AutoConfiguration.imports,3.x 全面转向新文件。新格式的好处是:一个文件一行类名,解析更快,也更容易做工具支持。另外,3.x 对 AOT 编译做了大量优化,条件判断在编译期就能确定一部分,减少运行时的开销。
这些演进背后的思路是一致的:在保持"约定优于配置"的前提下,不断提升启动性能和可维护性。作为使用者,我们不需要追每一个版本的变化,但理解这套机制的核心链路,能让你在面对新版本时快速上手。
写到这里,关于自动装配的核心链路基本讲完了。从@EnableAutoConfiguration触发,到AutoConfigurationImportSelector加载候选,再到条件注解过滤,最后到 bean 注册,这条链路是 SpringBoot 最精妙的设计之一。我个人的体会是,光看源码容易忘,最好的方式是动手写一个 starter,把每个环节都跑一遍,遇到问题再回去看源码,这样理解才扎实。