@ConditionalOnMissingBean 注解
一句话定位:
@ConditionalOnMissingBean是 Spring Boot 自动配置的用户配置优先守卫。它确保:当开发者已经手动定义了某个 Bean 时,框架的自动配置会优雅退让,绝不覆盖用户的显式选择。这是"约定优于配置,但用户配置高于约定"哲学的最终体现。
定义与作用
@ConditionalOnMissingBean 检查当前 Spring 容器中是否已经存在指定类型或名称的 Bean。如果不存在,条件成立,当前配置类或 @Bean 方法会被执行;如果已存在,条件不成立,自动配置静默跳过。
这个注解是 Spring Boot 自动配置能够安全运行的最后一道防线。没有它,自动配置类注册的默认 Bean 可能会覆盖用户精心编写的自定义实现,导致难以排查的诡异行为。
在自动配置中的核心地位
自动配置类注册 Bean 的标准模式
↓
@ConditionalOnClass → 类路径有依赖?
@ConditionalOnProperty → 用户开启了功能?
@ConditionalOnMissingBean → 用户没自己配过?
↓ 全部通过 → 注册默认 Bean
↓ 任一失败 → 跳过,不注册
适用位置与常用属性
适用位置
@ConditionalOnMissingBean 可以标注在:
- 类级别:当容器中不存在某 Bean 时,整个配置类生效
- @Bean 方法级别:当容器中不存在某 Bean 时,才注册该方法返回的 Bean
// 类级别:用户未自定义 DataSource 时,本配置才生效
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean(DataSource.class)
public class DefaultDataSourceConfiguration { ... }
// 方法级别:用户未自定义 RestTemplate 时,才注册默认实例
@Configuration
public class RestTemplateConfiguration {
@Bean
@ConditionalOnMissingBean(RestTemplate.class)
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
常用属性
| 属性 | 类型 | 说明 |
|---|---|---|
value | Class<?>[] | 按类型检查,容器中不存在该类型的 Bean 时条件成立 |
name | String[] | 按名称检查,容器中不存在该名称的 Bean 时条件成立 |
type | String[] | 按全限定类名字符串检查(用于避免编译期依赖问题) |
annotation | Class<? extends Annotation>[] | 按注解检查,容器中不存在带该注解的 Bean 时条件成立 |
ignored | Class<?>[] | 检查时要忽略的 Bean 类型 |
ignoredType | String[] | 检查时要忽略的 Bean 类型(字符串形式) |
search | SearchStrategy | 搜索策略:CURRENT(仅当前上下文)、ANCESTORS(仅父上下文)、ALL(全部) |
重要:
value和type是互斥的,通常使用value(类型安全)。name可以单独使用,也可以与value组合使用(要求同时满足"无该类型"且"无该名称")。
核心原理
容器 Bean 存在性检查流程
用户配置 vs 自动配置的优先级
执行时序:
- 用户配置类(如
@SpringBootApplication所在包下的@Configuration)通常先于自动配置类被加载 - 用户的
@Bean方法先被注册为BeanDefinition - 自动配置类执行时,
@ConditionalOnMissingBean发现容器中已有同类型 Bean,于是退让 - 最终容器中只有用户自定义的 Bean,自动配置的默认实现被完全屏蔽
完整示例
场景简述
飞翔科技公司的学生成绩管理系统需要发送短信通知。架构师白歌引入了一个第三方短信 SDK 的 Spring Boot Starter,该 Starter 会自动配置一个 SmsService Bean。但白歌同时要求:如果小崔需要对接公司自建的短信网关(而非第三方 SDK),必须能够用自己的实现覆盖 Starter 的默认实现,且不需要修改任何 Starter 的代码。
小崔需要验证 @ConditionalOnMissingBean 如何保障这种"用户优先"机制。
操作前:自动配置覆盖用户配置
假设第三方短信 Starter 的自动配置类没有使用 @ConditionalOnMissingBean:
// 操作前:第三方 Starter 的错误实现(无 @ConditionalOnMissingBean)
package com.thirdparty.sms.autoconfigure;
import com.thirdparty.sms.DefaultSmsService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class SmsAutoConfiguration {
@Bean // ← 没有 @ConditionalOnMissingBean!
public SmsService smsService() {
return new DefaultSmsService("third-party-api-key");
}
}
小崔在业务代码中自定义了公司短信网关的实现:
package com.feixiang.student.service;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class FeixiangSmsConfig {
@Bean
public SmsService smsService() {
return new FeixiangGatewaySmsService("https://sms.learnto.cn/api");
}
}
后果:Spring 容器启动时,两个 SmsService 类型的 Bean 定义同时存在(DefaultSmsService 和 FeixiangGatewaySmsService)。由于二者类型相同,Spring 在注入点 @Autowired SmsService 处抛出 NoUniqueBeanDefinitionException:
Caused by: org.springframework.beans.factory.NoUniqueBeanDefinitionException:
No qualifying bean of type 'com.feixiang.student.service.SmsService' available:
expected single matching bean but found 2: smsService, smsService
小崔被迫去修改第三方 Starter 的源码,或者在自己的 @Bean 上加 @Primary,但这都是不优雅的 workaround。
使用该注解的完整代码
第三方短信 Starter 的正确实现(使用 @ConditionalOnMissingBean):
package com.thirdparty.sms.autoconfigure;
import com.thirdparty.sms.DefaultSmsService;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(SmsService.class) // 类路径有短信 SDK 才加载
public class SmsAutoConfiguration {
@Bean
@ConditionalOnMissingBean(SmsService.class) // ← 关键:用户没配时才注册默认实现
public SmsService defaultSmsService() {
return new DefaultSmsService("third-party-api-key");
}
}
小崔的公司自定义实现保持不变:
package com.feixiang.student.service;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class FeixiangSmsConfig {
@Bean
public SmsService smsService() {
return new FeixiangGatewaySmsService("https://sms.learnto.cn/api");
}
}
短信接口定义:
package com.feixiang.student.service;
public interface SmsService {
void send(String phone, String message);
}
默认实现(第三方 SDK):
package com.thirdparty.sms;
import com.feixiang.student.service.SmsService;
public class DefaultSmsService implements SmsService {
private final String apiKey;
public DefaultSmsService(String apiKey) {
this.apiKey = apiKey;
}
@Override
public void send(String phone, String message) {
System.out.println("[第三方SDK] 发送短信至 " + phone + ": " + message);
}
}
公司网关实现:
package com.feixiang.student.service;
public class FeixiangGatewaySmsService implements SmsService {
private final String gatewayUrl;
public FeixiangGatewaySmsService(String gatewayUrl) {
this.gatewayUrl = gatewayUrl;
}
@Override
public void send(String phone, String message) {
System.out.println("[乐途网关] 调用 " + gatewayUrl + " 发送短信至 " + phone + ": " + message);
}
}
业务代码中注入使用:
package com.feixiang.student.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class NotificationService {
private final SmsService smsService;
@Autowired
public NotificationService(SmsService smsService) {
this.smsService = smsService;
}
public void notifyScorePublished(Long studentId, String phone) {
String message = "【飞翔科技】同学您好,您的期末成绩已发布,请登录系统查看。";
smsService.send(phone, message);
}
}
操作后运行结果及分析
场景 A:小崔未自定义 SmsService(使用 Starter 默认实现)
2024-05-20 11:00:15.234 INFO 12345 --- [main] c.t.s.a.SmsAutoConfiguration :
defaultSmsService matched:
@ConditionalOnMissingBean (types: SmsService; SearchStrategy: all) did not find any beans
2024-05-20 11:00:15.567 INFO 12345 --- [main] c.f.s.service.NotificationService :
[第三方SDK] 发送短信至 13800138000: 【飞翔科技】同学您好,您的期末成绩已发布...
场景 B:小崔自定义了 SmsService(使用公司网关)
2024-05-20 11:05:22.123 INFO 12345 --- [main] c.t.s.a.SmsAutoConfiguration :
defaultSmsService did not match:
@ConditionalOnMissingBean (types: SmsService; SearchStrategy: all)
found: feixiangSmsConfig.smsService
2024-05-20 11:05:22.456 INFO 12345 --- [main] c.f.s.service.NotificationService :
[乐途网关] 调用 https://sms.learnto.cn/api 发送短信至 13800138000: 【飞翔科技】同学您好...
分析:
- 场景 A:容器中不存在
SmsService类型的 Bean,@ConditionalOnMissingBean条件成立,defaultSmsService被注册,业务代码使用第三方 SDK。 - 场景 B:
FeixiangSmsConfig先于SmsAutoConfiguration被加载,smsServiceBean 已存在于容器中。@ConditionalOnMissingBean检测到SmsService已存在,条件不成立,defaultSmsService被跳过。业务代码自动使用公司网关,无需任何额外配置。
易错场景与面试考点
易错场景一:Bean 名称与类型检查混淆
小崔试图确保容器中不存在名为 dataSource 的 Bean 时才注册:
// 错误示范:意图检查名称,却用了 value
@Bean
@ConditionalOnMissingBean(value = DataSource.class) // ← 这是按类型检查!
public DataSource dataSource() { ... }
后果:如果用户定义了一个名为 myDataSource、类型为 HikariDataSource 的 Bean,@ConditionalOnMissingBean(DataSource.class) 仍然会发现 DataSource 类型的 Bean 已存在(因为 HikariDataSource 继承自 DataSource),于是跳过。但小崔的真实意图可能是"只要没有名为 dataSource 的 Bean 就注册"。
正确做法:按名称检查应使用 name 属性:
@Bean
@ConditionalOnMissingBean(name = "dataSource") // 按名称检查
public DataSource dataSource() { ... }
或者同时约束类型和名称:
@Bean
@ConditionalOnMissingBean(value = DataSource.class, name = "dataSource")
public DataSource dataSource() { ... }
易错场景二:搜索策略不当导致父上下文误判
在 Spring MVC 应用中,ApplicationContext 存在层级结构:Root WebApplicationContext(父)和 Servlet WebApplicationContext(子)。
// 错误示范:在子上下文中检查,却搜索了父上下文
@Bean
@ConditionalOnMissingBean(value = SmsService.class, search = SearchStrategy.ALL)
public SmsService smsService() { ... }
后果:如果父上下文(Root Context)中已经定义了 SmsService(例如由 @SpringBootApplication 扫描得到),SearchStrategy.ALL 会搜索到父上下文中的 Bean,导致条件不成立,子上下文中的自动配置被跳过。但小崔可能期望子上下文有自己的独立配置。
正确做法:在子上下文中应使用 SearchStrategy.CURRENT,仅检查当前上下文:
@Bean
@ConditionalOnMissingBean(value = SmsService.class, search = SearchStrategy.CURRENT)
public SmsService smsService() { ... }
面试考点
Q:@ConditionalOnMissingBean 和 @ConditionalOnBean 有什么区别?
@ConditionalOnMissingBean在不存在指定 Bean 时条件成立,用于自动配置的"默认退让";@ConditionalOnBean在存在指定 Bean 时条件成立,用于自动配置之间的依赖(例如"只有存在 DataSource 时才注册 JdbcTemplate")。二者是互逆逻辑。
Q:为什么 @ConditionalOnMissingBean 通常放在自动配置类上,而不是用户配置类上?
因为自动配置类由框架提供,用户无法修改其源码。
@ConditionalOnMissingBean让框架能够感知用户的自定义配置并主动退让。如果放在用户配置类上,则变成了用户代码去感知框架,违背了"用户配置优先"的设计原则。
Q:@ConditionalOnMissingBean 的判断时机是什么时候?如果用户 Bean 和自动配置 Bean 同时定义,谁优先?
判断发生在 BeanDefinition 注册阶段。Spring Boot 的自动配置类通过
DeferredImportSelector机制延迟加载,确保用户配置类(由@ComponentScan扫描)先于自动配置类被注册。因此用户的@Bean总是先进入容器,自动配置后执行时@ConditionalOnMissingBean能正确检测到用户 Bean 的存在。
Q:能否用 @ConditionalOnMissingBean 实现策略模式的自动选择?
可以。例如定义
StorageService接口,提供LocalStorageService和OssStorageService两个实现。在默认配置类上用@ConditionalOnMissingBean(StorageService.class)注册LocalStorageService;在 OSS 配置类上用@ConditionalOnProperty+@ConditionalOnClass注册OssStorageService。如果用户自定义了StorageService,则两者都退让,使用用户实现。