引子我们做内部数据访问 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.factoriesSpring 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 RedisTemplateString, Object redisTemplate(RedisConnectionFactory factory) { RedisTemplateString, 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 定义是有先后顺序的先处理ComponentScan扫到的Component/Configuration包括你主应用包下的业务配置。再处理自动装配类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是时序相关的没看到不等于真的没有。这是自动装配最容易踩的顺序雷。我们的修复与验证定位靠这一行// 启动后 / 单元测试里打印确认到底注册了几个 MapString, 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条件要写全套ConditionalOnClassConditionalOnPropertyConditionalOnMissingBean三件套尽量齐全既不让 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挑一个核心类型比如DataSource、RedisTemplate、RestTemplate跑一次ctx.getBeansOfType(XXX.class)看看数量是不是 1。如果不是 1试着用AutoConfigureBefore或挪动自定义 bean 的注册时机把它压回 1 个再观察启动日志和连接数。