Spring Boot 自动装配机制的原理
Spring Boot 自动装配机制的原理
面试指北 · 蔚来
题意
面试官问「Spring Boot 自动装配的原理」,表面上是在问一个注解,实际上想确认三件事:
- 你是否知道
@SpringBootApplication这个注解到底拆成了哪几个注解,各自负责什么; - 你是否清楚「引入一个 starter 依赖,就能直接用上里面的 Bean」这条链路是怎么走通的,配置文件在哪里、由谁读取、什么时候读取;
- 你是否理解「条件注解 + 顺序控制」这套设计,为什么它能做到「你有就用你的,你没有我用默认的」。
换句话说,这道题的完整回答应该覆盖:入口注解 → 候选配置的发现 → 候选配置的过滤 → 配置类里 Bean 的注册 → 用户自定义的覆盖优先级。原文给出的三条(依赖注入 / 自动配置 / 组件扫描)是这条链路上的三个点,下面把它们串成一条线。
结论
先给一句能在面试里直接说出口的总结:
Spring Boot 的自动装配,本质上是把「需要哪些配置类」这件事从「开发者手写
@Configuration」变成了「框架根据 classpath 上有什么依赖,用 SPI 的方式把一批候选配置类捞出来,再用条件注解现场筛一遍,最后@Import进容器」。
拆开就是四个关键词:
- 入口:
@SpringBootApplication=@SpringBootConfiguration+@EnableAutoConfiguration+@ComponentScan; - 发现:
@EnableAutoConfiguration通过@Import(AutoConfigurationImportSelector.class)引入选择器,选择器从META-INF/spring.factories(Boot 2.7 之前)或META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports(Boot 2.7+ / 3.x)里读出候选配置类的全限定名列表; - 过滤:候选配置类上的一堆
@ConditionalOnXxx条件注解,在启动时逐个求值,不满足的直接跳过,所以「引入了一百多个自动配置类」并不会真的注册一百多个 Bean; - 覆盖:用户自己声明了同类型 Bean,
@ConditionalOnMissingBean就让自动配置退让,这就是「约定优于配置 + 可覆盖」。
至于原文提到的依赖注入(@Autowired / @Qualifier)和组件扫描(@ComponentScan),它们解决的是你自己的代码怎么进容器;自动装配解决的是第三方 jar 包里的代码怎么进容器。这是两件不同的事,面试时一定要分开讲,混在一起说会被追问到答不上来。
要点展开
1. 从入口注解拆起
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
@SpringBootApplication 是一个组合注解,核心是三个:
@SpringBootConfiguration // 本质是 @Configuration,标记这是一个配置类
@EnableAutoConfiguration // 自动装配的总开关
@ComponentScan // 扫描当前类所在包及其子包
三者分工很清楚:
| 注解 | 负责什么 | 作用范围 |
|---|---|---|
@ComponentScan |
扫描业务代码里的 @Component / @Service / @Repository / @Configuration |
主类所在包及其子包 |
@Autowired / @Qualifier |
在容器已有的 Bean 之间建立依赖关系 | 容器内 |
@EnableAutoConfiguration |
把第三方 jar 里的配置类批量导入 | classpath 上所有 jar |
注意 @ComponentScan 的默认范围是主启动类所在的包。这也是为什么「把启动类放到最外层包」是 Spring Boot 的约定——放偏了,业务类就扫不到。但它扫不到别的 jar 包,别的 jar 也不会按你的包路径来组织代码,所以必须另有一套机制,这就是自动装配存在的根本原因。
2. @EnableAutoConfiguration 干了什么
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage // 把主类所在包注册成一个 AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class) // 关键:引入选择器
public @interface EnableAutoConfiguration {
String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
Class<?>[] exclude() default {};
String[] excludeName() default {};
}
这里有两个细节值得说:
@AutoConfigurationPackage把主类所在包记录到一个BasePackages里,JPA 的实体扫描、MyBatis 的 Mapper 扫描都依赖它,可以理解为「告诉第三方框架你的业务代码在哪个包」。@Import引入的是AutoConfigurationImportSelector,它实现了ImportSelector接口,作用是返回一批类的全限定名,让 Spring 把这些类当成配置类处理。
AutoConfigurationImportSelector 的实现(简化):
public class AutoConfigurationImportSelector
implements DeferredImportSelector, BeanClassLoaderAware, ResourceLoaderAware,
BeanFactoryAware, EnvironmentAware, Ordered {
@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return NO_IMPORTS;
}
AutoConfigurationEntry entry = getAutoConfigurationEntry(annotationMetadata);
return StringUtils.toStringArray(entry.getConfigurations());
}
protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata metadata) {
// 1. 读取候选配置类
List<String> configurations = getCandidateConfigurations(metadata, null);
// 2. 去重
configurations = removeDuplicates(configurations);
// 3. 去掉 @SpringBootApplication(exclude=...) 和 spring.autoconfigure.exclude 里声明的
Set<String> exclusions = getExclusions(metadata, null);
configurations.removeAll(exclusions);
// 4. 用 AutoConfigurationImportFilter 做一次快速过滤(OnClassCondition / OnBeanCondition / OnWebApplicationCondition)
configurations = getConfigurationClassFilter().filter(configurations);
// 5. 触发 AutoConfigurationImportEvent 事件
fireAutoConfigurationImportEvents(configurations, exclusions);
return new AutoConfigurationEntry(configurations, exclusions);
}
}
这里有个极易被忽略但很加分的点:它实现的是 DeferredImportSelector 而不是普通的 ImportSelector。
DeferredImportSelector 的「延迟」体现在:它要等所有用户自己写的 @Configuration 都处理完之后才执行。正因为如此,自动配置类里的 @ConditionalOnMissingBean 才能准确判断「用户到底有没有定义这个 Bean」——如果它和用户配置同时处理,顺序就不可控,判断结果也就不可靠。
这就是「用户配置优先于自动配置」这条规则的技术底座。
3. 候选配置类从哪来:spring.factories → AutoConfiguration.imports
早期(Spring Boot 2.6 及以前)用的是 Spring 自己的 SPI 机制:
# META-INF/spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.starter.FooAutoConfiguration,\
com.example.starter.BarAutoConfiguration
SpringFactoriesLoader.loadFactoryNames(EnableAutoConfiguration.class, classLoader) 会把 classpath 上所有 jar 里这个 key 对应的值汇总成一个列表。
Spring Boot 2.7 开始引入了新文件,3.0 起彻底移除了 spring.factories 中对 EnableAutoConfiguration 的支持:
# META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
com.example.starter.FooAutoConfiguration
com.example.starter.BarAutoConfiguration
一行一个类名,可读性更好,也不用担心 properties 里的转义和换行。
对应的注解也从 @Configuration 换成了 @AutoConfiguration:
@AutoConfiguration(after = DataSourceAutoConfiguration.class)
@ConditionalOnClass(DataSource.class)
@EnableConfigurationProperties(FooProperties.class)
public class FooAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public FooService fooService(FooProperties properties) {
return new FooService(properties.getPrefix());
}
}
@AutoConfiguration 本质是 @Configuration(proxyBeanMethods = false) 加上 before / after 两个排序属性,是给自动配置类专用的语义化注解。
面试表述建议:「Spring Boot 2.7 之前是 spring.factories + SpringFactoriesLoader,2.7 引入了 AutoConfiguration.imports,3.0 之后自动配置只认新文件;但 spring.factories 本身没废弃,像 EnvironmentPostProcessor、ApplicationContextInitializer 这些扩展点仍然用它。」这样说既准确又能体现你关注版本差异。
4. 条件注解:按需装配的关键
候选列表可能有上百个类,但绝大多数都不该生效。真正做「筛」的动作,是配置类上的条件注解:
| 注解 | 生效条件 | 典型场景 |
|---|---|---|
@ConditionalOnClass |
classpath 上存在指定类 | 有 DataSource 才装配数据源相关配置 |
@ConditionalOnMissingClass |
classpath 上不存在指定类 | 排除某实现 |
@ConditionalOnBean |
容器中已有指定 Bean | 依赖其他配置的产物 |
@ConditionalOnMissingBean |
容器中没有指定 Bean | 给用户留出覆盖口 |
@ConditionalOnProperty |
配置项等于指定值 / 不为 false | @ConditionalOnProperty(prefix="foo", name="enabled", havingValue="true") |
@ConditionalOnWebApplication |
当前是 Web 应用(可细分 SERVLET / REACTIVE) | Web 相关组件 |
@ConditionalOnResource |
存在指定资源文件 | 有配置文件才生效 |
@ConditionalOnExpression |
SpEL 表达式为真 | 复杂条件 |
判断结果会被记录进 ConditionEvaluationReport,启动时加 --debug 或者打开 actuator 的 /actuator/conditions 端点就能看到「哪些配置类生效了、哪些没生效、为什么没生效」——这是排查「我的 Bean 怎么没注进来」最快的手段,比瞎猜强得多。
5. 排序与覆盖:谁先谁后、谁说了算
排序用三个注解(Boot 3 里也可以用 @AutoConfiguration(before/after)):
@AutoConfigureBefore(DataSourceAutoConfiguration.class)
@AutoConfigureAfter(JacksonAutoConfiguration.class)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
覆盖则靠两件事配合:
- 自动配置类里的
@Bean方法上普遍带@ConditionalOnMissingBean; AutoConfigurationImportSelector是DeferredImportSelector,天然后处理。
所以:
// 用户自己的配置
@Configuration
public class MyConfig {
@Bean
public FooService fooService() {
return new MyFooServiceImpl(); // 只要这个存在
}
}
自动配置里的 fooService() 就不会再注册,用户实现直接胜出。这就是「约定优于配置,但配置可被覆盖」的落地方式。
6. 组件扫描与依赖注入各自的活
这两点和自动装配经常被混为一谈,单独拎出来说清楚:
@ComponentScan:按包路径找类,只能扫到启动类所在包及子包,也就是「你自己的代码」。- 自动装配:按类全名列表导入,不受包路径限制,能覆盖 classpath 上任意 jar 里的配置类。
@Autowired/@Qualifier/@Resource:Bean 已经在容器里了,它只负责建立引用关系。类型冲突时用@Qualifier("beanName")指定名称,或者用@Primary标记默认实现。
一句话:扫描和自动装配是「往容器里放东西」,依赖注入是「在容器里找东西」。
7. 自己写一个 starter 的骨架
面试里如果被追问「你写过 starter 吗」,可以讲结构而不必编造业务细节:
foo-spring-boot-starter/
└── pom.xml # 只做依赖聚合,不写代码
foo-spring-boot-autoconfigure/
├── src/main/java/.../FooProperties.java
├── src/main/java/.../FooAutoConfiguration.java
└── src/main/resources/META-INF/spring/
org.springframework.boot.autoconfigure.AutoConfiguration.imports
@AutoConfiguration
@ConditionalOnClass(FooClient.class)
@EnableConfigurationProperties(FooProperties.class)
public class FooAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public FooClient fooClient(FooProperties properties) {
return new FooClient(properties.getEndpoint(), properties.getTimeout());
}
}
FooProperties 用 @ConfigurationProperties(prefix = "foo") 绑定 application.yml 里的配置,配合 @EnableConfigurationProperties 注册。
三个设计要点:starter 只做依赖聚合、autoconfigure 里全用条件注解、所有 @Bean 都加 @ConditionalOnMissingBean 留出覆盖口。
易错点与对比
1. 「自动装配」和「自动配置」不是一回事。
严格说,AutoConfiguration 是自动配置(往容器里注册 Bean),而「装配」在 Spring 语境里指的是 Bean 之间的依赖注入(wiring)。面试时如果面试官用词模糊,你可以先点一句「我理解您问的是自动配置 auto-configuration 这部分」,显得你概念是清楚的。
2. @ConditionalOnMissingBean 放错位置会失效。
它只在「被判断的 Bean 一定已经处理过」时才可靠。放在自动配置类里是安全的(延迟导入保证后处理),但如果随手写在用户自己的 @Configuration 中,两个配置类的处理顺序不确定,结果就不可靠。Spring 官方文档明确建议:@ConditionalOnMissingBean 只用在自动配置类里。
3. @ConditionalOnClass 引用缺失类时的坑。
如果注解所在的方法返回类型直接写了那个缺失的类,JVM 加载方法签名时就会 NoClassDefFoundError。安全写法是把条件注解放到类上,或者用 name 属性写字符串:
@ConditionalOnClass(name = "com.example.MaybeMissingClass")
4. 条件注解是「启动时一次性判断」,不是运行时开关。
想动态切换 Bean 得用 @Profile、@ConditionalOnProperty 配合重启,或者 @Scope("prototype") / 工厂模式,别指望条件注解能热更新。
5. 版本差异别答错。
| 版本 | 自动配置的注册文件 | 推荐注解 |
|---|---|---|
| Boot 2.6 及以前 | META-INF/spring.factories |
@Configuration |
| Boot 2.7 ~ 2.x | 两者都支持,spring.factories 方式被标记废弃 |
@AutoConfiguration |
| Boot 3.x | 只认 AutoConfiguration.imports |
@AutoConfiguration |
6. 与 SPI 的关系。
spring.factories 本质就是 Java SPI 思想的一个变种——「接口 + 约定路径的配置文件 + 加载器」,只是 Spring 做成了 key-value 形式,支持多个实现按 key 聚合。把这个类比说出来,能体现你有抽象能力。
7. 三种「让 Bean 进容器」的方式对比。
| 方式 | 触发依据 | 能否跨 jar | 典型用途 |
|---|---|---|---|
@ComponentScan |
包路径 | 不能(除非包名恰好匹配) | 自己的业务组件 |
@Import / 自动配置 |
类全名列表 | 能 | 三方 starter |
@Bean 方法 |
手写 | 能 | 精细控制第三方类 |
小结
把整条链路压缩成一句话,方便面试时一口气说完:
@SpringBootApplication里的@EnableAutoConfiguration通过@Import引入了AutoConfigurationImportSelector,这个选择器作为DeferredImportSelector延迟到用户配置处理完之后,从AutoConfiguration.imports(旧版是spring.factories)中读出所有候选配置类,先按exclude和AutoConfigurationImportFilter做粗筛,再在解析配置类的过程中用@ConditionalOnClass、@ConditionalOnMissingBean等条件注解逐个精筛,最终把满足条件的@Bean注册进容器;同时因为自动配置最后处理,用户自定义的 Bean 天然拥有更高优先级,这就是「开箱即用 + 可覆盖」的实现原理。
答题时建议按 入口注解 → 候选发现 → 条件过滤 → 覆盖机制 的顺序讲,中途主动提一句 DeferredImportSelector 和 Spring Boot 3 的文件变更,基本就能把这道题答到「超出预期」的位置。