ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Spring Boot 自动装配不是魔法:@ConditionalOnMissingBean 失效那天,我们重读了 spring.factories 的加载顺序

2026/8/3 17:04:41 拓冰建站 浏览量
Spring Boot 自动装配不是魔法:@ConditionalOnMissingBean 失效那天,我们重读了 spring.factories 的加载顺序

引子

我们做内部数据访问 starter 的时候,封装了一个默认DataSource,用@ConditionalOnMissingBean(DataSource.class)留了个"用户自定义就覆盖我"的后门。本以为万事大吉。结果上线两周后,测试库密码轮换,监控里突然冒出一堆连不上测试库的报错——可我们业务代码明明白白用的是自己的DataSource啊。最后用ctx.getBeansOfType(DataSource.class)一查,容器里竟然有 2 个 DataSource bean。那个"本该缺席"的默认 DataSource 悄悄注册了,还默默建了一个 HikariCP 连接池连着测试库。

这次事故逼我们把 Spring Boot 自动装配的加载顺序啃了一遍。结论是:@ConditionalOnMissingBean不是"看全局有没有这个 bean",它只看"到当前这一步为止已经注册了哪些 bean"——顺序错了,它就会失手。这篇文章把自动装配机制和这个顺序坑讲透。

问题:自动装配到底装了什么

你写的@SpringBootApplication是个复合注解,真正负责自动装配的是它身上的@EnableAutoConfiguration。展开看,自动装配就干了一件事:在应用启动时,按条件把一大批"预先写好的 @Configuration"注册进容器

这些预先写好的配置类从哪来?Spring Boot 2.6 及更早版本,靠SpringFactoriesLoader读取所有 jar 包里META-INF/spring.factories文件的EnableAutoConfiguration键:

# spring.factories(Spring Boot 2.6 及之前) org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.example.autoconfig.RedisAutoConfiguration,\ com.example.autoconfig.DataSourceAutoConfiguration

而每个XxxAutoConfiguration类头上挂满了@Conditional系列注解,决定"在当前环境下,这个类到底装不装、里面的 bean 到底建不建"。所谓"自动",本质是"按条件批量注册配置 + 用户可覆盖"。

版本提醒:Spring Boot 2.7 起spring.factories被标记弃用,3.0 直接移除,改用在META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports里列全类名。如果你在 3.x 上还按老写法放spring.factories,自动装配类根本不会被加载——这是另一个高频坑。

原理一:自定义一个 starter 的自动装配

一个最小可工作的自动装配类长这样(以 Redis 为例):

@Configuration @ConditionalOnClass(RedisTemplate.class) // ① 类路径有 RedisTemplate 才装配 @ConditionalOnProperty(prefix = "app.redis", name = "enabled", havingValue = "true", matchIfMissing = true) // ② 开关,默认开 public class RedisAutoConfiguration { @Bean @ConditionalOnMissingBean(RedisTemplate.class) // ③ 用户没定义才用这个默认 public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) { RedisTemplate<String, Object> t = new RedisTemplate<>(); t.setConnectionFactory(factory); t.setKeySerializer(new StringRedisSerializer()); t.setValueSerializer(new GenericJackson2JsonRedisSerializer()); return t; } }

逐行解释:

  • 第 2 行@ConditionalOnClass(RedisTemplate.class):只有项目真的引入了spring-data-redisRedisTemplate在类路径上)才装配这个类。你的 starter 被一个没用 Redis 的项目依赖时,它自动"隐身",不会因缺类而启动报错。这是自动装配"不污染别人"的关键。
  • 第 3 行@ConditionalOnProperty:用配置项app.redis.enabled当总开关,matchIfMissing = true表示不配也默认开启。
  • 第 8 行@ConditionalOnMissingBean(RedisTemplate.class):这才是"用户可覆盖"的后门——容器里到此刻还没有RedisTemplate类型的 bean,才注册默认的;有了就跳过。

原理二:@Conditional 系列是自动装配的灵魂

Spring Boot 提供了一整套条件注解,自动装配类就是靠它们拼出"按需装配"的:

@Configuration @ConditionalOnWebApplication(type = Type.SERVLET) // 仅 Servlet Web 环境才装 @ConditionalOnClass(DispatcherServlet.class) // 类路径有这个类才装 @ConditionalOnProperty("feature.x.enabled") // 配置开关为 true @AutoConfigureAfter(DataSourceAutoConfiguration.class) // 排在数据源自动装配之后 public class XAutoConfiguration { @Bean @ConditionalOnMissingBean public XService xService() { return new XService(); } }

逐行解释:

  • 第 2 行@ConditionalOnWebApplication:你的 starter 被一个纯后端批处理程序依赖时,它不是 Web 环境,整个配置类跳过,避免引入 servlet 相关 bean 导致冲突。
  • 第 4 行@ConditionalOnProperty不带havingValue时,配置存在且非false即生效,常用来做功能开关。
  • 第 5 行@AutoConfigureAfter是"顺序编排":很多自动装配之间有依赖,比如用到DataSource的配置,必须排在DataSourceAutoConfiguration之后,否则它依赖的DataSourcebean 还没建好。这里的After/Before/Order只影响自动装配类之间的相对顺序,不影响普通的@ComponentScanbean。
  • 第 9 行@ConditionalOnMissingBean不带参数时,默认按方法返回类型(XService)判断。

原理三:@ConditionalOnMissingBean 的"检测窗口"陷阱

这是栽我们的根因,必须讲清。@ConditionalOnMissingBean检查的是"到当前 bean 定义处理时刻为止,容器中已存在的 bean",而不是"最终的全部 bean"。而 Spring 处理 bean 定义是有先后顺序的:

  1. 先处理@ComponentScan扫到的@Component/@Configuration(包括你主应用包下的业务配置)。
  2. 再处理自动装配类(@EnableAutoConfiguration引入的那批),且自动装配类之间按@AutoConfigureBefore/After/Order排序。

所以,如果用户的自定义 bean 是通过@ComponentScan注册的(最常见,写在主应用包下),它先于自动装配处理,@ConditionalOnMissingBean能看到它,正确"缺席"——这是正常情况

出问题的,是下面这种:

// 业务模块里,把自定义 DataSource 写在一个 @Configuration 里(注意:不是 @Component) @Configuration public class BizDataSourceConfig { @Bean public DataSource dataSource() { // 业务自己的 DataSource return new HikariDataSource(...); } } // starter 里: @Configuration @ConditionalOnMissingBean(DataSource.class) // 本以为能检测到业务的 dataSource public class StarterDataSourceAutoConfig { @Bean public DataSource dataSource() { return new HikariDataSource(...); // 结果:也注册了! } }

逐行解释:

  • 第 3 行业务dataSource写在BizDataSourceConfig这个普通@Configuration里。如果该配置类因为某种排序原因,在 starter 的自动装配类之后才被处理(比如 starter 用了@AutoConfigureBefore抢先,或业务配置本身又被别的自动装配@Import延迟),那么处理 starter 的@ConditionalOnMissingBean那一刻,业务的dataSource还没注册,@ConditionalOnMissingBean判定"没有"→ 默认 bean 被注册。
  • 第 12 行 starter 的dataSource于是也注册进去。由于业务的@Primary让注入走业务那个,表面上"能跑",但容器里实际上有 2 个 DataSource bean,其中一个默默连着测试库、建着多余的连接池——直到密码轮换才炸出来。

换句话说:@ConditionalOnMissingBean的"Missing"是时序相关的,"没看到"不等于"真的没有"。这是自动装配最容易踩的顺序雷。

我们的修复与验证

定位靠这一行:

// 启动后 / 单元测试里打印,确认到底注册了几个 Map<String, DataSource> dsMap = ctx.getBeansOfType(DataSource.class); System.out.println("DataSource 数量 = " + dsMap.size()); // 预期 1,实际 2 dsMap.forEach((name, ds) -> System.out.println(name + " -> " + ds));

确认有 2 个后,修复有两招:

  • 招一(推荐):把业务的自定义DataSource放在主应用包下、用@Component/@Configuration走组件扫描,保证它先于自动装配被注册,这样@ConditionalOnMissingBean能正常看到它并让默认 bean 缺席。
  • 招二:在业务配置类上显式声明@AutoConfigureBefore(StarterDataSourceAutoConfig.class),强制它在 starter 自动装配之前处理。但@AutoConfigureBefore只对"自动装配类"生效,业务配置得先变成自动装配类(进spring.factories/ imports 文件)才行,代价较大。

我们选了招一:把业务DataSource挪进主应用包下的@Configuration,问题立刻消失,getBeansOfType回到 1 个,那个连测试库的冗余连接池也随之消失。

我的取舍判断

  • 自动装配是便利,不是魔法,更不该是黑盒:用别人 starter 时,遇到"多了一个 bean""配置没生效",第一反应应该是getBeansOfType(XXX.class)看容器里到底有几个,而不是盲目加@Primary掩盖。@Primary 能解决注入冲突,但解决不了"多余 bean 偷偷建连接池"这种资源泄漏。
  • 写自己的 starter,条件要写全套@ConditionalOnClass+@ConditionalOnProperty+@ConditionalOnMissingBean三件套尽量齐全,既不让 starter 污染不需要它的项目,也留好用户覆盖的口子。我们那次就是只写了@ConditionalOnMissingBean,没考虑顺序,才埋雷。
  • @ConditionalOnMissingBean 别依赖"全局唯一"的假设:它看的是时序窗口内的 bean。如果你的覆盖 bean 定义在一个可能被延后处理的@Configuration里,它就不可靠。稳妥的做法是让覆盖 bean 走组件扫描(主应用包)注册。
  • Spring Boot 3.x 务必改用AutoConfiguration.imports文件,别再写spring.factories,否则自动装配类整批不加载,排查起来非常迷惑。

总结

Spring Boot 自动装配 =@EnableAutoConfiguration触发 +SpringFactoriesLoader(或 3.x 的 imports 文件)批量加载XxxAutoConfiguration+ 一堆@Conditional决定装不装。它的价值是"按需、可覆盖",但@ConditionalOnMissingBean的"Missing"是时序相关的——只看处理到当前时刻已注册的 bean。我们的事故正是业务DataSource因排序延后注册,导致@ConditionalOnMissingBean失手、默认 bean 偷偷注册、冗余连接池连着测试库。排查这类问题,永远从getBeansOfType数 bean 开始。

思考题

你项目里引入的第三方 starter,有没有可能"偷偷"注册了你不想要的 bean?挑一个核心类型(比如DataSourceRedisTemplateRestTemplate),跑一次ctx.getBeansOfType(XXX.class),看看数量是不是 1。如果不是 1,试着用@AutoConfigureBefore或挪动自定义 bean 的注册时机把它压回 1 个,再观察启动日志和连接数。