
说个我实际碰到的场景。给公司内部做了一套基于Spring Boot的微服务基座里面有个全局的RestTemplate自动配置统一管理连接池、超时和负载均衡策略。结果有一天同事在业务服务里自己new了一个RestTemplate加了自定义拦截器服务一启动基座配的那个就被覆盖了好几个调用方开始报连接异常日志里还看不到任何迹象。后来我把配置改成用ConditionalOnMissingBean兜底也就是容器里已经有自定义实现咱就不掺和没有的话我再补一个默认的问题才彻底解决。这个ConditionalOnMissingBean只是Spring条件装配注解族里的其中一个。这一族注解还包括ConditionalOnBean、ConditionalOnClass、ConditionalOnProperty、ConditionalOnExpression等。它们的作用都是在容器刷新过程中根据当前环境、类路径、已有Bean的情况决定某些配置是否生效、某些Bean是否注册。Spring Boot能实现引入一个依赖就自动配置好但你又可以随时覆盖这种体验靠的就是这套机制。这篇文章我把它们的原理、使用场景、以及实战中容易踩的坑一次说清楚。1. 为什么需要条件装配从硬编码容器到按需生效1.1 没有条件装配时Bean管理有多僵硬先想象一个最传统的方式写配置类。你有个Configuration里面固定注册三个Bean它们永远生效不管外面环境怎么变。这在单项目、单团队里没毛病但一旦做成可复用的组件、Starter、或者多环境的公共服务立刻就有问题使用者想换一个自己的实现就得改你的源码或者去掉你的配置类。不同的部署环境里数据源、缓存、消息队列的依赖情况不一样一个配置写死另一个环境依赖缺失就直接启动失败。多个组件拼在一起时大家都想注册同一个类型的Bean谁后注册谁赢结果不可控。Spring Boot解决这个问题的方式就是文档里一句轻描淡写的话自动配置类会在条件满足时生效。ConditionalOnMissingBean这类注解就是执行这句话的裁判员。1.2 条件装配注解族谱一张表看清各自职责官方把这组注解放在org.springframework.boot.autoconfigure.condition包里按用途分大概有这些注解判断依据典型场景ConditionalOnClass/ConditionalOnMissingClass类路径上是否存在指定类某个类存在于依赖中才加载对应配置ConditionalOnBean/ConditionalOnMissingBean容器中是否已注册指定类型的Bean用户自定义Bean时跳过默认实现ConditionalOnProperty配置文件中的某个配置项通过配置开关控制功能启用ConditionalOnExpressionSpEL表达式结果多条件的复杂组合判断ConditionalOnWebApplication/ConditionalOnNotWebApplication应用类型是否为Web应用Web场景专属配置ConditionalOnJavaJava版本号不同JDK版本采用不同实现ConditionalOnResource类路径上是否存在资源文件存在指定文件才启用配置ConditionalOnCloudPlatform是否运行在指定云平台云环境专属配置它们本质上都是Conditional的派生组合。Conditional是Spring 4.0引入的原生注解指定一个Condition实现类由该实现类的matches()方法决定配置是否生效。Spring Boot在你写条件注解之前先帮你把最常用的判断逻辑封装好了省得每次从零写判断代码。用的时候如果想做非常个人的判断逻辑也可以直接实现Condition接品配Conditional这一族注解的底层也都是同一套机制。2. ConditionalOnMissingBean 的判断机制与生效时序2.1 它到底在看什么BeanDefinition 层面的判断很多人以为ConditionalOnMissingBean是在容器创建完所有Bean之后做检查实际上不是。它在BeanDefinition注册阶段就去数人头了——Spring在解析配置类时会把每个Bean方法、每个组件扫描到的类都转成BeanDefinition注册到BeanDefinitionRegistry。ConditionalOnMissingBean这时候就开始翻阅当前已经注册完成的BeanDefinition有没有目标类型没有条件通过注册这个Bean有条件不通过跳过这个Bean。这里有个关键认知它在判断时那些还没来得及被处理的配置类中声明的Bean是看不到的。所以后面配置类的加载顺序就成了决定性变量。举一个最经典的例子Configuration public class MyAutoConfiguration { Bean ConditionalOnMissingBean public MyService myService() { return new DefaultMyService(); } }如果使用者自己在Configuration里定义了一个MyService类型的Bean而使用者的配置类恰好排在自动配置类之前被解析那条件判断就会命中已存在自动配置里那个默认实现就不再注册。反过来如果使用者配置类排在后面ConditionalOnMissingBean看到的容器里并没有MyService默认实现会先注册进来后面使用者的Bean又注册进来结果就是两个同名或同类型的Bean都在容器里按类型注入时直接报NoUniqueBeanDefinitionException。刚接触条件装配的人最容易在这上面栽跟头以为写了ConditionalOnMissingBean就能自动让路但让路的逻辑对顺序极度敏感。2.2 ConfigurationCondition为什么还分阶段求值如果你读过SpringBootCondition的源码会发现它实现了ConfigurationCondition接口里面有这么一个方法Override public ConfigurationPhase getConfigurationPhase() { return ConfigurationPhase.REGISTER_BEAN; }ConfigurationPhase有两个值PARSE_CONFIGURATION和REGISTER_BEAN。前者是在配置类解析阶段执行条件判断——这个时候Bean方法对应的BeanDefinition大多还没注册完后者是在BeanDefinition注册阶段执行判断——此时已经能看到前面注册的BeanDefinition。Spring Boot把ConditionalOnBean、ConditionalOnMissingBean这两个条件放进REGISTER_BEAN阶段就是为了尽量多看一些Bean再下结论。而ConditionalOnClass、ConditionalOnProperty这类不依赖容器状态的判断留在PARSE_CONFIGURATION阶段就够了。这个阶段的取舍对结果影响很大。假如把ConditionalOnMissingBean放到PARSE_CONFIGURATION阶段它几乎永远看不到任何用户Bean默认实现会在绝大多数场景下生效那这个注解的意义就大打折扣。Spring Boot选择在REGISTER_BEAN阶段核心目的是让用户自定义优先于自动配置默认值这个直觉成立。2.3 配置类加载顺序AutoConfigureAfter 不是摆设既然条件判断依赖已有的BeanDefinition那配置类之间的先后顺序就成了决定胜负的隐性规则。Spring Boot用AutoConfigureBefore、AutoConfigureAfter、AutoConfigureOrder来管理自动配置类之间的顺序并且对自动配置类 vs 用户配置类做了硬性规定用户自定义的配置类通常会被优先处理自动配置类统一排在后面。再具体一点Spring Boot在AutoConfigurationImportSelector里做加载时会把自动配置类放在最后面处理。也就是说你应用里手写的Configuration配置类在默认情况下都会被扫描并解析得比自动配置类更早。这就是为什么用户自定了一个Bean自动配置里的ConditionalOnMissingBean能看到它能成立的基础。假如有人反手写了一个AutoConfigureBefore(用户配置类)的骚操作把自动配置硬塞到用户配置前面原本能正常跳过逻辑的默认实现就会抢先注册然后用户再定义同类Bean容器秒变有两个候选。在线排查这类问题时优先去翻启动日志里的“自动配置报告”。Spring Boot会列出每个自动配置类的状态比如Positive match、Negative match、Excluded。看到Positive match了还要再留意后面BeanDefinition的注册顺序别急着下结论说条件判断错了。2.4 search 策略到底是个啥ConditionalOnMissingBean还有一个容易被忽略的属性search取值是SearchStrategy枚举CURRENT当前容器、ANCESTORS只向上找父容器、ALL当前容器加所有祖先容器。Spring Boot应用中单容器场景居多CURRENT是默认值基本够用。一旦牵扯到Spring Cloud这类会有Bootstrap Context、Servlet Context、Application Context多层容器的环境父容器里已经注册了某个服务子容器里又想做条件判断这时候只搜当前容器就会误判。解决方式是显式声明search SearchStrategy.ALLBean ConditionalOnMissingBean(search SearchStrategy.ALL) public RemoteConfigClient remoteConfigClient() { return new DefaultRemoteConfigClient(); }这样条件判断会沿着容器层级往上查询避免在子容器里因为看不到父容器而重复注册。3. 实战用 ConditionalOnMissingBean 做一个可被覆盖的默认实现3.1 场景设定一个小组件要允许别人替换实现我拿一个实际做过的组件来说明。假设要给一套内部框架加一个业务操作审计日志能力默认实现是把审计日志打印到应用日志文件里方便本地开发但线上团队通常希望把审计日志推到统一日志平台。这个组件不能写死默认行为使用者可能引用了官方SDK、也可能自己接入了公司的日志网关。所以它就该做成我提供一个默认的AuditLogger但如果用户在应用里自己定义了AuditLogger类型的Bean默认那个就直接作废。不直接作废还不够要更精确使用者没定义默认顶上使用者定义了默认退下。这就是ConditionalOnMissingBean的标准生存形态。3.2 代码落地自动配置类 条件注解先定义一个接口public interface AuditLogger { void log(String action, String operator, String detail); }再给默认实现public class Slf4jAuditLogger implements AuditLogger { private static final Logger log LoggerFactory.getLogger(Slf4jAuditLogger.class); Override public void log(String action, String operator, String detail) { log.info(audit action{}, operator{}, detail{}, action, operator, detail); } }然后是自动配置类Configuration(proxyBeanMethods false) AutoConfiguration public class AuditAutoConfiguration { Bean ConditionalOnMissingBean(AuditLogger.class) public AuditLogger slf4jAuditLogger() { return new Slf4jAuditLogger(); } }proxyBeanMethods false这是一个性能优化点直接告诉Spring这个配置类的Bean方法不需要返回代理对象。条件判断是纯注册期逻辑与是否代理无关写自动配置类可以放心关掉。AutoConfiguration是Spring Boot 2.7以后推荐的自动配置标注相比传统在spring.factories里声明、再配合Configuration的做法语义更明确也方便编译期处理。把这个类对应到META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件里每行一个全限定类名com.example.starter.audit.AuditAutoConfiguration启动应用后默认生效的AuditLogger就是Slf4jAuditLogger。如果使用者的应用里定义了一个AuditLogger的BeanConfiguration public class AppConfig { Bean public AuditLogger kafkaAuditLogger() { return new KafkaAuditLogger(); } }因为用户配置类会先被处理AuditAutoConfiguration里的ConditionalOnMissingBean(AuditLogger.class)在REGISTER_BEAN阶段能看到kafkaAuditLogger这个BeanDefinition就判定条件不成立自动配置里的默认实现不会注册。最终容器里只有kafkaAuditLogger一个候选按类型注入干净利落。3.3 让使用者覆盖默认实现的四条路径从组件设计者的角度来看使用方通常有这几种方式接管你的默认实现路径操作方式适用场景自定义同类型Bean在自己配置类里直接Bean最常见的替换方式排除自动配置类spring.autoconfigure.excludecom.example.starter.audit.AuditAutoConfiguration或者SpringBootApplication(exclude ...)整个自动配置都不需要条件不满足类路径里不引入默认实现依赖或配置项不满足ConditionalOnProperty按环境控制通过ConditionalOnMissingBean的annotation属性使用者加一个指定注解来标记自己的实现给实现打标签组件只认标了注解的Bean最后一条展开说一句ConditionalOnMissingBean除了按类型判断还支持按注解、按BeanName判断。比如组件要求必须用CustomMarker注解的Bean那条件注解可以这样写Bean ConditionalOnMissingBean(annotation CustomMarker.class) public AuditLogger slf4jAuditLogger() { return new Slf4jAuditLogger(); }判断逻辑会变成容器里有没有被CustomMarker标注的Bean有就跳过默认实现。4. 条件装配的高危坑点与排查链路4.1 配置顺序导致的判断时还没看到Bean在2.1节我提过顺序问题这里给一个更明确的翻车例子。服务里有两份配置一份来自公司公共依赖一份是本应用的配置// 公共依赖里的配置 Configuration public class CommonConfig { Bean public MessageConverter commonConverter() { return new CommonMessageConverter(); } } // 本应用里的配置 Configuration public class AppConfig { Bean ConditionalOnMissingBean(MessageConverter.class) public MessageConverter appConverter() { return new AppMessageConverter(); } }如果CommonConfig被扫描得比AppConfig早appConverter()的条件判断会看到commonConverter已经存在于是跳过。到这里还算正常。反过来如果CommonConfig没有参与组件扫描而是以Import(CommonConfig.class)的方式硬编码引入加载顺序可能截然不同。结果是appConverter()先注册然后CommonConfig又注册了一个commonConverter。容器里同时存在两个MessageConverter注入时报错。这种问题的本质不是ConditionalOnMissingBean坏了而是条件判断发生的时机早于你期望的所有Bean都齐了的时机。想彻底规避最稳妥的做法是默认实现的Bean上加上ConditionalOnMissingBean并对你依赖的公共配置做顺序管控用AutoConfigureAfter(公共配置类.class)显式声明先后。4.2 泛型擦除让条件判断失灵ConditionalOnMissingBean的类型判断在泛型场景下有一个很隐蔽的问题。看这个接口public interface MessageHandlerT { void handle(T message); }两个实现分别处理订单消息和库存消息Bean public MessageHandlerOrderMessage orderHandler() { return new OrderMessageHandler(); } Bean ConditionalOnMissingBean(MessageHandler.class) public MessageHandlerInventoryMessage inventoryHandler() { return new InventoryMessageHandler(); }因为Java泛型擦除MessageHandler和MessageHandler在运行时看都是裸类型MessageHandler。orderHandler注册之后条件判断在数类型时看到的是MessageHandler裸类型于是判定已经有这个类型的Bean了inventoryHandler直接被跳过。这就把同一接口的不同参数化类型实现错当成了同一个Bean。规避方案有两类。一类是接口设计时避开泛型或者至少给条件判断加个限定用ConditionalOnMissingBean(name inventoryHandler)按BeanName判断别按类型判断。另一类是注册时带上有语义的BeanName条件是缺哪个名字就注册哪个名字Bean(inventoryHandler) ConditionalOnMissingBean(name inventoryHandler) public MessageHandlerInventoryMessage inventoryHandler() { return new InventoryMessageHandler(); }这样即便泛型擦除了按BeanName判断依然准确。实际项目里同接口多泛型实现是常态条件注解的判断目标尽量细化到BeanName级别比盲目按类型判断稳得多。4.3 组合条件时条件注解之间的短路逻辑ConditionalOnMissingBean不是只能单独用。Spring允许在同一个配置类或Bean方法上叠多个条件注解它们之间是与关系全部通过才注册。这里有个反直觉的坑。有人想表达如果没有目标Bean而且配置项打开了才注册默认实现于是写Bean ConditionalOnMissingBean(MyService.class) ConditionalOnProperty(name myapp.feature.enabled, havingValue true, matchIfMissing true) public MyService defaultMyService() { return new DefaultMyService(); }看着没问题实际也没问题。但换一个需求就出事了有人想表达如果目标Bean存在才注册增强Bean同时还要满足配置项于是写Bean ConditionalOnBean(MyService.class) ConditionalOnMissingBean(MyService.class) public MyServiceEnhanced enhanceService() { return new MyServiceEnhanced(); }这个条件组永远不会成立。同一个类型ConditionalOnBean要求存在ConditionalOnMissingBean要求不存在两者互斥。这类写法在复制粘贴时最容易出现检查条件组合时一定要把每个条件代入具体场景推理一遍别默认条件叠加等于更严格。顺带说一句ConditionalOnBean是判断容器里当前是否已有目标Bean这和ConditionalOnMissingBean天然对立一般不会放在同一个Bean上。真要表达有A类才注册B类但B类还没被注册这种诉求应该让两个条件对着不同目标类型做判断Bean ConditionalOnBean(MQConnectionFactory.class) ConditionalOnMissingBean(MQConsumerContainer.class) public MQConsumerContainer defaultConsumerContainer() { return new DefaultConsumerContainer(); }条件目标是MQConnectionFactory和MQConsumerContainer两个不同类互不冲突逻辑成立。4.4 条件注解在 Bean 方法上与在类上的差异许多自动配置类会选择把ConditionalOnMissingBean直接放在Configuration类上而不是某个Bean方法上。这两种位置的行为有一点关键差异。放在类上时条件不成立的话整个配置类全跳过这个类里的所有Bean方法都不会处理。这适用于这一类配置的总开关语义是这个模块整体不启用。放在Bean方法上时只影响那一个方法同一个配置类里其他Bean照常注册。这适用于模块整体启用但其中一两个Bean允许被覆盖的组合。如果条件注解既放在类上、又放在单个方法上要用这个模式去管理嵌套场景。比如Configuration(proxyBeanMethods false) ConditionalOnProperty(name myapp.http.enabled, havingValue true) public class HttpClientAutoConfiguration { Bean ConditionalOnMissingBean(HttpClient.class) public HttpClient httpClient() { return new ApacheHttpClient(); } }类上的ConditionalOnProperty是模块总开关方法上的ConditionalOnMissingBean是Bean级覆盖点。这种两级结构在真实Starter里很常见。4.5 条件判断只看 BeanDefinition不负责保证 Bean 能创建成功最后一个容易忽略的点ConditionalOnMissingBean的条件通过不代表后续Bean方法里的对象一定能创建成功。它只是判断容器里目前缺不缺这个类型至于满足条件后Bean构造期间抛异常、初始化失败那是另外一套流程处理的事。常见翻车案例是自动配置里的默认实现依赖一个第三方库环境里没引入ConditionalOnClass应该拦在前面但配置类上只写了ConditionalOnMissingBean没写ConditionalOnClass。结果是条件判断通过进入Bean方法执行ClassNotFoundException当场炸出来。所以正确姿势往往是多个条件一起上各管一段Bean ConditionalOnMissingBean(RedisTemplate.class) ConditionalOnClass(name org.springframework.data.redis.core.RedisTemplate) public RedisTemplateString, Object defaultRedisTemplate() { // ... }这俩条件并不重复一个负责类路径有没有Redis依赖一个负责用户有没有自定义RedisTemplate缺一不可。5. 如何排查条件装配不生效5.1 开启条件评估日志看匹配结果如果条件注解某个Bean莫名其妙没生效第一步不是猜也不是打断点而是让Spring把决策过程打出来。在application.properties里加一行debugtrue启动日志里就会出现类似这样的信息 CONDITIONS EVALUATION REPORT Positive matches: ----------------- MessageConverterAutoConfiguration matched: - ConditionalOnClass found required classes ... Negative matches: ----------------- AuditAutoConfiguration: Did not match: - ConditionalOnMissingBean found bean kafkaAuditLoggerPositive matches是条件成立、配置类生效的部分Negative matches是条件不成立、配置被跳过的部分后面会跟着具体哪个条件没满足。绝大多数不知道Bean为什么没生效的问题在这一步就能看到答案。5.2 从 ConditionEvaluationReport 反推失败原因日志打出来只是第一步。拿到结果后要学会读门道。我看过太多人盯着Negative matches里那一行就完事了实际上这里还要分清是哪个条件不满足ConditionalOnClass did not find required class ...——类路径依赖缺失去补依赖。ConditionalOnMissingBean found bean xxx——容器里已经有目标Bean这是设计内行为不是Bug。ConditionalOnProperty matched但onBeanCondition没匹配——多个条件叠加时可能需要逐一确认。ConditionalOnWebApplication did not find web application classes——应用类型不匹配比如Web场景配置被非Web应用加载。还有一种情况配置类压根没出现在报告里。那问题不在条件判断而是AutoConfiguration.imports文件没写好、类没被扫描到、或者自动配置被exclude掉了。这种情况再去看启动类上的SpringBootApplication有没有排除逻辑以及依赖里有没有spring.factories外置排除。5.3 上报完整上下文而不是一句我用了条件注解没生效排查条件装配问题有一条经验很值钱问别人问题的时候尽量把debugtrue的启动日志一起贴出来。条件装配的结果和配置类顺序、容器刷新阶段、依赖结构都强相关光说一句我加了ConditionalOnMissingBean不管用别人离现场十万八千里根本没法帮你定位。我的排查习惯是先拉完整启动日志看这个Bean在Positive还是Negative看它的条件注解状态再定位到对应自动配置类看它的AutoConfigureBefore/AutoConfigureAfter顺序最后看定义的Bean类型是不是因为泛型、父类、接口实现等原因绕过了条件判断。从日志到源码再到运行态三步走基本能定位九成以上的问题。6. 结合热词的扩展思考条件装配在更复杂体系里的位置6.1 Spring Cloud 场景里条件装配配合父子容器的特殊性Spring Cloud Alibaba、Spring Cloud Gateway这些体系里一个进程里往往存在多个容器Bootstrap容器负责读取远程配置、主容器负责业务Bean、可能有子容器承载某个特殊上下文模块。这种多容器结构下ConditionalOnMissingBean的search策略会频繁派上用场。举个例子服务启动时要先从配置中心拉取数据源配置这个逻辑在Bootstrap阶段完成往Bootstrap容器里注册了一个DataSourceInitializer。主容器初始化时条件判断如果只用默认的CURRENT它看不到Bootstrap容器里的Bean就可能重复创建一个DataSourceInitializer初始化逻辑跑两遍。显式配置search SearchStrategy.ALL之后判断范围扩大能感知父容器状态避免重复执行。多容器还会放大顺序问题的复杂度。不同容器刷新是独立的过程条件判断在各自刷新阶段内执行不一定能感知另一个容器的最终状态。所以遇到微服务体系里的条件装配怪问题先画清楚容器层级再说条件注解判断得对不对。6.2 Spring Security 的自动配置里随处可见条件装配的影子Spring Security的默认登录页能开箱即用靠的也是条件装配。UserDetailsServiceAutoConfiguration里就是用ConditionalOnMissingBean判断用户没定义UserDetailsService就自动从配置内存出一个默认用户一旦用户自定义了默认的那个消失。这类框架级自动配置把默认能用但不挡路做成了标准范式。理解这一层以后再去看官方Starter源码会顺畅很多。你可以顺手打开spring-boot-autoconfigure包里几十个自动配置类几乎每个都是类上条件管依赖、方法上条件管覆盖、配置项条件管开关的组合拳。6.3 自定义 Starter 时条件注解是 API 设计的一部分很多人把ConditionalOnMissingBean当成内部实现细节这是低估它了。对使用者来说条件注解的可组合性本身就是Starter对外暴露的扩展点。用户第一次接你的Starter时不会读你的源码但他们一定会试我自定义一个Bean能不能覆盖你的默认实现我的配置项关掉功能还会不会生效所以设计阶段就应该把哪些Bean允许被覆盖、哪些不允许定清楚然后写进文档用条件注解落实。允许覆盖的用ConditionalOnMissingBean不允许覆盖、默认实现必须存在的不加条件注解或者用ConditionalOnProperty做总开关。场景不同选择不同没有一种是永远正确的写法。7. 我自己常用的几个判断套路最后分享几个我平时写条件装配时的固定套路这些算是我踩了不少坑之后沉淀下来的习惯。第一个默认实现的Bean方法上尽量把ConditionalOnMissingBean写成注解的目标类型而不是只写ConditionalOnMissingBean。后者默认按方法返回类型推断但当方法返回类型是接口、具体类型是子类时判断范围可能和你预期不一致。显式指定目标更可控Bean ConditionalOnMissingBean(MessageSender.class) public MessageSender messageSender() { return new RocketMessageSender(); }第二个多条件叠加时按依赖条件放上面、覆盖条件放下面的顺序排列。把ConditionalOnClass、ConditionalOnProperty写在前面ConditionalOnMissingBean写在最后读代码的人一眼就能分清哪些条件是环境约束、哪些条件是用户覆盖点。第三个启动日志里看一眼自动配置报告形成条件反射。每次新写一个自动配置类我至少会跑一次完整启动翻一遍Positive matches和Negative matches确认它不是碰巧没生效、也不是碰巧生效了。这个习惯能避免很多上线后才发现的问题。第四个条件注解只解决注册不注册不解决注入哪个。Autowired依赖注入时如果容器里存在同一类型的多个Bean仍然会报NoUniqueBeanDefinitionException。ConditionalOnMissingBean能尽量减少默认实现和用户实现并存的概率但并不能保证注入阶段百分之百没有歧义。真要稳妥配合Primary或者Qualifier一起用双保险。这些套路不会让你写出更炫的代码但它们能保证你的自动配置在真实项目里该生效时生效、该让路时让路少一些半夜排查的糟心事。条件的判断逻辑本身就是Spring容器生命周期里最微妙的一段搞懂它用起来比背一百遍文档要有用得多。