Spring Boot 自动配置:一组带条件的候选配置
自动配置不是「自动扫描所有东西」。框架准备了一批候选配置类,启动时逐个判断条件:classpath 上有没有某个类、用户有没有自己声明某个 Bean、某个属性是否打开。条件都满足的才生效,用户自己的配置通常优先。
写一个公司内部的 starter:引入依赖就有一个默认的支付客户端,用户可以自己声明一个覆盖它。本地测试一切正常,某天有人加了一个兜底的「空实现」自动配置,结果容器里同时出现了两个客户端,注入时报 NoUniqueBeanDefinitionException。兜底配置的条件写的是「没有这个 Bean 时才创建」,它判断的时候,真正的客户端还没有被注册。
本文用 Spring Boot 4.1.1 验证自动配置的几个关键行为:四种常用条件、条件报告、自动配置之间的顺序,以及自动配置为什么不依赖组件扫描。
一、先说结论
- 自动配置是候选集合加条件筛选:候选来自
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports,不依赖应用的组件扫描路径。 - 用户的 Bean 优先:
@ConditionalOnMissingBean让默认实现在用户声明了同类 Bean 时退让,实测容器里只有用户的那一个。 - 条件报告能直接回答「为什么没生效」:属性不对、类不在 classpath 上,报告里都写着具体原因。
- 自动配置之间要声明顺序:兜底配置没有声明
after,实测容器里出现 2 个同类 Bean;声明之后只剩 1 个。 - 组件扫描和自动配置是两套机制:自动配置类放在扫描路径之外也会被加载;不在 imports 文件里、也不在扫描路径里的配置类不会生效。
二、启动时发生了什么
不必背调用栈,按层次理解即可:
- 准备
Environment:加载配置文件、环境变量、命令行参数和 profile; - 创建应用上下文,注册主类;
- 解析配置类:先处理组件扫描找到的用户配置,再从 imports 文件读出自动配置候选;
- 对每个候选评估条件,满足条件的注册为 Bean 定义;
- 实例化非懒加载的单例,生命周期见 Spring Bean 生命周期;
- 启动内嵌服务器(Web 应用)、执行 runner,发布就绪事件。
第 3 步的顺序很关键:用户配置先于自动配置处理,所以自动配置里的 @ConditionalOnMissingBean 能看到用户声明的 Bean。
三、四种常用条件
实验里的支付自动配置:
@AutoConfiguration
@ConditionalOnClass(PaymentSdk.class)
@ConditionalOnProperty(prefix = "payment", name = "enabled", havingValue = "true", matchIfMissing = true)
@EnableConfigurationProperties(PaymentProperties.class)
public class PaymentAutoConfiguration {
@Bean
@ConditionalOnMissingBean
PaymentClient paymentClient(PaymentProperties properties) {
return () -> "默认 HTTP 客户端 → " + properties.endpoint() + "(" + PaymentSdk.version() + ")";
}
}用 ApplicationContextRunner 分别检查:
| 情况 | 结果 | 条件报告 |
|---|---|---|
| 条件都满足 | 默认客户端,读到了 payment.endpoint | — |
用户声明了 PaymentClient | 容器里 1 个,是用户的 | — |
payment.enabled=false | 没有 PaymentClient | @ConditionalOnProperty (payment.enabled=true) found different value in property 'enabled' |
classpath 上没有 PaymentSdk | 没有 PaymentClient | @ConditionalOnClass did not find required class 'labs.payment.sdk.PaymentSdk' |
几点说明:
@ConditionalOnClass放在类上,判断发生在加载配置类的方法之前,所以配置类里可以放心引用这个可能不存在的类;@ConditionalOnMissingBean默认按方法返回类型判断,只适合用在自动配置里,在普通配置类里它的结果取决于配置类的处理顺序;matchIfMissing = true表示「没配置就当打开」,关闭需要显式写false。
ApplicationContextRunner 是测试自动配置的标准方式:它不启动完整应用,可以方便地加用户配置、改属性、过滤 classpath,适合写进 starter 自己的测试。
四、为什么某个自动配置没生效
排查顺序:
- 依赖是否真的在运行时 classpath 上(
provided、optional的依赖不会传递,见 Maven Scope); - 属性名和取值是否正确,是否被某个 profile 覆盖;
- 是否已经有同类 Bean 让它退让,可能来自另一个 starter;
- 应用类型是否匹配(Servlet、Reactive、非 Web);
- 读条件报告:启动参数加
--debug会打印CONDITIONS EVALUATION REPORT,引入 Actuator 可以访问conditions端点。报告分 Positive matches(生效了,为什么)和 Negative matches(没生效,卡在哪个条件)。
条件报告比异常栈更直接。上面表格里的两条原因,就是从 ConditionEvaluationReport 里读出来的原文。
五、自动配置之间的顺序
有一个自动配置提供真正的指标输出 MeterSink,另一个自动配置想做兜底:没有指标输出时,提供一个空实现。
@AutoConfiguration // 没有声明顺序
public class FallbackSinkAutoConfiguration {
@Bean
@ConditionalOnMissingBean(MeterSink.class)
MeterSink noopSink() { return () -> "noop"; }
}兜底配置没有声明 after:MeterSink 2 个 [noop, prometheus]
兜底配置声明 after = MetricsAutoConfiguration:MeterSink 1 个 [prometheus]@ConditionalOnMissingBean 在处理这个配置类的那一刻判断。兜底配置排在前面处理时,真正的 MeterSink 还没有注册,条件成立;之后真正的配置再注册一个,容器里就有了两个。修复方法是声明顺序:
@AutoConfiguration(after = MetricsAutoConfiguration.class)实验里为了稳定复现,把兜底配置的类名起成字母序更靠前的名字。真实项目中,没有声明顺序时谁先处理取决于排序规则,可能恰好正确,换个类名、加一个依赖就出问题。凡是自动配置里用 @ConditionalOnMissingBean 判断另一个自动配置的产物,都要显式声明 after 或 before。
六、自动配置与组件扫描是两套机制
@SpringBootApplication 组合了三件事:@SpringBootConfiguration、@ComponentScan(从主类所在的包开始扫描)和 @EnableAutoConfiguration(从 imports 文件读取候选)。
实验里主类在 labs.app,支付自动配置在 labs.payment.autoconfigure,不在扫描范围里:
PaymentClient 存在=true;不在 imports 文件里的 labs.payment.internal.InternalConfig 生效=false自动配置经 imports 文件加载,与扫描路径无关;一个既不在扫描路径、也不在 imports 文件里的配置类不会生效。反过来也成立:不要让自动配置类落在应用的扫描路径里,否则它会被当作普通配置类处理,失去自动配置的排序和条件语义。写 starter 时,自动配置放在独立的包里,并且只通过 imports 文件注册。
旧版本用 META-INF/spring.factories 里的 EnableAutoConfiguration 键注册自动配置;Spring Boot 2.7 引入 imports 文件,3.0 起不再从 spring.factories 读取自动配置。升级时这个位置没改,自动配置就会静默消失。
七、写一个 starter 的检查清单
- 自动配置类用
@AutoConfiguration,注册在 imports 文件里,不在任何应用的扫描路径下; - 可选依赖用
@ConditionalOnClass保护,默认 Bean 用@ConditionalOnMissingBean让位; - 和其他自动配置有依赖时,用
after/before声明顺序; - 属性用
@ConfigurationProperties绑定,写明默认值与开关; - 用
ApplicationContextRunner测试每一种条件组合,包括「依赖不在 classpath 上」。
站内的 Domain Driven Kit 的各个 starter 就是按这套方式组织的。
八、常见误区
- 「自动配置就是自动扫描」:它读取的是 imports 文件里的候选,和组件扫描是两套机制。
- 「写了
@ConditionalOnMissingBean就不会重复」:它只看判断那一刻已经注册的 Bean,自动配置之间要声明顺序。 - 「没生效就去翻源码」:先看条件报告,原因通常写在里面。
- 「主类放错包,所有自动配置都会失效」:影响的是组件扫描,自动配置照常加载。
- 「在普通配置类里用
@ConditionalOnMissingBean也可靠」:普通配置类之间没有这套排序,结果取决于处理顺序。
小结
自动配置的本质是一组带条件的候选:候选从 imports 文件来,条件决定去留,用户配置先于自动配置处理,所以用户的 Bean 可以让默认实现退让。遇到「没生效」看条件报告,遇到「重复了」检查自动配置之间的顺序。循环依赖在 Spring Boot 里为什么默认会启动失败,见 Spring 循环依赖。
配套实验
- codesphere-labs/spring/boot-auto-configuration:四种条件与条件报告、自动配置之间的顺序、不在扫描路径里的自动配置经 imports 文件加载(验证记录)
参考资料