乐途乐途
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
  • 学习路径
  • 第1章 Spring概述与IoC容器

    • Spring概述与IoC容器
    • Spring Framework 概述
    • IoC 与 DI 核心概念
    • @Configuration 详解
    • @Component 详解
    • @ComponentScan 详解
    • @Import 详解
    • @Profile 详解
    • @PropertySource 详解
    • @Service 详解
    • @Repository 详解
  • 第2章 Bean的定义与依赖注入

    • Bean的定义与依赖注入
    • @Bean 详解
    • @Autowired 详解
    • @Qualifier 详解
    • @Primary 详解
    • @Resource 详解
    • @Inject 详解
    • @Named 详解
    • @Value 详解
    • @Scope 详解
    • @Lazy 详解
  • 第3章 Bean生命周期与作用域

    • Bean生命周期与作用域
    • Bean生命周期概述
    • @PostConstruct
    • @PreDestroy
    • InitializingBean
    • DisposableBean
    • BeanPostProcessor
    • BeanFactoryPostProcessor
  • 第4章 AOP面向切面编程

    • AOP面向切面编程
    • AOP核心概念
    • @EnableAspectJAutoProxy
    • @Aspect
    • @Pointcut
    • @Before
    • @After
    • @AfterReturning
    • @AfterThrowing
    • @Around
  • 第5章 数据访问与事务管理

    • 数据访问与事务管理
    • 数据访问概述
    • @EnableTransactionManagement
    • @Transactional
    • @Transactional 的传播行为
    • @Transactional 的隔离级别
    • @Transactional 的回滚规则
    • @Transactional 的超时与只读属性
    • @TransactionalEventListener
  • 第6章 Spring Boot自动配置基础

    • Spring Boot自动配置基础
    • @SpringBootApplication 注解
    • @EnableAutoConfiguration 注解
    • @ConfigurationProperties 注解
    • @ConditionalOnClass 注解
    • @ConditionalOnMissingBean 注解
    • @ConditionalOnProperty 注解
  • 第7章 从容器到Web: Spring MVC导引

    • Spring MVC 导引
  • 第8章 扩展阅读

    • 扩展阅读
    • Spring 事件机制 — ApplicationEvent / ApplicationListener
    • @EventListener
    • SpEL — Spring 表达式语言
    • 校验 Validation — JSR-303 / JSR-380 Bean Validation
    • 类型转换与数据绑定 — Converter / DataBinder
  • 附录

    • Spring Framework 专业术语
    • Spring 核心知识点
    • Spring 面试高频考点
    • Spring 核心注解速查表

@ConditionalOnMissingBean 注解

一句话定位:@ConditionalOnMissingBean 是 Spring Boot 自动配置的用户配置优先守卫。它确保:当开发者已经手动定义了某个 Bean 时,框架的自动配置会优雅退让,绝不覆盖用户的显式选择。这是"约定优于配置,但用户配置高于约定"哲学的最终体现。


定义与作用

@ConditionalOnMissingBean 检查当前 Spring 容器中是否已经存在指定类型或名称的 Bean。如果不存在,条件成立,当前配置类或 @Bean 方法会被执行;如果已存在,条件不成立,自动配置静默跳过。

这个注解是 Spring Boot 自动配置能够安全运行的最后一道防线。没有它,自动配置类注册的默认 Bean 可能会覆盖用户精心编写的自定义实现,导致难以排查的诡异行为。

在自动配置中的核心地位

自动配置类注册 Bean 的标准模式
  ↓
@ConditionalOnClass      → 类路径有依赖?
@ConditionalOnProperty   → 用户开启了功能?
@ConditionalOnMissingBean → 用户没自己配过?
  ↓ 全部通过 → 注册默认 Bean
  ↓ 任一失败 → 跳过,不注册

适用位置与常用属性

适用位置

@ConditionalOnMissingBean 可以标注在:

  1. 类级别:当容器中不存在某 Bean 时,整个配置类生效
  2. @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();
    }
}

常用属性

属性类型说明
valueClass<?>[]按类型检查,容器中不存在该类型的 Bean 时条件成立
nameString[]按名称检查,容器中不存在该名称的 Bean 时条件成立
typeString[]按全限定类名字符串检查(用于避免编译期依赖问题)
annotationClass<? extends Annotation>[]按注解检查,容器中不存在带该注解的 Bean 时条件成立
ignoredClass<?>[]检查时要忽略的 Bean 类型
ignoredTypeString[]检查时要忽略的 Bean 类型(字符串形式)
searchSearchStrategy搜索策略:CURRENT(仅当前上下文)、ANCESTORS(仅父上下文)、ALL(全部)

重要:value 和 type 是互斥的,通常使用 value(类型安全)。name 可以单独使用,也可以与 value 组合使用(要求同时满足"无该类型"且"无该名称")。


核心原理

容器 Bean 存在性检查流程

用户配置 vs 自动配置的优先级

执行时序:

  1. 用户配置类(如 @SpringBootApplication 所在包下的 @Configuration)通常先于自动配置类被加载
  2. 用户的 @Bean 方法先被注册为 BeanDefinition
  3. 自动配置类执行时,@ConditionalOnMissingBean 发现容器中已有同类型 Bean,于是退让
  4. 最终容器中只有用户自定义的 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: 【飞翔科技】同学您好...

分析:

  1. 场景 A:容器中不存在 SmsService 类型的 Bean,@ConditionalOnMissingBean 条件成立,defaultSmsService 被注册,业务代码使用第三方 SDK。
  2. 场景 B:FeixiangSmsConfig 先于 SmsAutoConfiguration 被加载,smsService Bean 已存在于容器中。@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,则两者都退让,使用用户实现。

上一页
@ConditionalOnClass 注解
下一页
@ConditionalOnProperty 注解