用过 Spring Boot 的人都知道"引入依赖就能用",但一旦要自己封装一个公共组件给团队复用,就必须搞懂自动装配。本文从源码层面拆开 ,然后带你手写一个能放进生产环境的 Starter。
一、自动装配到底做了什么
1.1 从 @SpringBootApplication 拆起
每个 Spring Boot 项目的启动类上都挂着这么一个注解:
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
它其实是一个"三合一"的组合注解,扒开源码能看到:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@SpringBootConfiguration // ① 本质就是 @Configuration
@EnableAutoConfiguration // ② 自动装配的核心开关
@ComponentScan // ③ 扫描当前包及子包下的组件
public @interface SpringBootApplication {
// ...
}
三个注解各司其职:
| 注解 | 作用 | 不写会怎样 |
|---|---|---|
@SpringBootConfiguration |
标记这是一个配置类,允许在类中定义 @Bean |
启动类里的 @Bean 不生效 |
@EnableAutoConfiguration |
开启自动装配,加载 classpath 下的自动配置类 | 所有 starter 全部失效 |
@ComponentScan |
扫描 @Component @Service 等 |
自己写的业务 Bean 扫不进来 |
其中 ② 才是本文的主角。
1.2 @EnableAutoConfiguration 如何工作
点进 @EnableAutoConfiguration,会看到它通过 @Import 引入了一个选择器:
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
// ...
}
AutoConfigurationImportSelector 实现了 DeferredImportSelector,它最核心的方法是 selectImports(),简化后逻辑如下:
@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return NO_IMPORTS;
}
// 1. 加载所有候选自动配置类
AutoConfigurationEntry entry = getAutoConfigurationEntry(annotationMetadata);
return StringUtils.toStringArray(entry.getConfigurations());
}
getAutoConfigurationEntry() 内部做了四件事,这是理解自动装配的关键:
protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata metadata) {
// 步骤一:读取 spring.boot.enableautoconfiguration 开关,默认 true
if (!isEnabled(metadata)) return EMPTY_ENTRY;
// 步骤二:加载所有候选配置类的全限定名(去重)
List<String> configurations = getCandidateConfigurations(metadata, attributes);
// 步骤三:按 @Order / AutoConfigureOrder 排序
configurations = sort(configurations, metadata);
// 步骤四:逐个执行 @ConditionalOnXxx 条件判断,过滤掉不满足的
configurations = getConfigurationClassFilter().filter(configurations);
return new AutoConfigurationEntry(configurations, exclusions);
}
关键点:自动装配不是"无脑全部加载",而是"先全量加载候选,再按条件逐个淘汰"。这也是为什么自动配置类里满屏都是 注解。
1.3 候选配置从哪里读:Spring Boot 2 与 3 的差异
这是面试高频、也最容易踩坑的地方。两个大版本的配置文件位置和格式都不一样。
Spring Boot 2.x —— 读取所有 jar 包里的:
META-INF/spring.factories
内容格式是 properties:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.demo.DemoAutoConfiguration,\
com.example.demo.OtherAutoConfiguration
Spring Boot 3.x —— 改为读取:
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
内容格式是纯文本,一行一个全限定名:
com.example.demo.DemoAutoConfiguration
com.example.demo.OtherAutoConfiguration
Spring Boot 3 已完全移除对 注册自动配置的支持。如果你按 2.x 的教程写 Starter 放到 3.x 项目里,现象是依赖引了、配置写了,但 Bean 一个都没注册,而且不报任何错,排查起来非常折磨人。
1.4 完整时序串起来
应用启动
↓
SpringApplication.run()
↓
刷新上下文 refreshContext()
↓
ConfigurationClassPostProcessor 解析配置类
↓
发现 @EnableAutoConfiguration → @Import(AutoConfigurationImportSelector)
↓
selectImports() 加载候选配置类清单
↓
去重 → 排序 → @ConditionalOnXxx 条件过滤
↓
剩下的配置类注册为 BeanDefinition
↓
实例化 Bean,注入到容器
二、条件注解:自动装配的"开关"
条件注解决定了"什么情况下这个自动配置才生效"。Spring Boot 在 org.springframework.boot.autoconfigure.condition 包下提供了十几 个。
2.1 常用条件注解速查
| 注解 | 生效条件 | 典型用途 |
|---|---|---|
@ConditionalOnClass |
classpath 中存在指定类 | 引入了某依赖才装配 |
@ConditionalOnMissingClass |
classpath 中不存在指定类 | 缺省兜底方案 |
@ConditionalOnBean |
容器中存在指定 Bean | 依赖其他 Bean 才装配 |
@ConditionalOnMissingBean |
容器中不存在指定 Bean | 允许用户自定义覆盖默认 |
@ConditionalOnProperty |
配置项满足条件 | 通过配置开关控制 |
@ConditionalOnWebApplication |
当前是 Web 应用 | Web 环境专属配置 |
@ConditionalOnNotWebApplication |
当前非 Web 应用 | 非 Web 环境配置 |
@ConditionalOnExpression |
SpEL 表达式为 true | 复杂组合条件 |
@ConditionalOnJava |
指定 Java 版本 | 版本兼容处理 |
2.2 @ConditionalOnMissingBean 为什么最重要
看一眼 Spring Boot 自带的 RedisAutoConfiguration 源码片段:
@Bean
@ConditionalOnMissingBean(name = "redisTemplate")
@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
return template;
}
@ConditionalOnMissingBean(name = "redisTemplate") 的含义是:只有当用户自己没定义 redisTemplate 时,我才用默认的。
这就实现了自动装配最重要的一条设计原则 —— 约定优于配置,但允许用户覆盖。你只要在配置类里自己写个 @Bean 叫 redisTemplate,默认的自动失效,全程不需要任何 exclude 操作。
写 Starter 时,任何对外提供的默认 Bean 都应该加 。不加的话,使用者想自定义就只能去排除整个自动配置类,非常难用。
三、动手写一个 Starter
3.1 场景与项目结构
假设我们要封装一个"短信发送"组件,团队里多个微服务都要用。目标是:其他项目引入依赖 + 配置账号,就能直接注入 SmsService 使用。
命名规范很重要,官方约定:
- 官方 starter:
spring-boot-starter-xxx - 第三方 starter:
xxx-spring-boot-starter
我们建两个模块:
sms-spring-boot-starter/ # 空壳,只做依赖聚合
└── pom.xml
sms-spring-boot-autoconfigure/ # 真正干活的地方
├── pom.xml
└── src/main/java/com/example/sms/
├── SmsProperties.java # 配置属性
├── SmsService.java # 核心服务
├── SmsAutoConfiguration.java # 自动配置类
└── resources/META-INF/
├── spring/.../AutoConfiguration.imports (Boot 3)
└── spring.factories (Boot 2)
为什么要拆成两个模块?因为 autoconfigure 模块承载全部逻辑,starter 模块只是个"依赖清单"。这样使用者可以只引 starter 拿到全量能力,也可以只引 autoconfigure 自己控制版本。Spring Boot 官方所有 starter 都是这个结构。
3.2 配置属性类
@ConfigurationProperties(prefix = "sms")
@Data
public class SmsProperties {
/** 是否启用,默认关闭 */
private boolean enabled = false;
/** 接入密钥 */
private String accessKey;
/** 密钥 */
private String secretKey;
/** 短信签名 */
private String signName;
/** 连接超时(毫秒) */
private int connectTimeout = 3000;
/** 最大重试次数 */
private int maxRetry = 2;
}
@ConfigurationProperties(prefix = "sms") 把配置文件里的 sms.* 一次性绑定到这个对象上,比逐个 @Value 清爽得多。
3.3 核心服务类
@Slf4j
public class SmsService {
private final SmsProperties properties;
public SmsService(SmsProperties properties) {
this.properties = properties;
}
public boolean send(String phone, String templateCode, Map<String, String> params) {
if (!properties.isEnabled()) {
log.warn("短信功能未启用,跳过发送:{}", phone);
return false;
}
int retry = 0;
while (retry <= properties.getMaxRetry()) {
try {
log.info("发送短信 phone={}, template={}, sign={}",
phone, templateCode, properties.getSignName());
// 这里调用真实服务商 SDK
return true;
} catch (Exception e) {
retry++;
log.error("短信发送失败,第 {} 次重试, phone={}", retry, phone, e);
}
}
return false;
}
}
3.4 自动配置类
@Slf4j
@Configuration
@EnableConfigurationProperties(SmsProperties.class)
@ConditionalOnProperty(prefix = "sms", name = "enabled", havingValue = "true")
public class SmsAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public SmsService smsService(SmsProperties properties) {
log.info("初始化 SmsService,签名:{}", properties.getSignName());
return new SmsService(properties);
}
}
三个注解各有用意:
@EnableConfigurationProperties(SmsProperties.class)—— 把SmsProperties注册进容器,否则注入不进来@ConditionalOnProperty(prefix = "sms", name = "enabled", havingValue = "true")—— 总开关,没配置sms.enabled=true的话整个自动配置都不加载@ConditionalOnMissingBean—— 允许用户自定义SmsService覆盖默认实现
3.5 注册自动配置
这一步最容易忘,也最坑。
Spring Boot 3.x,在 src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 写入:
com.example.sms.SmsAutoConfiguration
Spring Boot 2.x,在 src/main/resources/META-INF/spring.factories 写入:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.sms.SmsAutoConfiguration
如果想同时兼容两个大版本,两个文件都写是安全的 —— 3.x 只认 imports 文件,2.x 只认 spring.factories,互不干扰。
3.6 autoconfigure 模块的 pom
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- 配置处理器:生成配置元数据,让 IDE 能自动提示 sms.xxx -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
3.7 starter 模块的 pom
starter 模块不写任何 Java 代码,只有一个 pom:
<dependencies>
<dependency>
<groupId>com.example</groupId>
<artifactId>sms-spring-boot-autoconfigure</artifactId>
<version>1.0.0</version>
</dependency>
</dependencies>
starter 的 pom 里不要引入 、 这类重依赖。Starter 应该保持最小依赖,否则会污染使用者的 classpath,甚至引发版本冲突。
3.8 使用方怎么用
第一步,引依赖:
<dependency>
<groupId>com.example</groupId>
<artifactId>sms-spring-boot-starter</artifactId>
<version>1.0.0</version>
</dependency>
第二步,加配置:
sms:
enabled: true
accessKey: your-access-key
secretKey: your-secret-key
signName: 神经蛙科技
connect-timeout: 5000
max-retry: 3
第三步,直接注入:
@Service
@RequiredArgsConstructor
public class UserService {
private final SmsService smsService;
public void sendVerifyCode(String phone, String code) {
smsService.send(phone, "SMS_123456", Map.of("code", code));
}
}
全程没有一行 new,没有 @Import,没有 XML —— 这就是自动装配的价值。
四、微服务常用 Starter 与整合要点
4.1 常用 Starter 清单
| Starter | 作用 | 备注 |
|---|---|---|
spring-boot-starter-web |
Web MVC + 内嵌 Tomcat | 最常用 |
spring-boot-starter-validation |
参数校验(Hibernate Validator) | Boot 2.3 后需单独引入 |
spring-boot-starter-data-redis |
Redis 操作 | 默认 Lettuce 客户端 |
spring-boot-starter-aop |
切面编程 | 自定义注解必备 |
spring-boot-starter-actuator |
健康检查与监控端点 | 生产必备 |
spring-cloud-starter-gateway |
API 网关 | 基于 WebFlux,不能与 web 共存 |
spring-cloud-starter-openfeign |
声明式 HTTP 调用 | 需配合注册中心 |
spring-cloud-starter-alibaba-nacos-discovery |
服务注册与发现 | 阿里系 |
4.2 配置热更新
配置中心改了配置,应用不重启就生效,靠的是 @RefreshScope:
@RefreshScope
@Component
public class DynamicConfig {
@Value("${business.max-retry:3}")
private int maxRetry;
public int getMaxRetry() {
return maxRetry;
}
}
原理是 @RefreshScope 把 Bean 的作用域改成了 refresh,配置变更事件触发时,Spring Cloud 会把这个 Bean 销毁并重建。
有个坑:加了它之后,该 Bean 是懒加载代理的,每次调用都会走一次代理解析。高频调用的热路径上慎用。另外它只对 和 有效,对构造函数里已经算好的静态字段无效。
五、踩坑记录
5.1 自动配置类不生效
排查顺序:
- 确认
sms.enabled=true这类开关条件是否满足 - 确认 imports / spring.factories 文件路径完全正确(尤其是 Boot 3 那一长串目录名)
- 确认文件被打包进了 jar —— 用
jar tf target/xxx.jar | grep imports检查 - 开启 debug 日志看自动装配报告:
debug: true,启动日志会打印哪些配置生效、哪些被淘汰
5.2 包扫描路径不一致
自动配置类不受 @ComponentScan 路径限制,因为它走的是 @Import 机制。但如果你在自动配置类里用了 @ComponentScan 去扫别的包,就很容易扫漏。推荐做法是显式声明 Bean,不要用扫描。
5.3 循环依赖
两个自动配置类互相 @ConditionalOnBean 依赖对方时,会形成死锁。解决方式是用 @AutoConfigureAfter / @AutoConfigureBefore 显式声明装配顺序:
@Configuration
@AutoConfigureAfter(RedisAutoConfiguration.class)
public class SmsAutoConfiguration {
// 保证 Redis 先装配完,Sms 再装配
}
5.4 配置提示不出现
IDE 里写 sms. 没有补全,检查两件事:
- 是否引入了
spring-boot-configuration-processor target/classes/META-INF/spring-configuration-metadata.json是否生成
5.5 Spring Boot 3 迁移要点
| 变化 | Boot 2 | Boot 3 |
|---|---|---|
| JDK 基线 | 8+ | 17+ |
| Java EE → Jakarta | javax.* |
jakarta.* |
| 自动配置注册 | spring.factories |
AutoConfiguration.imports |
| 配置属性绑定 | 宽松绑定 | 更严格,部分写法废弃 |
其中 javax.* → jakarta.* 是最痛的一条,所有 Servlet 相关的 import 都要改。
总结:自动装配的本质是"全量候选 + 条件过滤"。写 Starter 记住三条就够:一、对外提供的 Bean 一律加 ;二、加一个 总开关便于排障;三、Boot 2 和 3 的配置文件都要写。理解了这套机制,再看任何 starter 的源码都不会发怵。