Spring Boot 自动配置原理与手写一个可用的 Starter
从 @EnableAutoConfiguration 到条件装配的加载顺序,再手写一个能用的 Starter。搞懂自动配置,才敢改别人的脚手架。
Spring Boot 最核心的能力是"依赖了就自动配好"。你加个 spring-boot-starter-data-redis,什么都没写,RedisTemplate 就能注入了。这背后不是魔法,是一套设计得相当克制的加载机制。
这篇从注解拆到源码,最后手写一个能用的 Starter。
一、@SpringBootApplication 拆开是什么
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
这个注解是三个注解的组合:
@SpringBootConfiguration // 本质是 @Configuration,标记配置类
@EnableAutoConfiguration // 开启自动配置,核心
@ComponentScan // 扫描当前包及子包的 @Component
@ComponentScan 的默认范围是启动类所在包及其子包。这就是为什么"我加了 @Service 但注入失败"最常见的原因是包放错位置——启动类在 com.example.app,服务类在 com.example.service,扫不到。
@EnableAutoConfiguration 才是自动配置的入口:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration { }
关键在 @Import(AutoConfigurationImportSelector.class)——它把一个 ImportSelector 注册进容器,这个 selector 返回的类名列表就是所有要生效的自动配置类。
二、自动配置的加载流程
AutoConfigurationImportSelector.selectImports() 做的事,简化后是这样:
// 1. 找出所有候选配置类
List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
// 2. 去重
configurations = removeDuplicates(configurations);
// 3. 按条件过滤(这一步才是精华)
configurations = filter(configurations, autoConfigurationMetadata);
// 4. 排序(@AutoConfigureBefore / @AutoConfigureAfter)
configurations = sortAutoConfigurations(configurations, ...);
// 5. 排除用户显式 exclude 的
return configurations.toArray(new String[0]);
第 1 步读的是 classpath 下所有 jar 里的清单文件。Spring Boot 2.7 之前是:
META-INF/spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.FooAutoConfiguration,\
com.example.BarAutoConfiguration
Spring Boot 2.7 之后改用新文件:
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
com.example.FooAutoConfiguration
com.example.BarAutoConfiguration
这个变化值得特别注意:2.7 开始两种都支持(spring.factories 标记为废弃),3.0 之后新文件是唯一方式。如果你维护的 Starter 还在用 spring.factories 注册自动配置,升级到 Boot 3 后会静默失效——不报错,只是不生效。
三、条件注解:什么时候该生效
自动配置类全部加载进容器是不可能的——DataSourceAutoConfiguration 在没有数据库驱动时就不该生效。控制这件事的是条件注解族:
| 注解 | 判断依据 | 典型用法 |
|---|---|---|
@ConditionalOnClass |
classpath 有某个类 | 有 DataSource 才配数据源 |
@ConditionalOnMissingBean |
容器里没有某个 Bean | 用户没自定义时才用默认的 |
@ConditionalOnProperty |
配置项存在且等于指定值 | 开关某个功能 |
@ConditionalOnBean |
容器里已有某个 Bean | 依赖前置 Bean |
@ConditionalOnWebApplication |
是 Web 应用 | 排除非 Web 场景 |
@ConditionalOnResource |
存在某个资源文件 | 配置文件驱动的功能 |
@ConditionalOnExpression |
SpEL 表达式为真 | 复杂组合条件 |
抓一个真实的例子看,DataSourceAutoConfiguration:
@AutoConfiguration
@ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class })
@EnableConfigurationProperties(DataSourceProperties.class)
@Import({ DataSourcePoolMetadataProvidersConfiguration.class,
DataSourceCheckpointRestoreConfiguration.class })
public class DataSourceAutoConfiguration {
@Configuration(proxyBeanMethods = false)
@Conditional(PooledDataSourceCondition.class)
@ConditionalOnMissingBean({ DataSource.class, XADataSource.class })
@Import({ DataSourceConfiguration.Hikari.class, ... })
protected static class PooledDataSourceConfiguration { }
}
读法就是一句话:classpath 里有 DataSource 类,并且用户没有自己定义 DataSource Bean,才用内置的连接池配置。
@ConditionalOnMissingBean 是"约定优于配置"能落地的关键——它保证了用户的定义永远优先于框架的默认。这也是为什么你加一个 @Bean DataSource 就能整个接管数据源配置。
四、手写一个 Starter
假设我们要做一个"审计日志"组件:只要方法加了 @AuditLog,就自动把参数、耗时、结果记下来。
4.1 项目结构
推荐拆成两个模块:
audit-spring-boot-starter/
├── pom.xml <- 只做依赖聚合
└── audit-spring-boot-autoconfigure/
├── pom.xml
└── src/main/
├── java/com/example/audit/
│ ├── AuditLog.java <- 注解
│ ├── AuditProperties.java <- 配置项
│ ├── AuditAspect.java <- 切面
│ └── AuditAutoConfiguration.java <- 自动配置
└── resources/META-INF/spring/
└── org.springframework.boot.autoconfigure.AutoConfiguration.imports
为什么拆两个模块:Starter 只放依赖,autoconfigure 放代码。这样用户可以只依赖 autoconfigure(比如自己组装依赖版本),也是 Spring Boot 官方 Starter 的统一做法。
4.2 注解
package com.example.audit;
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AuditLog {
/** 操作描述 */
String value() default "";
/** 是否记录返回值 */
boolean recordResult() default false;
}
4.3 配置属性
package com.example.audit;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
@ConfigurationProperties(prefix = "audit")
public class AuditProperties {
/** 是否启用 */
private boolean enabled = true;
/** 超过这个耗时才记录,默认全记 */
private Duration slowThreshold = Duration.ZERO;
/** 不记录参数的字段名(脱敏) */
private List<String> excludeFields = new ArrayList<>();
/** 日志前缀 */
private String prefix = "[AUDIT]";
// getter / setter 省略
}
关键点:配置类只做数据载体,不加 @Component。@ConfigurationProperties 会被 @EnableConfigurationProperties 注册,加了 @Component 反而会导致在非 Boot 环境重复注册。
4.4 切面
package com.example.audit;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Aspect
public class AuditAspect {
private static final Logger log = LoggerFactory.getLogger(AuditAspect.class);
private final AuditProperties properties;
public AuditAspect(AuditProperties properties) {
this.properties = properties;
}
@Around("@annotation(auditLog)")
public Object around(ProceedingJoinPoint pjp, AuditLog auditLog) throws Throwable {
long start = System.currentTimeMillis();
MethodSignature sig = (MethodSignature) pjp.getSignature();
String method = sig.getDeclaringType().getSimpleName() + "#" + sig.getName();
String desc = auditLog.value().isEmpty() ? method : auditLog.value();
try {
Object result = pjp.proceed();
long cost = System.currentTimeMillis() - start;
if (cost >= properties.getSlowThreshold().toMillis()) {
log.info("{} {} 成功 cost={}ms args={}", properties.getPrefix(), desc, cost,
auditLog.recordResult() ? result : maskArgs(pjp.getArgs()));
}
return result;
} catch (Throwable e) {
long cost = System.currentTimeMillis() - start;
log.warn("{} {} 失败 cost={}ms err={}", properties.getPrefix(), desc, cost, e.getMessage());
throw e;
}
}
private String maskArgs(Object[] args) {
// 简化实现:实际项目里按字段名遍历脱敏
return args == null ? "" : java.util.Arrays.toString(args);
}
}
注意这里用构造器注入,不加 @Component——Bean 的注册交给自动配置类,这样才受 @ConditionalOnMissingBean 控制。
4.5 自动配置类
package com.example.audit;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
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;
import org.aspectj.lang.annotation.Aspect;
@AutoConfiguration
@ConditionalOnClass({ Aspect.class })
@ConditionalOnProperty(prefix = "audit", name = "enabled", havingValue = "true", matchIfMissing = true)
@EnableConfigurationProperties(AuditProperties.class)
public class AuditAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public AuditAspect auditAspect(AuditProperties properties) {
return new AuditAspect(properties);
}
}
四个注解各司其职:
@AutoConfiguration—— Boot 2.7+ 的专用注解,本质是@Configuration(proxyBeanMethods = false),额外支持before/after排序@ConditionalOnClass—— 没有 AspectJ 就不启用(防止用户没引 aop 依赖时启动报错)@ConditionalOnProperty—— 支持audit.enabled=false一键关闭@ConditionalOnMissingBean—— 用户自己定义了AuditAspect就用他的
proxyBeanMethods = false 值得单独说。设为 false 后,@Bean 方法之间不能直接调用来获取单例(会每次 new 一个),但省掉了 CGLIB 子类生成,启动更快、内存更省。对于"Bean 之间没有互相引用"的自动配置类,这是推荐值。
4.6 注册自动配置
src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports:
com.example.audit.AuditAutoConfiguration
一行,全限定类名,不要多余空行和注释(解析器容错有限)。
4.7 依赖打包
autoconfigure 模块的 pom 里,自动配置类需要的依赖用 optional:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
optional 的意义:编译需要它,但不强制传递给使用者。配合 @ConditionalOnClass,用户没引 aspectj 时自动配置不生效,而不是启动直接 NoClassDefFoundError。这是 Starter 健壮性的关键设计。
spring-boot-configuration-processor 会在编译期生成 META-INF/spring-configuration-metadata.json,IDE 里就有配置提示了。
4.8 使用
<dependency>
<groupId>com.example</groupId>
<artifactId>audit-spring-boot-starter</artifactId>
<version>1.0.0</version>
</dependency>
audit:
enabled: true
slow-threshold: 500ms
exclude-fields:
- password
- idCard
@AuditLog("创建订单")
public void createOrder(Order order) { ... }
零配置即可工作,audit.enabled 一键关闭。
五、排查自动配置为什么没生效
5.1 打开条件评估报告
java -jar app.jar --debug
或者配置:
debug: true
启动日志里会出现 CONDITIONS EVALUATION REPORT,分成三块:
Positive matches: <- 生效了,以及为什么
DataSourceAutoConfiguration matched:
- @ConditionalOnClass found required classes 'javax.sql.DataSource'...
Negative matches: <- 没生效,以及为什么
RedisAutoConfiguration did not match:
- @ConditionalOnClass did not find required class 'redis.clients.jedis.Jedis'
Exclusions: <- 被排除的
"Negative matches" 是排查的核心,它直接告诉你缺了哪个类、哪个属性不对。
5.2 用 Actuator 看运行时状态
management:
endpoints:
web:
exposure:
include: beans,conditions,configprops
curl -s localhost:8080/actuator/conditions | jq '.contexts.application.negativeMatches'
curl -s localhost:8080/actuator/configprops | jq '.contexts.application.beans[].prefix'
configprops 能看到所有 @ConfigurationProperties 的实际绑定值,排查"配置写了没生效"特别有效——如果这里显示的是默认值,说明前缀写错了或者没有 @EnableConfigurationProperties。
5.3 几个高频错误
| 现象 | 原因 |
|---|---|
| 自动配置类完全没加载 | AutoConfiguration.imports 路径写错(注意是 META-INF/spring/ 子目录) |
报 BeanDefinitionOverrideException |
自动配置类没加 @ConditionalOnMissingBean,与用户 Bean 冲突 |
NoClassDefFoundError |
依赖没标 optional,或漏了 @ConditionalOnClass |
| 配置项不生效 | 前缀写错、属性名没有 kebab-case、缺 @EnableConfigurationProperties |
| Boot 3 升级后失效 | 还在用 spring.factories,需迁到 AutoConfiguration.imports |
六、Starter 的命名与分层的约定
Spring Boot 官方的命名规则值得遵循:
- 第三方 Starter:
xxx-spring-boot-starter(如mybatis-spring-boot-starter) - 官方 Starter:
spring-boot-starter-xxx(如spring-boot-starter-web) - 不要用
spring-boot-starter-开头做自己的包,会和官方混淆
依赖层次上有个经验值:Starter 里的传递依赖要尽可能少。一个只做审计的 Starter 不该顺带引入整个 spring-boot-starter-web,那会让不支持 Web 的项目无法使用。用 optional + 条件注解把依赖关系变成"可选的增强",而不是"强制的前置"。
小结
自动配置的机制可以用一句话概括:"把一堆候选配置类列成清单,逐个用条件注解判断该不该生效,用户定义永远优先"。
理解这个模型之后,那些曾经神秘的行为就都有解释了:
- 为什么加了依赖就有 Bean —— 清单里有对应的自动配置类
- 为什么自己定义的 Bean 能覆盖默认 ——
@ConditionalOnMissingBean - 为什么某些功能"莫名不生效" —— 某个条件不满足,
--debug一看便知
写 Starter 时最容易漏的三件事,值得贴在显示器上:清单文件放在 META-INF/spring/ 子目录下、依赖标 optional、自动配置类加 @ConditionalOnMissingBean。
正在加载评论…