Spring Boot自动配置原理与自定义Starter
引言
先讲一个我亲身经历的线上事故。
三年前,我负责的一个订单服务在凌晨两点突然出现大量超时告警。排查发现,一个刚入职的同事为了让日志链路追踪生效,在 pom.xml 里引入了公司内部的 trace-log-starter,但忘了在 application.yml 里配置 trace.sample-rate。结果这个 Starter 内部默认采样率是 100%,每个请求都往远端日志服务打全量 Trace,把日志服务打挂了,进而拖垮了整个调用链路。
问题不在代码,而在于——这个 Starter 的默认配置和行为,对使用者完全是个黑盒。他不知道自己引入了一个"默认全量采样"的东西。
这件事让我重新思考:Spring Boot 的自动配置(Auto-Configuration)和 Starter 机制,是 Java 生态里最成功的"约定优于配置"实践,也是最容易被误用的地方。绝大多数人会用 @SpringBootApplication,能写 @Configuration,但真正理解"为什么我什么都没配,DataSource 就自动有了"的人,其实不多。
这篇文章,我想把自动配置这件事从源码层面讲透,然后手把手带你写一个生产级可用的 Starter。目标读者是已经写了 3-5 年 Spring Boot、但没深挖过自动配置源码的工程师。
核心概念:从"餐厅后厨"说起
生活类比:自动配置就像一家"预制菜中央厨房"
传统 Spring 开发,就像你开一家餐厅,从买菜、洗菜、切菜、调酱汁全部自己来。每开一家新店,都得把这些流程重走一遍,哪怕两家店卖的是同样的菜。
Spring Boot 的自动配置,相当于引入了一个中央厨房:
- 你只需要告诉中央厨房"我要做川菜"(引入
spring-boot-starter-web);
- 中央厨房会预判你需要什么(Tomcat、Jackson、DispatcherServlet),把这些"半成品"送到你店里;
- 如果你自己店里已经有某个半成品(比如你自己配了一个
ObjectMapper),中央厨房送来的那份就自动作废——这叫@ConditionalOnMissingBean;
- 如果你明确说"我不要某样东西"(
@ConditionalOnProperty),它也不会硬塞给你。
这个"预判"的能力,靠的是 Spring Boot 在启动时扫描所有 jar 包里的一个约定文件,然后根据一堆 @Conditional 注解决定"这批货要不要送、送哪几样"。
技术定义
Spring Boot 自动配置的本质是:
- 启动时,通过
@EnableAutoConfiguration触发AutoConfigurationImportSelector; - 该 Selector 从所有依赖 jar 的
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports(Spring Boot 2.7+)文件中,读取出候选配置类全限定名列表; - 经过去重、排除(
@SpringBootApplication(exclude=...))、条件过滤后,注册为普通的@Configuration类; - 这些配置类内部通过
@Bean+@ConditionalOnXxx决定每个 Bean 是否真正注册。
一句话:自动配置 = 一堆带条件的 @Configuration 类的批量、按需加载机制。
源码深度剖析:一次启动到底发生了什么
关键入口:@EnableAutoConfiguration
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
Class<?>[] exclude() default {};
String[] excludeName() default {};
}注意 @Import(AutoConfigurationImportSelector.class)——这是整个机制的总开关。它把 AutoConfigurationImportSelector 注册为一个 ImportSelector,在配置类解析阶段被回调。
核心方法:getAutoConfigurationEntry
AutoConfigurationImportSelector 继承自 AutoConfigurationImportSelector → ImportSelector。真正干活的是 getAutoConfigurationEntry:
protected AutoConfigurationEntry getAutoConfigurationEntry(
AutoConfigurationMetadata autoConfigurationMetadata,
AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return EMPTY_ENTRY;
}
// 1. 读取 @EnableAutoConfiguration 上的 exclude / excludeName 属性
AnnotationAttributes attributes = getAttributes(annotationMetadata);
// 2. 从所有 jar 的 imports 文件里加载候选配置类
List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
// 3. 去重(多个 starter 可能引入同一个配置类)
configurations = removeDuplicates(configurations);
// 4. 去掉被 exclude 的
Set<String> exclusions = getExclusions(annotationMetadata, attributes);
checkExcludedClasses(configurations, exclusions);
configurations.removeAll(exclusions);
// 5. 关键:用 AutoConfigurationImportFilter 做粗粒度过滤(OnBeanCondition 等的快速版)
configurations = filter(configurations, autoConfigurationMetadata);
// 6. 广播事件,允许扩展
fireAutoConfigurationImportEvents(configurations, exclusions);
return new AutoConfigurationEntry(configurations, exclusions);
}这里面最容易被忽略的是第 5 步 filter。Spring Boot 用 AutoConfigurationImportFilter(实现类有 OnBeanCondition、OnClassCondition、OnWebApplicationCondition)做了一次"预筛"。这一步不加载类,只读取配置类在 imports 文件旁生成的 .imports 元数据(编译期通过注解处理器生成),所以非常快。
为什么要有这一步? 因为一个大型项目可能引入几百个候选自动配置类,如果每个都真正加载字节码、解析注解,启动会非常慢。预筛把明显不满足条件的(比如 @ConditionalOnClass 里要求的类不存在)直接踢掉,剩下的才真正加载。
条件注解的判定时机
真正的条件判定发生在 ConfigurationClassParser 解析配置类时。以 @ConditionalOnClass 为例:
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(OnClassCondition.class)
public @interface ConditionalOnClass {
Class<?>[] value() default {};
String[] name() default {};
}OnClassCondition 继承 SpringBootCondition,其 getMatchOutcome 里用 ClassNameFilter.MISSING 判断类是否在 classpath。注意它优先用 name 字符串而不是 value 的 Class 对象——因为用 Class 会在注解解析时触发类加载,如果类不存在会直接抛 NoClassDefFoundError。这是 Spring Boot 里非常经典的一个设计细节。
整体流程
一个容易被忽视的细节:自动配置的加载顺序
自动配置类之间是有顺序的,靠 @AutoConfigureBefore / @AutoConfigureAfter / @AutoConfigureOrder 控制。比如 DataSourceAutoConfiguration 必须在 MybatisAutoConfiguration 之前,因为后者依赖前者产出的 DataSource。
Spring Boot 在 AutoConfigurationSorter 里用拓扑排序处理这些依赖。如果你自己写 Starter,涉及多配置类时一定要显式声明顺序,否则在某些 classpath 组合下会出现"Bean 找不到"的诡异问题。
实战代码:三个完整示例
示例一:手写一个最小可用的 Starter(限流组件)
我们做一个 rate-limit-spring-boot-starter,提供基于令牌桶的接口限流,自动配置限流器并暴露 @RateLimit 注解。
步骤 1:Starter 的模块结构
生产级 Starter 通常拆成两个模块:
rate-limit-spring-boot-autoconfigure:放自动配置逻辑;
rate-limit-spring-boot-starter:只放依赖,把 autoconfigure 和第三方库都引进来。
这是 Spring 官方推荐的"autoconfigure + starter"分离模式,好处是使用者可以只引 autoconfigure,自己控制依赖版本。
步骤 2:配置属性类
package com.example.ratelimit.autoconfigure;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* 限流配置属性。
* 前缀为 rate-limit,对应 application.yml 中的 rate-limit.*
* 使用构造器绑定(Spring Boot 2.2+ 推荐),保证不可变性和线程安全。
*/
@ConfigurationProperties(prefix = "rate-limit")
public class RateLimitProperties {
/** 是否启用限流,默认开启。对应 rate-limit.enabled */
private boolean enabled = true;
/** 默认每秒允许的请求数(令牌桶容量) */
private int defaultPermitsPerSecond = 100;
/** 获取令牌的超时时间(毫秒),超时则拒绝请求 */
private long timeoutMillis = 0;
public boolean isEnabled() { return enabled; }
public void setEnabled(boolean enabled) { this.enabled = enabled; }
public int getDefaultPermitsPerSecond() { return defaultPermitsPerSecond; }
public void setDefaultPermitsPerSecond(int v) { this.defaultPermitsPerSecond = v; }
public long getTimeoutMillis() { return timeoutMillis; }
public void setTimeoutMillis(long timeoutMillis) { this.timeoutMillis = timeoutMillis; }
}步骤 3:核心限流器
package com.example.ratelimit.autoconfigure;
import com.google.common.util.concurrent.RateLimiter;
import java.util.concurrent.ConcurrentHashMap;
/**
* 基于 Guava RateLimiter 的限流器。
* 每个资源名(key)维护一个独立的令牌桶。
*/
public class RateLimitExecutor {
private final RateLimitProperties properties;
private final ConcurrentHashMap<String, RateLimiter> limiters = new ConcurrentHashMap<>();
public RateLimitExecutor(RateLimitProperties properties) {
this.properties = properties;
}
/**
* 尝试获取令牌。
* @param key 资源标识,通常是 "类名#方法名"
* @return true 表示放行,false 表示被限流
*/
public boolean tryAcquire(String key) {
RateLimiter limiter = limiters.computeIfAbsent(key,
k -> RateLimiter.create(properties.getDefaultPermitsPerSecond()));
// timeoutMillis=0 表示不等待,拿不到令牌立即返回 false
return limiter.tryAcquire(properties.getTimeoutMillis(),
java.util.concurrent.TimeUnit.MILLISECONDS);
}
}步骤 4:自动配置类
package com.example.ratelimit.autoconfigure;
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.springframework.context.annotation.Configuration;
/**
* 限流自动配置。
* 加载条件:
* 1. classpath 上存在 Guava RateLimiter(@ConditionalOnClass)
* 2. 配置 rate-limit.enabled 不为 false(@ConditionalOnProperty)
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(RateLimiter.class)
@ConditionalOnProperty(prefix = "rate-limit", name = "enabled",
havingValue = "true", matchIfMissing = true)
@EnableConfigurationProperties(RateLimitProperties.class)
public class RateLimitAutoConfiguration {
/**
* 只有当容器里没有用户自定义的 RateLimitExecutor 时才注册。
* 这就是"用户配置优先于自动配置"的体现。
*/
@Bean
@ConditionalOnMissingBean
public RateLimitExecutor rateLimitExecutor(RateLimitProperties properties) {
return new RateLimitExecutor(properties);
}
}步骤 5:注册自动配置类
在 src/main/resources/META-INF/spring/ 下创建 org.springframework.boot.autoconfigure.AutoConfiguration.imports:
com.example.ratelimit.autoconfigure.RateLimitAutoConfiguration注意:Spring Boot 2.7 之前用的是
META-INF/spring.factories里的org.springframework.boot.autoconfigure.EnableAutoConfiguration=...。2.7 引入新文件并标记spring.factories方式为 deprecated,3.0 完全移除。写新 Starter 请一律用.imports文件。
步骤 6:AOP 切面(可选,让注解生效)
package com.example.ratelimit.autoconfigure;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
@Aspect
public class RateLimitAspect {
private final RateLimitExecutor executor;
public RateLimitAspect(RateLimitExecutor executor) {
this.executor = executor;
}
@Around("@annotation(rateLimit)")
public Object around(ProceedingJoinPoint pjp, RateLimit rateLimit) throws Throwable {
MethodSignature sig = (MethodSignature) pjp.getSignature();
String key = sig.getDeclaringTypeName() + "#" + sig.getName();
if (!executor.tryAcquire(key)) {
throw new RateLimitExceededException("请求过于频繁,请稍后重试");
}
return pjp.proceed();
}
}配置类里再补一个 @Bean @ConditionalOnMissingBean 注册切面,并加 @ConditionalOnClass(Aspect.class)。这样当用户没引 AOP 依赖时,整个切面自动不生效——这就是条件装配的威力。
示例二:自定义条件注解(判断是否在 Kubernetes 环境)
自动配置的强大之处在于 @Conditional。我们实现一个 @ConditionalOnKubernetes,只有检测到 K8s 环境时才装配某个 Bean。
package com.example.condition;
import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;
/**
* 判断当前是否运行在 Kubernetes 环境。
* 依据:存在 KUBERNETES_SERVICE_HOST 环境变量,
* 或存在 /var/run/secrets/kubernetes.io/serviceaccount 目录。
*/
public class OnKubernetesCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
String host = context.getEnvironment().getProperty("KUBERNETES_SERVICE_HOST");
if (host != null && !host.isEmpty()) {
return true;
}
return new java.io.File("/var/run/secrets/kubernetes.io/serviceaccount").exists();
}
}package com.example.condition;
import org.springframework.context.annotation.Conditional;
import java.lang.annotation.*;
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(OnKubernetesCondition.class)
public @interface ConditionalOnKubernetes {
}使用:
@Configuration(proxyBeanMethods = false)
public class K8sHealthAutoConfiguration {
@Bean
@ConditionalOnKubernetes
public K8sReadinessIndicator readinessIndicator() {
return new K8sReadinessIndicator();
}
}这里有个关键坑:Condition 接口的 matches 会被调用非常多次(每个条件、每次刷新),所以实现里绝不能做耗时操作(比如发起网络请求探测)。上面读环境变量和文件是否存在,是最安全的做法。如果你需要缓存结果,用 SpringBootCondition 而不是裸 Condition,它提供了 getMatchOutcome 和日志支持。
示例三:让自动配置可观测——自定义 FailureAnalyzer
生产级 Starter 应该"报错友好"。如果用户配置错了,应该给出清晰的错误提示,而不是一堆堆栈。Spring Boot 提供了 FailureAnalyzer 机制。
package com.example.ratelimit.autoconfigure;
import org.springframework.boot.diagnostics.AbstractFailureAnalyzer;
import org.springframework.boot.diagnostics.FailureAnalysis;
/**
* 当限流配置非法(如每秒令牌数 <= 0)时,给出可读的错误提示。
*/
public class InvalidRateLimitConfigFailureAnalyzer
extends AbstractFailureAnalyzer<IllegalArgumentException> {
@Override
protected FailureAnalysis analyze(Throwable rootFailure, IllegalArgumentException cause) {
if (!cause.getMessage().contains("permitsPerSecond")) {
return null;
}
return new FailureAnalysis(
"限流配置非法:" + cause.getMessage(),
"请检查 application.yml 中的 rate-limit.default-permits-per-second,"
+ "该值必须大于 0。",
cause
);
}
}在 META-INF/spring.factories 中注册(FailureAnalyzer 至今仍走 spring.factories,不走 .imports):
org.springframework.boot.diagnostics.FailureAnalyzer=\
com.example.ratelimit.autoconfigure.InvalidRateLimitConfigFailureAnalyzer这样当用户把 default-permits-per-second 配成 0 时,控制台会打印一段人话提示,而不是 IllegalArgumentException 堆栈。这个细节是区分"能用的 Starter"和"好用的 Starter"的分水岭。
方案对比:自动配置 vs 其他装配方案
| 维度 | Spring Boot 自动配置 | 传统 XML/Java Config | SPI(ServiceLoader) | Spring Cloud 的 Bootstrap |
|---|---|---|---|---|
| 装配时机 | 容器启动,条件驱动 | 手动声明 | 运行时按需 | 容器启动前(Bootstrap 阶段) |
| 是否可覆盖 | 是(@ConditionalOnMissingBean) |
完全手动 | 否 | 是 |
| 启动开销 | 有预筛,较快 | 无 | 极低 | 额外一次上下文 |
| 适用场景 | 通用组件、中间件集成 | 业务配置 | 插件化 | 分布式配置 |
| 调试难度 | 高(需 --debug 看报告) |
低 | 中 | 高 |
核心结论:自动配置适合"框架/中间件向业务提供默认能力"的场景。业务代码不应该滥用自动配置——你的业务 Service 用 @Component 就够了,别搞个 BizAutoConfiguration,那是杀鸡用牛刀,还会让依赖关系变隐晦。
最佳实践与避坑指南
1. 永远不要用 @ComponentScan 扫自动配置类
自动配置类必须通过 .imports 文件注册,不要把它放在你的业务包下被 @ComponentScan 扫到。否则条件判定时机可能不对,还会导致重复注册。
2. @ConditionalOnMissingBean 要慎用类型
@Bean
@ConditionalOnMissingBean // 默认按返回类型判断
public RateLimitExecutor rateLimitExecutor(...) { ... }如果用户定义了 RateLimitExecutor 的子类,@ConditionalOnMissingBean 默认按类型匹配,子类也会被认为"已存在",从而跳过。如果你的意图是"用户没定义任何实现就注册默认的",这没问题;但如果想更精确,用 @ConditionalOnMissingBean(value = RateLimitExecutor.class)。
3. 配置类用 @Configuration(proxyBeanMethods = false)
从 Spring Boot 2.2 起官方推荐自动配置类都加 proxyBeanMethods = false。因为自动配置类里的 @Bean 方法之间不应该互相调用(那会绕过容器,破坏单例),关掉 CGLIB 代理能显著减少启动时的类增强开销。
4. spring.factories vs .imports 的迁移
- Spring Boot ≤ 2.6:
META-INF/spring.factories;
- Spring Boot 2.7+:
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports;
- Spring Boot 3.0+:只认
.imports。
如果你维护的 Starter 要同时兼容 2.x 和 3.x,可以两个文件都放。但注意 FailureAnalyzer、EnvironmentPostProcessor 等非自动配置的扩展点,仍然走 spring.factories,别搞混。
5. 用 --debug 或 Actuator 排查
启动加 --debug 参数,Spring Boot 会打印 ConditionEvaluationReport,清楚列出每个自动配置类为什么生效/没生效:
RateLimitAutoConfiguration matched:
- @ConditionalOnClass found required class 'com.google.common.util.concurrent.RateLimiter'
- @ConditionalOnProperty (rate-limit.enabled) matched生产环境可以用 spring-boot-actuator 的 /actuator/conditions 端点在线查看。
6. 常见坑:配置属性绑定失败静默
@ConfigurationProperties 绑定时,如果类型不匹配(比如 yml 里写了字符串,字段是 int),默认会静默忽略。务必加 @Validated + JSR-303 注解,让非法配置在启动时就 fail-fast。
@ConfigurationProperties(prefix = "rate-limit")
@Validated
public class RateLimitProperties {
@Min(1)
private int defaultPermitsPerSecond = 100;
// ...
}总结
回到开头的那个事故。如果那个 trace-log-starter 的作者做到了三点,事故就不会发生:
- 默认值保守:采样率默认 10% 而不是 100%;
- 条件清晰:用
@ConditionalOnProperty明确要求用户配置采样率; - 可观测:用
FailureAnalyzer在配置缺失时给出友好提示。
Spring Boot 自动配置的精髓,是用条件装配把"默认行为"和"用户覆盖"优雅地统一起来。它不是什么魔法,本质就是:
启动时扫描
.imports文件 → 预筛 → 加载 → 条件判定 → 注册 Bean。
理解这五个步骤,你就能:
- 写出对使用者友好的 Starter;
- 排查"为什么我的 Bean 没生效"这类问题;
- 在阅读 Spring Cloud、MyBatis、Dubbo 等框架源码时,一眼看穿它们的装配逻辑。
延伸思考:Spring Boot 3.x 引入了 AOT(Ahead-Of-Time)编译和 GraalVM Native Image 支持。在 AOT 模式下,条件判定发生在构建期而非运行期,这意味着运行时动态变化的条件(比如读环境变量决定是否装配)会失效。这是自动配置机制在云原生时代面临的新挑战,也是理解 Spring Boot 未来演进的关键方向。如果你在做 Native Image 迁移,这块一定要提前踩点。
自动配置写得好,是"润物细无声";写得不好,就是"薛定谔的 Bean"——你不启动看日志,永远不知道它在不在。愿你的 Starter 都能被温柔以待。