Spring Boot 有一句被反复引用的话约定大于配置。这句话落到源码层面真正支撑它的并不是启动时扫描包的逻辑而是一整套条件装配注解。尤其是ConditionalOnMissingBean名字看着低调但在Spring Boot的自动配置里几乎每个Bean方法上都站着它。它做着同一件事如果容器里已经有用户自己定义的Bean我就不创建默认的没有我再来兜底。这些年我排查过不少Spring Boot启动问题发现很多看似诡异的场景——比如自动配置没生效、引入某个starter后Bean冲突、自己想覆盖默认实现却覆盖不掉——最后都绕回条件装配这一环。所以这篇内容准备把ConditionalOnMissingBean以及它身边那批ConditionalOn*注解彻底讲透包括它们各自的定位、源码里的判定逻辑、在实际starter里的写法以及那些官方文档不会明说的坑。适合正在做Spring Boot开发或维护基础框架的人也适合想理解“自动配置到底是怎么自动起来”的初学者。1. 为什么需要条件装配1.1 从硬编码 Bean 到智能装配在聊ConditionalOnMissingBean之前先说说没有条件装配时Spring 配置是个什么状态。早期用XML配置Spring一个系统往往要维护多份配置文件开发环境一份、测试环境一份、生产环境一份。Bean 的连接方式全靠手工切换配置文件改一个Bean实现类兴许要动好几个地方。后来进入注解时代ConfigurationBean帮我们省掉了大量XML但还是解决不了一个核心问题Bean 的创建逻辑是写死的。比如一个Bean方法如果里面用if (condition) return new A(); else return new B();这只能应付简单的分支。可Spring面对的真实场景远比分支复杂依赖A存在时装配方案X依赖B存在时装配方案Y当前是Web环境还是非Web环境用户自己在应用代码里定义了一个同名Bean我不能再去创建默认Bean某个配置项写的是true我才需要抛出对应的Bean。这些判断不能靠写死在Bean方法里完成因为它们需要感知容器当下的状态——目前有哪些Bean、有哪些类、运行环境是什么。于是Spring从4.0开始引入Conditional注解从Spring Boot 1.0开始把一大批基于Conditional的派生注解交给开发者使用。这就是条件装配整套玩法的起点。1.2 条件装配到底解决了什么问题条件装配的本质是把“Bean 该不该创建”这个决策从写死的代码里抽出来变成一个可插拔的判定过程。我通常会跟新人举一个例子来说明你装修房子水电工的活儿自己干不了所以找装修公司。但如果你已经私下请了电工装修公司进场时就得自动跳过水电这一项不能重复施工。这就是“缺失才装配”。落到代码世界它解决了三类高频问题第一类是多实现适配。比如缓存组件Redis存在就用Redis实现Redis不在就回退到本地内存实现。ConditionalOnClass在这里做依赖探测。第二类是优雅降级。比如系统里引入了MinIO客户端依赖自动配置就创建MinIO的连接没引入就创建一个基于本地文件系统的实现。Spring Boot 自身大量使用这种策略比如ConditionalOnClassConditionalOnMissingBean的组合。第三类是用户优先。框架提供默认的SecurityManager、默认的DataSource、默认的消息发送器但用户完全可以自定义替换。此时框架必须“退让”——检测到用户已有Bean就不注册自己那份默认实现。这正是ConditionalOnMissingBean最经典的用途。条件装配的价值不在于它能少写几行if而在于它把框架行为从“写死”变成“可协商”框架提供默认用户拥有优先权一切由容器状态自动裁决。2. 条件装配注解全家族解析2.1 底层其实是 Condition 接口所有条件装配注解追根溯源都指向Spring最底层的Condition接口。Condition接口只有一个方法boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);ConditionContext可以从里面拿BeanFactory、Environment、ResourceLoader等对象AnnotatedTypeMetadata则用于读取当前注解上声明的属性。matches返回true说明条件命中Bean定义继续注册返回false则跳过。而Conditional是Spring原生注解它本身只需要声明一个Condition数组Target({ElementType.TYPE, ElementType.METHOD}) Retention(RetentionPolicy.RUNTIME) interface Conditional { Class? extends Condition[] value(); }Spring Boot 的ConditionalOnClass、ConditionalOnBean、ConditionalOnMissingBean等等本质上都是被Conditional标注的组合注解每个注解都绑定了对应的Condition实现类。比如ConditionalOnClass绑定OnClassConditionConditionalOnMissingBean绑定OnBeanCondition。理解这一层之后你就知道条件装配不是什么玄学它就是Spring IoC容器注册Bean定义前的一个前置校验器。校验通过了Bean定义被登记校验不通过这个Bean定义会被直接忽略。2.2 看一遍条件装配注解全家福Spring Boot 提供了一批现成的条件注解使用频率和定位各不相同。我整理了一个速查表方便对比记忆。注解判断依据典型场景ConditionalOnClass/ConditionalOnMissingClass指定类是否存在于classpath依赖存在才装配相关组件比如有RedisTemplate才装配Redis客户端ConditionalOnBean/ConditionalOnMissingBean容器中是否已经存在指定Bean用户已定义则跳过默认实现是自动配置的“兜底”主力ConditionalOnProperty配置项是否满足havingValue要求通过开关控制某个功能是否启用ConditionalOnExpressionSpEL 表达式结果是否为真多配置项组合判断灵活但可读性差ConditionalOnJava当前JDK版本是否满足范围按Java版本启用不同实现ConditionalOnWebApplication/ConditionalOnNotWebApplication当前应用是否为Web环境Web环境才注册Servlet相关组件ConditionalOnResource指定资源是否存在检测配置文件是否存在再装配ConditionalOnSingleCandidate容器中指定类型只有一个候选Bean自动配置希望依赖单一实现场景ConditionalOnCloudPlatform当前是否运行在特定云平台云平台差异适配这么多注解真正应用最广、也最容易踩坑的就是ConditionalOnBean和ConditionalOnMissingBean。它们和ConditionalOnClass这类纯classpath探测不同它们的判断对象是容器中的Bean定义集合。这就引出了一个关键问题判断时机、Bean注册顺序会直接影响判定结果。2.3 怎么用代码判断一个条件装配注解的“命中”如果你去看Spring Boot的源码会发现条件装配的评估不是一条线走到底而是分阶段的。Spring Boot 2.x 之后ConditionEvaluator在内部区分了ConfigurationPhasePARSE_CONFIGURATION阶段处理配置类上的条件注解读取元数据决定要不要解析某个配置类。REGISTER_BEAN阶段处理Bean方法上的条件注解决定要不要注册某个Bean定义。举个例子下面的写法Configuration ConditionalOnProperty(name my.enable, havingValue true) public class MyConfig { Bean ConditionalOnMissingBean(MyService.class) public MyService myService() { return new MyService(); } }ConditionalOnProperty作用于类在PARSE_CONFIGURATION阶段执行ConditionalOnMissingBean作用于Bean方法在REGISTER_BEAN阶段执行。两个阶段拿到的容器状态是不一样的这直接影响判定结果。Spring Boot 特意把阶段区分开就是为了避免一种错误配置类被解析时自己注册的Bean提前“污染”了条件判断。如果你自己写框架尤其要留意这个机制否则很容易出现“我明明加了ConditionalOnMissingBean怎么还是创建了两个Bean”的怪现象。3. ConditionalOnMissingBean 深入剖析3.1 这个注解的职责与使用限制ConditionalOnMissingBean从名字就能猜出逻辑当容器中缺少某个Bean时条件才成立。它作用于Configuration类或Bean方法上通常用于声明“默认实现”的场景。最常见的官方用法比如RedisAutoConfigurationBean ConditionalOnMissingBean(name redisTemplate) public RedisTemplateObject, Object redisTemplate(RedisConnectionFactory connectionFactory) { RedisTemplateObject, Object template new RedisTemplate(); template.setConnectionFactory(connectionFactory); return template; }这段代码传递的语言是你已经创建了redisTemplate这个Bean我就不重复创建了你要是没创建我提供一个默认的。这里的“Bean”在判定时是基于BeanDefinition的而不是基于已经实例化的对象。有个容易混淆的点很多人以为ConditionalOnMissingBean和ConditionalOnBean刚好互为反义词。它们确实是反义逻辑但有一个显著差异决定了它们的使用场景不同——ConditionalOnMissingBean通常出现在自动配置类中而ConditionalOnBean在自动配置类里用起来要格外小心。原因在于自动配置类的加载顺序在所有用户配置之后用户定义的Bean已经注册所以“是否存在”基本是准的而ConditionalOnBean如果被某个自动配置类使用一旦同类Bean的定义注册顺序晚于当前判断就可能出现“明明会有但此时还没有”的误判。3.2 参数与匹配规则详解ConditionalOnMissingBean注解的定义参数不少实际经常用到的主要是下面这些参数作用示例value按类型匹配需要检测的Bean类型ConditionalOnMissingBean(NotificationSender.class)name指定Bean名称按名字匹配ConditionalOnMissingBean(name notificationSender)type用字符串形式指定类型名ConditionalOnMissingBean(type com.example.NotificationSender)annotation容器中带有指定注解的BeanConditionalOnMissingBean(annotation EnableAspectJAutoProxy.class)search搜索策略CURRENT当前容器/PARENT父容器/ALL全部默认ALLparameterizedContainer泛型容器类型用于精确匹配ListX这类Bean比较冷门其中value和name是最常用的。需要特别强调的是类型判断走的是Class层面的匹配框架会遍历所有已注册的BeanDefinition名称转换对应类型进行比对。所以它判断的不是“有没有生成实例”而是“有没有这个类型的Bean定义”。还有一个细节注解声明了多个属性时比如value和name同时出现Spring Boot的处理方式不是“与”而是“或”——其中之一命中就认为Bean存在。我刚用的时候被这个行为坑过一次后来翻源码才确认这里提前给大家排雷。3.3 源码级解析OnBeanCondition 是如何工作的要真正懂ConditionalOnMissingBean得看看它的条件类OnBeanCondition。这个类实现了Spring的Condition接口核心逻辑在getMatchOutcome方法里流程可以概括为三步。第一步从注解标签中收集条件信息。OnBeanCondition会读取ConditionalOnBean或ConditionalOnMissingBean上声明的类型、名称、注解约束汇总成一个内部匹配容器。第二步从BeanFactory获取候选Bean定义。它通过ListableBeanFactory的getBeanNamesForType方法拿到所有匹配类型的Bean名称。这里有个细节如果当前容器找不到会根据search策略回溯到父容器继续查找。考虑到效率这个动作每次都尽量复用已有的BeanDefinition集合不会去触发Bean的提前创建。第三步根据“空则命中”或者“有则命中”的逻辑返回匹配结果。ConditionalOnMissingBean走的是反向判断——候选集合为空才返回匹配成功。如果只讲到这里估计有人会问那它判断时会不会触发Bean初始化一般来说不会。因为条件评估阶段容器主要基于BeanDefinition工作绝大多数场景不至于提前实例化。但如果是通过Condition自定义条件类在里面手动调用beanFactory.getBean(...)那就会强制触发初始化有可能会带来一系列连锁副作用——这点写自定义条件时务必记住。OnBeanCondition的源码注释里还有一个值得注意的点它明确建议优先使用ConditionalOnMissingBean而不是ConditionalOnBean来做“默认Bean只装配一份”的约束。这不是拍脑袋定的规则而是实践之后总结出来的最佳实践补缺判断比存在判断更稳定因为“缺”默认是常态“存在”往往依赖注册顺序。4. 实战实现一个带默认实现的通知starter4.1 需求与设计纸上谈兵差不多了拿一个实际项目里的场景来完整演示。假设我要做一个消息通知的starter对外提供NotificationSender接口代码使用方只要注入它就能发通知。设计原则很简单如果使用者自己定义了NotificationSender类型的Bean就优先用他的如果没有定义自动配置类提供一个默认的DefaultNotificationSender配套一个NotificationProperties允许通过配置开关控制是否启用自动配置。为了让演示更贴近实际开发我会同时使用ConditionalOnMissingBean、ConditionalOnProperty和AutoConfiguration三个注解。4.2 编写自动配置类先定义接口和默认实现public interface NotificationSender { void send(String phone, String content); } public class DefaultNotificationSender implements NotificationSender { Override public void send(String phone, String content) { System.out.println(默认短信通道: phone - content); } }这两个类就是普通类不需要加任何Spring注解。然后是配置属性类ConfigurationProperties(prefix custom.notification) public class NotificationProperties { /** * 是否启用通知自动配置默认开启 */ private boolean enabled true; public boolean isEnabled() { return enabled; } public void setEnabled(boolean enabled) { this.enabled enabled; } }最后是自动配置类AutoConfiguration ConditionalOnClass(NotificationSender.class) EnableConfigurationProperties(NotificationProperties.class) public class NotificationAutoConfiguration { Bean ConditionalOnMissingBean(NotificationSender.class) public NotificationSender notificationSender() { return new DefaultNotificationSender(); } Bean ConditionalOnMissingBean ConditionalOnProperty(prefix custom.notification, name enabled, havingValue true, matchIfMissing true) public NotificationSenderConfig notificationSenderConfig() { return new NotificationSenderConfig(); } }这里我故意写了两个Bean来展示细节。自动配置类用AutoConfiguration而不是Configuration是为了和Spring Boot 2.7之后的自动配置加载机制对齐ConditionalOnClass保证类路径有NotificationSender才解析这个配置类ConditionalOnMissingBean则保证用户一旦有自己的实现默认实现自动让位。至于NotificationSenderConfig这个Bean它演示了另一个用法用ConditionalOnProperty控制某个扩展开关。使用者把custom.notification.enabled配成false这个配置类就不注册。4.3 启动验证与覆盖验证代码写完之后按以下三步验证。第一步不加任何自定义Bean直接启动应用。你会看到DefaultNotificationSender被创建因为NotificationSender这个类型在容器里没有其他Bean定义ConditionalOnMissingBean条件成立。第二步在应用配置类里自己创建一个实现Configuration public class CustomNotificationConfig { Bean public NotificationSender notificationSender() { return new EmailNotificationSender(); } }此时容器里已经有NotificationSender类型的Bean定义自动配置类里的默认实现因为ConditionalOnMissingBean判定“缺失不成立”而被跳过。启动日志里不会出现DefaultNotificationSender注入的是EmailNotificationSender。第三步修改配置custom.notification.enabledfalse即使没有自定义实现notificationSenderConfig也不会被装配这就完成了按配置项裁剪的功能。这个实战里的三个注解各司其职ConditionalOnClass管依赖存在性ConditionalOnMissingBean管用户优先权ConditionalOnProperty管功能开关。这也是Spring Boot官方自动配置里最常见的“三件套”组合。5. 常见问题与排查经验5.1 条件没生效时先看条件评估报告遇到“自动配置没生效”的疑问别急着猜Spring Boot内置了一张体检表。只要在application.properties里加上debugtrue启动时控制台会打印一份自动配置报告里面分成四块Positive matches命中的条件配置、Negative matches未命中的条件配置、Unconditional classes无条件配置类、Exclusions被排除的配置。每一行都会附上对应条件的判断依据例如Negative matches: NotificationAutoConfiguration#notificationSender Did not match: - ConditionalOnMissingBean (types: com.example.NotificationSender; SearchStrategy: all) found beans of type com.example.NotificationSender (customNotificationConfig.notificationSender)看到这种输出问题就直接定位了不是自动配置没扫到而是条件被用户自定义Bean给“否决”了。这是我排查条件装配问题时最依赖的工具。5.2 ConditionalOnMissingBean 失效的三大原因综合我踩过的坑这个注解失效主要集中在三种情况。第一种是条件注解加到了实例方法执行之后。有人会把ConditionalOnMissingBean直接放在Service类上期望“容器没有某Bean才加载这个服务”——但这会受配置类扫描顺序影响不像在自动配置类里那么可靠。官方最佳实践依然是在自动配置类上配合Bean使用。第二种是Bean名冲突导致误判。前面提到过ConditionalOnMissingBean的匹配既可以按类型也可以按名称。如果用户Bean的类型确实匹配但名称不同类型维度上还是能命中“已存在”的判断这一点是符合预期的反过来如果自动配置类里只声明了name而没声明value用户Bean即使类型一样但名字不同就检测不到默认Bean照样创建。所以设计自动配置时优先用类型匹配少用裸的name。第三种是Bean注册顺序颠倒。ConditionalOnMissingBean的判断依赖于当前容器里已有的BeanDefinition集合。如果某个自动配置类在用户配置类之前被解析而用户Bean又恰恰是在自动配置类之后才注册那你等不到“缺失”的结果。解决方式是使用AutoConfigureAfter或者Order显式控制自动配置类的加载顺序。Spring Boot 官方也提供了一整套AutoConfigurationOrder常见场景大可不必自己排序但如果你发现条件“诡异”多半还是要回来看顺序。5.3 和 ConditionalOnBean 的爱恨情仇ConditionalOnBean与ConditionalOnMissingBean是同一套底层逻辑但ConditionalOnBean在自动配置类里非常容易出现反直觉的结果。一个典型场景是Configuration ConditionalOnBean(DataSource.class) public class MybatisAutoConfiguration { }看起来逻辑很清晰容器里有DataSource才装配MyBatis。而实际运行时自动配置类的解析顺序很可能早于DataSource的真正注册条件判断为空导致这个配置类根本没被解析。官方对这个问题的处理方式是在MybatisAutoConfiguration上标注AutoConfigureAfter(DataSourceAutoConfiguration.class)强调在DataSource装配之后再做判断。这也是为什么你去看Spring Boot的数据库相关自动配置类几乎都会有AutoConfigureAfter的身影。所以我的经验是ConditionalOnBean更适合在用户自己的配置类里使用用来判断“我依赖的组件是否已被外置装配”在框架的自动配置类里尽量用ConditionalOnMissingBean去兜底而不是用ConditionalOnBean去抢占。5.4 自定义条件注解的两个要点如果你想写自己的条件注解有两件事要认真对待。第一matches方法里尽量只读BeanDefinitionRegistry不要触发Bean实例化。你可以用ConfigurableListableBeanFactory.getBeanNamesForType拿到Bean名称再基于名称判断而不是直接getBean。否则一个简单的条件判断可能会把一整套Bean初始化链给拉起来启动耗时和潜在副作用都不可控。第二注意条件评估阶段。如果你要让注解在类级别生效实现ConfigurationCondition接口并正确返回getConfigurationPhase()比直接实现Condition更稳妥。官方注释也强调Bean方法上的条件最好在REGISTER_BEAN阶段评估类的条件在PARSE_CONFIGURATION阶段评估。分不清这个就会出现“条件类执行了但注解没效果”的情况。写在最后说点个人体会。条件装配注解看起来很轻一个注解一行注解参数但它是Spring Boot自动配置的灵魂。那些我们平时不太注意的“默认行为”绝大多数都是ConditionalOnMissingBean在背后做的让步。真正理解它之后再看自动配置源码你会突然发现很多“这里为什么这么写”的问题都有了答案。一个小技巧送给正在排查相关问题的读者遇到自动配置相关Bug先开debugtrue看条件评估报告比漫无目的地打断点效率高得多调ConditionalOnMissingBean时优先保证用户自定义的Bean定义先于自动配置类注册再考虑条件本身的写法。条件装配这套机制平时不显山露水但它一旦出错影响的就是整个容器的装配结果值得每个Spring Boot开发者花时间吃透。