Spring Boot 自定义 Starter 与自动装配原理深度剖析

用过 Spring Boot 的人都知道”引入依赖就能用”,但一旦要自己封装一个公共组件给团队复用,就必须搞懂自动装配。本文从源码层面拆开 @SpringBootApplication,然后带你手写一个能放进生产环境的 Starter。

一、自动装配到底做了什么

1.1 从 @SpringBootApplication 拆起

每个 Spring Boot 项目的启动类上都挂着这么一个注解:

1
2
3
4
5
6
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}

它其实是一个”三合一”的组合注解,扒开源码能看到:

1
2
3
4
5
6
7
8
@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 引入了一个选择器:

1
2
3
4
5
6
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
// ...
}

AutoConfigurationImportSelector 实现了 DeferredImportSelector,它最核心的方法是 selectImports(),简化后逻辑如下:

1
2
3
4
5
6
7
8
9
@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return NO_IMPORTS;
}
// 1. 加载所有候选自动配置类
AutoConfigurationEntry entry = getAutoConfigurationEntry(annotationMetadata);
return StringUtils.toStringArray(entry.getConfigurations());
}

getAutoConfigurationEntry() 内部做了四件事,这是理解自动装配的关键:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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);
}

关键点:自动装配不是”无脑全部加载”,而是”先全量加载候选,再按条件逐个淘汰”。这也是为什么自动配置类里满屏都是 @ConditionalOnXxx 注解。

1.3 候选配置从哪里读:Spring Boot 2 与 3 的差异

这是面试高频、也最容易踩坑的地方。两个大版本的配置文件位置和格式都不一样。

Spring Boot 2.x —— 读取所有 jar 包里的:

1
META-INF/spring.factories

内容格式是 properties:

1
2
3
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.demo.DemoAutoConfiguration,\
com.example.demo.OtherAutoConfiguration

Spring Boot 3.x —— 改为读取:

1
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

内容格式是纯文本,一行一个全限定名:

1
2
com.example.demo.DemoAutoConfiguration
com.example.demo.OtherAutoConfiguration

Spring Boot 3 已完全移除spring.factories 注册自动配置的支持。如果你按 2.x 的教程写 Starter 放到 3.x 项目里,现象是依赖引了、配置写了,但 Bean 一个都没注册,而且不报任何错,排查起来非常折磨人。

1.4 完整时序串起来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
应用启动

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 源码片段:

1
2
3
4
5
6
7
8
@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 时,我才用默认的

这就实现了自动装配最重要的一条设计原则 —— 约定优于配置,但允许用户覆盖。你只要在配置类里自己写个 @BeanredisTemplate,默认的自动失效,全程不需要任何 exclude 操作。

写 Starter 时,任何对外提供的默认 Bean 都应该加 @ConditionalOnMissingBean。不加的话,使用者想自定义就只能去排除整个自动配置类,非常难用。

三、动手写一个 Starter

3.1 场景与项目结构

假设我们要封装一个”短信发送”组件,团队里多个微服务都要用。目标是:其他项目引入依赖 + 配置账号,就能直接注入 SmsService 使用。

命名规范很重要,官方约定:

  • 官方 starter:spring-boot-starter-xxx
  • 第三方 starter:xxx-spring-boot-starter

我们建两个模块:

1
2
3
4
5
6
7
8
9
10
11
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 配置属性类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@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 核心服务类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@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 自动配置类

1
2
3
4
5
6
7
8
9
10
11
12
13
@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 写入:

1
com.example.sms.SmsAutoConfiguration

Spring Boot 2.x,在 src/main/resources/META-INF/spring.factories 写入:

1
2
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.sms.SmsAutoConfiguration

如果想同时兼容两个大版本,两个文件都写是安全的 —— 3.x 只认 imports 文件,2.x 只认 spring.factories,互不干扰。

3.6 autoconfigure 模块的 pom

1
2
3
4
5
6
7
8
9
10
11
12
13
<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:

1
2
3
4
5
6
7
<dependencies>
<dependency>
<groupId>com.example</groupId>
<artifactId>sms-spring-boot-autoconfigure</artifactId>
<version>1.0.0</version>
</dependency>
</dependencies>

starter 的 pom 里不要引入 spring-boot-starter-webspring-boot-starter-data-jpa 这类重依赖。Starter 应该保持最小依赖,否则会污染使用者的 classpath,甚至引发版本冲突。

3.8 使用方怎么用

第一步,引依赖:

1
2
3
4
5
<dependency>
<groupId>com.example</groupId>
<artifactId>sms-spring-boot-starter</artifactId>
<version>1.0.0</version>
</dependency>

第二步,加配置:

1
2
3
4
5
6
7
sms:
enabled: true
accessKey: your-access-key
secretKey: your-secret-key
signName: 神经蛙科技
connect-timeout: 5000
max-retry: 3

第三步,直接注入:

1
2
3
4
5
6
7
8
9
10
@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

1
2
3
4
5
6
7
8
9
10
11
@RefreshScope
@Component
public class DynamicConfig {

@Value("${business.max-retry:3}")
private int maxRetry;

public int getMaxRetry() {
return maxRetry;
}
}

原理是 @RefreshScope 把 Bean 的作用域改成了 refresh,配置变更事件触发时,Spring Cloud 会把这个 Bean 销毁并重建。

@RefreshScope 有个坑:加了它之后,该 Bean 是懒加载代理的,每次调用都会走一次代理解析。高频调用的热路径上慎用。另外它只对 @Value@ConfigurationProperties 有效,对构造函数里已经算好的静态字段无效。

五、踩坑记录

5.1 自动配置类不生效

排查顺序:

  1. 确认 sms.enabled=true 这类开关条件是否满足
  2. 确认 imports / spring.factories 文件路径完全正确(尤其是 Boot 3 那一长串目录名)
  3. 确认文件被打包进了 jar —— 用 jar tf target/xxx.jar | grep imports 检查
  4. 开启 debug 日志看自动装配报告:debug: true,启动日志会打印哪些配置生效、哪些被淘汰

5.2 包扫描路径不一致

自动配置类不受 @ComponentScan 路径限制,因为它走的是 @Import 机制。但如果你在自动配置类里用了 @ComponentScan 去扫别的包,就很容易扫漏。推荐做法是显式声明 Bean,不要用扫描。

5.3 循环依赖

两个自动配置类互相 @ConditionalOnBean 依赖对方时,会形成死锁。解决方式是用 @AutoConfigureAfter / @AutoConfigureBefore 显式声明装配顺序:

1
2
3
4
5
@Configuration
@AutoConfigureAfter(RedisAutoConfiguration.class)
public class SmsAutoConfiguration {
// 保证 Redis 先装配完,Sms 再装配
}

5.4 配置提示不出现

IDE 里写 sms. 没有补全,检查两件事:

  1. 是否引入了 spring-boot-configuration-processor
  2. 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 一律加 @ConditionalOnMissingBean;二、加一个 enabled 总开关便于排障;三、Boot 2 和 3 的配置文件都要写。理解了这套机制,再看任何 starter 的源码都不会发怵。