Spring Boot 自动装配原理深度剖析——不懂这个你只是个 API 搬运工
Spring Boot 自动装配原理深度剖析——不懂这个你只是个 API 搬运工
适读人群:有 Spring Boot 使用经验、想深入理解底层原理的 Java 工程师 | 阅读时长:约16分钟 | 核心价值:彻底理解自动装配的工作机制,具备自定义 Starter 和排查装配问题的能力
一、小王的困惑
小王跟我说过一句话,让我印象很深。他说:"老张,我用 Spring Boot 写了三年代码,加一个 @SpringBootApplication 注解,pom 里引个依赖,服务就能跑起来了。但我一直搞不明白,它凭什么就知道我需要什么 Bean,凭什么不需要我配置就自动生效了?"
我问他:"你有没有试过自己写一个 Starter?"他摇摇头。
我说:"那你现在确实还是个 API 搬运工。"
这话说得有点直,但我说的是实话。Spring Boot 的自动装配机制是这个框架最核心的设计,理解它之后,你才能真正读懂框架代码,才能写出属于自己的 Starter,才能在装配出问题的时候不慌不忙地定位。
这篇文章我就把自动装配从头到尾拆给你看。
二、从 @SpringBootApplication 开始拆解
自动装配的入口是 @SpringBootApplication 这个复合注解。很多人知道它等于三个注解,但具体每个做什么,说不清楚。
// @SpringBootApplication 是三个注解的组合
@SpringBootConfiguration // 等价于 @Configuration,标记为配置类
@EnableAutoConfiguration // 开启自动装配的关键
@ComponentScan // 扫描当前包及子包下的组件
public @interface SpringBootApplication {}其中 @EnableAutoConfiguration 是自动装配的真正触发器。展开它:
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {}核心是 @Import(AutoConfigurationImportSelector.class)。这个 AutoConfigurationImportSelector 就是整个自动装配机制的执行引擎。
三、AutoConfigurationImportSelector 的工作流程
AutoConfigurationImportSelector 实现了 DeferredImportSelector 接口。Spring 容器在处理配置类时,会调用它的 selectImports 方法,获取需要导入的配置类列表。
整个流程如下:
@SpringBootApplication
└── @EnableAutoConfiguration
└── AutoConfigurationImportSelector.selectImports()
└── getAutoConfigurationEntry()
├── getCandidateConfigurations() // 读候选配置类
├── removeDuplicates() // 去重
├── getExclusions() // 处理排除项
└── filter() // 条件过滤关键步骤是 getCandidateConfigurations(),这里决定了从哪里读配置类:
Spring Boot 2.7 之前:从 META-INF/spring.factories 文件读取 org.springframework.boot.autoconfigure.EnableAutoConfiguration 键对应的值。
Spring Boot 2.7+ / Spring Boot 3.x:从 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件读取(每行一个全类名)。两种方式兼容共存,但推荐新格式。
四、条件注解——自动装配的过滤器
候选的自动配置类可能有几百个,但最终只有少数几十个会真正生效。过滤的关键是各种 @ConditionalOn* 注解。
常用条件注解一览:
| 注解 | 生效条件 |
|---|---|
| @ConditionalOnClass | classpath 上存在某个类 |
| @ConditionalOnMissingClass | classpath 上不存在某个类 |
| @ConditionalOnBean | 容器中存在某个 Bean |
| @ConditionalOnMissingBean | 容器中不存在某个 Bean |
| @ConditionalOnProperty | 配置文件中存在某个属性且值匹配 |
| @ConditionalOnWebApplication | 当前是 Web 应用 |
| @ConditionalOnExpression | SpEL 表达式为 true |
以 Redis 的自动配置为例,它的生效条件就是 classpath 上有 RedisOperations 类:
@Configuration
@ConditionalOnClass(RedisOperations.class) // 只有引入 spring-data-redis 才生效
@EnableConfigurationProperties(RedisProperties.class)
public class RedisAutoConfiguration {
@Bean
@ConditionalOnMissingBean(name = "redisTemplate") // 用户没自定义才装配默认的
public RedisTemplate<Object, Object> redisTemplate(
RedisConnectionFactory redisConnectionFactory) {
// ...
}
}这就是自动装配"约定优于配置"的精髓:有你的类就装配,有你的 Bean 就让步。
五、手写一个完整的自定义 Starter
光说不练假把式。我们来写一个真实的 Starter:一个自动装配短信发送能力的 sms-spring-boot-starter。
项目结构:
sms-spring-boot-starter/
├── src/main/java/com/example/sms/
│ ├── SmsProperties.java // 配置属性映射
│ ├── SmsClient.java // 核心功能类
│ └── SmsAutoConfiguration.java // 自动配置类
└── src/main/resources/
└── META-INF/spring/
└── org.springframework.boot.autoconfigure.AutoConfiguration.imports第一步:定义配置属性
package com.example.sms;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* 短信配置属性,对应 application.yml 中 sms.* 前缀的配置项。
*/
@ConfigurationProperties(prefix = "sms")
public class SmsProperties {
/** 短信服务商 AccessKey */
private String accessKey;
/** 短信服务商 SecretKey */
private String secretKey;
/** 默认发送方签名 */
private String signName = "默认签名";
/** 是否开启短信功能,默认 true */
private boolean enabled = true;
// 省略 getter/setter(实际代码中必须有)
public String getAccessKey() { return accessKey; }
public void setAccessKey(String accessKey) { this.accessKey = accessKey; }
public String getSecretKey() { return secretKey; }
public void setSecretKey(String secretKey) { this.secretKey = secretKey; }
public String getSignName() { return signName; }
public void setSignName(String signName) { this.signName = signName; }
public boolean isEnabled() { return enabled; }
public void setEnabled(boolean enabled) { this.enabled = enabled; }
}第二步:实现核心功能类
package com.example.sms;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* 短信客户端,封装短信发送逻辑。
* 实际项目中这里调用阿里云、腾讯云等 SDK。
*/
public class SmsClient {
private static final Logger log = LoggerFactory.getLogger(SmsClient.class);
private final SmsProperties properties;
public SmsClient(SmsProperties properties) {
this.properties = properties;
}
/**
* 发送短信验证码。
*
* @param phone 目标手机号
* @param templateId 短信模板 ID
* @param code 验证码内容
* @return 是否发送成功
*/
public boolean sendVerifyCode(String phone, String templateId, String code) {
if (!properties.isEnabled()) {
log.warn("[SmsClient] 短信功能已关闭,跳过发送。phone={}", phone);
return true;
}
log.info("[SmsClient] 发送短信验证码。phone={}, templateId={}, signName={}",
phone, templateId, properties.getSignName());
// 实际项目此处调用第三方 SDK
// AliyunSmsClient.send(properties.getAccessKey(), properties.getSecretKey(), ...);
return true;
}
}第三步:编写自动配置类
package com.example.sms;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
/**
* 短信自动配置类。
* 条件:配置文件中存在 sms.access-key 属性时才装配。
* 如果用户自己定义了 SmsClient Bean,则不覆盖(@ConditionalOnMissingBean)。
*/
@AutoConfiguration // Spring Boot 3.x 推荐使用 @AutoConfiguration 替代 @Configuration
@EnableConfigurationProperties(SmsProperties.class)
@ConditionalOnProperty(prefix = "sms", name = "access-key")
public class SmsAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public SmsClient smsClient(SmsProperties properties) {
return new SmsClient(properties);
}
}第四步:注册自动配置类
在 src/main/resources/META-INF/spring/ 目录下创建文件: org.springframework.boot.autoconfigure.AutoConfiguration.imports
内容:
com.example.sms.SmsAutoConfiguration这样,任何项目只要在 pom.xml 里引入这个 Starter 的坐标,并在 application.yml 里配置 sms.access-key,SmsClient 就会被自动装配进容器,直接 @Autowired 使用。
六、踩坑实录
坑1:自定义 Starter 里的 @ComponentScan 导致 Bean 重复注册
现象:引入自定义 Starter 后,启动时报 BeanDefinitionOverrideException,提示某个 Bean 被重复定义。
原因:这个坑我自己踩过。Starter 的自动配置类里加了 @ComponentScan,扫描了 Starter 自己的包,但引用方的主应用也做了包扫描,两次扫描重叠,导致 Bean 被注册两次。
解法:自动配置类不要加 @ComponentScan。Starter 里的 Bean 应该通过 @Bean 方法显式注册,而不是靠扫描。这是写 Starter 的基本原则。
坑2:@ConditionalOnMissingBean 判断时机早于用户配置类
现象:我在自动配置类里写了 @ConditionalOnMissingBean(SmsClient.class),用户在业务代码里也定义了一个 SmsClient Bean,但两个都被装配进来了。
原因:Spring Boot 的自动配置类会在用户自定义配置类之后处理(DeferredImportSelector 的设计保证了这一点),所以理论上不会有这个问题。但如果用户的 SmsClient 定义在某个被 @Import 引入的类里,加载顺序可能有变化。
解法:用 @AutoConfigureAfter 或 @AutoConfigureBefore 显式声明自动配置类的加载顺序,确保判断时机正确。
坑3:spring.factories 和 imports 文件同时存在导致重复装配
现象:升级 Spring Boot 3.x 后,某个自动配置被执行了两次,日志里看到 Bean 创建了两遍。
原因:遗留的 META-INF/spring.factories 和新的 META-INF/spring/*.imports 同时存在,Spring Boot 3.x 两个都会读,导致重复。
解法:清理掉 spring.factories 中 EnableAutoConfiguration 相关的条目,只保留 imports 文件。
七、如何排查自动装配问题
遇到"为什么这个 Bean 没被装配"或"为什么装配了我不想要的 Bean",有三个工具可用:
方法1:开启 debug 模式
# application.yml
debug: true启动后控制台会打出 "CONDITIONS EVALUATION REPORT",清晰列出哪些自动配置类生效了、哪些没生效以及原因。
方法2:使用 actuator conditions 端点
GET /actuator/conditions返回 JSON 格式的条件评估报告,适合在运行时检查。
方法3:查看 AutoConfigurationReport
在代码里注入 ConditionEvaluationReport,可以编程式获取条件评估结果,适合写自动化测试。
八、自动装配的本质
总结一下我对自动装配的理解:
自动装配本质上是一套"有条件的 Bean 注册机制"。Spring Boot 预先为各种主流技术栈准备了配置类,这些配置类通过条件注解感知你的依赖和配置,按需把 Bean 注册进容器。你引入一个 starter,等于引入了一份"配置模板",但这份模板处处留着"用户优先"的口子——只要你自定义了,框架的默认就退让。
理解这个机制之后,你就能做三件事:
- 读懂任何框架的 AutoConfiguration 源码
- 写出自己的 Starter,把公司内的通用能力包装成开箱即用的组件
- 遇到装配异常不慌不忙,知道往哪里看
