Skip to content

Spring 条件注解(@Conditional 家族)的实现与自定义

遇到过的真实场景

先从一个真实的线上问题说起。

团队接手了一个老项目,想在 Spring Boot 应用里同时兼容 MySQL 和 PostgreSQL 两套数据源——根据部署环境自动切换。最直接的想法是用 @Profile("mysql")@Profile("postgres") 来区分,但 Profile 是全局的,没办法做到"某个 Bean 只在 MySQL 驱动存在时才生效,另一个 Bean 只在 PostgreSQL 驱动存在时才生效"。更麻烦的是,有些自动配置类在类路径缺少某个依赖时直接报错启动,根本没有"优雅降级"的机会。

另一个场景来自生产事故:Spring Boot 2.3 升级到 2.7 时,DataSourceAutoConfiguration 因为 classpath 同时存在 HikariCP 和 Tomcat 连接池,两个 DataSource 的 Bean 都创建了,导致启动报 NoUniqueBeanDefinitionException。排查半天发现是 @ConditionalOnClass 在两个配置类上都匹配了,但 @ConditionalOnMissingBean 的评估顺序导致一个没生效。

第三个场景:某次灰度发布,在 application-gray.yml 里配置了 feature.new-payment=true,但 @ConditionalOnProperty 读到的是 application.yml 里的 false。排查发现 spring.profiles.active 加载顺序导致配置覆盖反了——application.yml 晚于 application-gray.yml 加载,直接把灰度配置覆盖了。这跟条件注解本身没关系,但让我深刻理解了 Environment 中的配置值来源优先级 对条件判断的影响。

这时就需要用到 Spring 的 条件注解(@Conditional 家族)

条件注解的底层原理

Condition 接口:一切条件判断的根基

Spring 的条件机制核心只有一个接口 —— Condition

java
@FunctionalInterface
public interface Condition {
    boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);
}

matches() 返回 true 表示条件满足,Bean 会被注册;返回 false 则跳过。

每个 @ConditionalXXX 注解背后都标注了 @Conditional({XXXCondition.class}),指定一个或多个 Condition 实现类。Spring 容器在加载这些 BeanDefinition 时,会调用对应的 matches() 方法做判断。

条件评估的完整流程(时序图)

SpringApplication.run()

  ├─ prepareEnvironment()          ← Environment 就绪
  ├─ createApplicationContext()
  │     └─ refresh()
  │           └─ invokeBeanFactoryPostProcessors()
  │                 └─ ConfigurationClassPostProcessor.processConfigBeanDefinitions()
  │                       │
  │                       ├─ [1] 解析配置类上的 @Conditional
  │                       │      └─ ConditionEvaluator.shouldSkip()
  │                       │            ├─ 获取注解上的 @Conditional({Condition.class})
  │                       │            ├─ 实例化 Condition 实现类
  │                       │            ├─ 调用 condition.matches()
  │                       │            │     ├─ Phase.PARSE_CONFIGURATION → 立即执行
  │                       │            │     └─ Phase.REGISTER_BEAN → 跳过,留到注册阶段
  │                       │            └─ 返回 true/false
  │                       │
  │                       ├─ [2] 如果跳过,移除该配置类的 BeanDefinition
  │                       │
  │                       └─ [3] 未被跳过的配置类 → 注册其中的 BeanDefinition 定义
  │                             └─ 再次调用 ConditionEvaluator.shouldSkip()
  │                                   └─ 这次执行 Phase.REGISTER_BEAN 的条件

  └─ finishBeanFactoryInitialization()  ← Bean 实例化(跟条件无关了)

关键结论:PARSE 阶段的条件可以阻止整个配置类被解析,所以它的 BeanDefinition 都不会注册到容器。REGISTER_BEAN 阶段的条件在 BeanDefinition 已经注册后再筛一遍,但此时该配置类的其他 BeanDefinition 已经注册了——这导致跨阶段条件互相影响的问题。

条件注解全家族对比

注解底层 Condition 类评估阶段判断依据典型用途
@ConditionalOnClassOnClassConditionPARSE_CONFIGURATIONClassLoader 能否加载到指定类根据依赖 jar 是否存在启用配置
@ConditionalOnMissingClassOnClassConditionPARSE_CONFIGURATION同上,取反降级方案
@ConditionalOnBeanOnBeanConditionREGISTER_BEANBeanFactory 中已有指定 BeanDefinition依赖其他 Bean 存在时才生效
@ConditionalOnMissingBeanOnBeanConditionREGISTER_BEAN同上,取反默认实现兜底
@ConditionalOnPropertyOnPropertyConditionPARSE_CONFIGURATIONEnvironment 中配置值开关控制
@ConditionalOnResourceOnResourceConditionPARSE_CONFIGURATIONclasspath 资源文件是否存在发现配置文件
@ConditionalOnWebApplicationOnWebApplicationConditionPARSE_CONFIGURATIONWebApplicationType 判断区分 Web/非 Web 环境
@ConditionalOnExpressionOnExpressionConditionPARSE_CONFIGURATIONSpEL 表达式求值复杂逻辑组合
@ConditionalOnJavaOnJavaConditionPARSE_CONFIGURATIONJava 版本兼容旧版本
@ConditionalOnJndiOnJndiConditionPARSE_CONFIGURATIONJNDI 资源是否存在传统 Java EE 迁移

关键点:评估阶段不同导致组合使用时行为不一致。PARSE 阶段的条件在配置类解析时执行,REGISTER_BEAN 阶段在 BeanDefinition 全部注册后执行。如果两个阶段的条件混用,先跳过的条件不会影响后一个条件的执行——但后一个条件的判断结果可能因为前一个跳过而变得不可预期。

评估时机:一个容易被忽略的细节

条件注解的评估发生在 ConfigurationClassPostProcessor.processConfigBeanDefinitions() 阶段。这个阶段在 refresh()invokeBeanFactoryPostProcessors 步骤中触发,此时:

  • ✅ Environment 已经就绪(可以读取配置值)
  • ✅ BeanDefinition 正在注册中
  • ❌ Bean 实例尚未创建
  • ❌ 依赖注入尚未开始

这意味着 @ConditionalOnBean / @ConditionalOnMissingBean 和 @ConditionalOnClass 的评估时机不同。@ConditionalOnClass 在 BeanDefinition 注册时执行,而 @ConditionalOnBean 在注册后评估,组合使用可能导致预期外的跳过。

常见条件注解的实现分析

@ConditionalOnClass

底层是 OnClassCondition.matches()

java
// 简化版逻辑
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
    MultiValueMap<String, Object> attributes = metadata.getAllAnnotationAttributes(
        ConditionalOnClass.class.getName());
    String[] classNames = (String[]) attributes.getFirst("value");
    for (String className : classNames) {
        if (!ClassUtils.isPresent(className, context.getClassLoader())) {
            return false;  // 类不存在,跳过
        }
    }
    return true;
}

ClassUtils.isPresent() 通过 ClassLoader.loadClass() 判断类是否存在,但不会初始化类,避免触发不必要的静态代码块。

面试追问点ClassUtils.isPresent()Class.forName() 有什么不同?前者只调 loadClass(),不会执行静态初始化块;后者默认会初始化。如果用 Class.forName() 做条件判断,可能因为触发静态代码块中的副作用导致问题。真实案例:某项目用 Class.forName("com.mysql.cj.jdbc.Driver") 做条件判断,结果 mysql-connector-java 的静态初始化块里注册了 JDBC 驱动,导致 java.sql.DriverManager 里多了一个已注册的驱动实例,DataSource 初始化时出现诡异的行为。

@ConditionalOnProperty

底层是 OnPropertyCondition.matches()

java
// 简化版逻辑
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
    MultiValueMap<String, Object> attributes = metadata.getAllAnnotationAttributes(
        ConditionalOnProperty.class.getName());
    String prefix = (String) attributes.getFirst("prefix");
    String name = (String) attributes.getFirst("name");
    String havingValue = (String) attributes.getFirst("havingValue");
    boolean matchIfMissing = (boolean) attributes.getFirst("matchIfMissing");
    
    String value = context.getEnvironment().getProperty(prefix + "." + name);
    if (value == null) {
        return matchIfMissing;
    }
    return havingValue.isEmpty() || havingValue.equals(value);
}

生产踩坑matchIfMissing 默认是 false。很多人写 @ConditionalOnProperty(name = "my.feature.enabled", havingValue = "true"),然后忘记在 application.yml 里配置。结果就是 matchIfMissing=false,配置不存在时条件不满足,Bean 没创建,调试半天才发现。正确做法是加 matchIfMissing = true 或者确保配置存在。

第二个坑prefixname 拼接时,OnPropertyCondition 会拼成 prefix + "." + name。如果 prefix 为空字符串,结果就是 ".name"(前面多个点),导致 Environment.getProperty() 找不到。prefix 为空的正确写法是直接用 name 属性带上完整键名,或者把 prefix 留空并用 name = "my.feature.enabled"

@ConditionalOnBean

这个注解需要特别注意 —— 它在 ConditionEvaluator 中评估时,底层会调用 ListableBeanFactory.getBeanNamesForType() 去检查容器中是否已有指定类型的 Bean。但它只检查已经注册的 BeanDefinition,不包括还没注册的。所以如果两个自动配置类相互依赖,很容易出现"明明有这个类,但条件判断为 false"的情况。

面试高频题@ConditionalOnBean@ConditionalOnClass 的区别?

  • @ConditionalOnBean 在 REGISTER_BEAN 阶段评估,判断的是 BeanDefinition 是否已注册到容器
  • @ConditionalOnClass 在 PARSE_CONFIGURATION 阶段评估,判断的是 classpath 上是否有这个类
  • 前者依赖 Spring 容器状态,后者依赖类加载器——两者完全不同的维度

进阶用法@ConditionalOnBeanparameterizedContainer 属性。Spring Boot 2.5+ 支持检查泛型容器 Bean 中的具体类型:

java
// 检查容器中是否有 Repository<Order> 类型的 Bean(而不是 Repository 类型)
@ConditionalOnBean(
    value = OrderRepository.class,
    parameterizedContainer = Repository.class
)

这个特性在有多层泛型抽象时非常有用——比如你有一个 BaseRepository<T> 接口,子类 OrderRepository extends BaseRepository<Order>UserRepository extends BaseRepository<User>,用 @ConditionalOnBean(OrderRepository.class) 就够用了,但如果你想检查"是否存在一个 BaseRepository<Order> 类型的 Bean",就需要 parameterizedContainer

自定义一个条件注解

回到开头的场景:需要根据数据源驱动是否存在来自动切换配置类。

第一步:定义条件注解

java
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(OnDataSourceCondition.class)
public @interface ConditionalOnDataSource {
    String value();  // 驱动类名,如 "com.mysql.cj.jdbc.Driver"
}

第二步:实现 Condition 接口

java
public class OnDataSourceCondition implements Condition {
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        Map<String, Object> attributes = metadata.getAnnotationAttributes(
            ConditionalOnDataSource.class.getName());
        String driverClassName = (String) attributes.get("value");
        try {
            Class.forName(driverClassName);
            return true;
        } catch (ClassNotFoundException e) {
            return false;
        }
    }
}

第三步:在配置类中使用

java
@Configuration
public class DataSourceAutoConfiguration {
    
    @Bean
    @ConditionalOnDataSource("com.mysql.cj.jdbc.Driver")
    public DataSource mysqlDataSource() {
        return DataSourceBuilder.create()
            .driverClassName("com.mysql.cj.jdbc.Driver")
            .url("jdbc:mysql://localhost:3306/db?useSSL=false&serverTimezone=Asia/Shanghai")
            .username("root")
            .password("root")
            .build();
    }
    
    @Bean
    @ConditionalOnDataSource("org.postgresql.Driver")
    public DataSource postgresDataSource() {
        return DataSourceBuilder.create()
            .driverClassName("org.postgresql.Driver")
            .url("jdbc:postgresql://localhost:5432/db?ApplicationName=myapp")
            .username("postgres")
            .password("postgres")
            .build();
    }
}

这样,部署时只需要在 classpath 放对应的 JDBC 驱动 jar,系统就会自动选择对应的数据源配置,无需修改代码或配置。

自定义条件注解的最佳实践

踩坑:上面用 Class.forName() 来检查驱动类存在,但前面说了 Class.forName() 会触发静态初始化块。对于 JDBC 驱动,DriverManager.registerDriver() 会在静态初始化块中被调用,虽然这不影响这段代码的判断逻辑,但会在 DriverManager 中注册一个重复的驱动实例。标准做法应该是用 ClassUtils.isPresent()

java
public class OnDataSourceCondition implements Condition {
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        Map<String, Object> attributes = metadata.getAnnotationAttributes(
            ConditionalOnDataSource.class.getName());
        String driverClassName = (String) attributes.get("value");
        // 用 ClassUtils.isPresent 替代 Class.forName,不触发静态初始化
        return ClassUtils.isPresent(driverClassName, context.getClassLoader());
    }
}

进阶:ConfigurationCondition 接口

普通的 Condition 接口只有一个 matches() 方法,Spring 容器会在 解析阶段(PARSE_CONFIGURATION) 调用它。但有时候你的条件依赖于其他 Bean 是否已经注册(而不是是否在类路径上),这时就需要在注册阶段(REGISTER_BEAN)评估。

ConfigurationCondition 接口解决了这个问题:

java
public interface ConfigurationCondition extends Condition {
    ConfigurationPhase getConfigurationPhase();
    
    enum ConfigurationPhase {
        PARSE_CONFIGURATION,   // 配置类解析阶段
        REGISTER_BEAN          // Bean 注册阶段
    }
}

比如 @ConditionalOnBean 的内部实现就实现了 ConfigurationCondition 并返回 REGISTER_BEAN,因为只有等到所有配置类解析完毕、BeanDefinition 都注册了之后,才能判断"某个 Bean 是否已存在"。

自定义 ConfigurationCondition 的实战场景

假设你需要一个条件:"某个配置类只在另一个配置类被跳过时才生效"。这需要判断另一个配置类的 BeanDefinition 是否存在,但普通 Condition 在 PARSE 阶段无法得知另一个配置类是否被跳过(因为跳过发生在同一个阶段且顺序不确定)。必须用 ConfigurationCondition 并指定 REGISTER_BEAN 阶段:

java
public class OnConfigSkippedCondition implements ConfigurationCondition {
    @Override
    public ConfigurationPhase getConfigurationPhase() {
        return ConfigurationPhase.REGISTER_BEAN;
    }
    
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        // 在 REGISTER_BEAN 阶段,所有 PARSE 阶段的条件已经执行完毕
        // 可以通过检查某些 BeanDefinition 是否存在来判断另一配置是否被跳过
        return !context.getBeanFactory().containsBeanDefinition("fallbackDataSource");
    }
}

条件注解的优先级和冲突处理

实际项目中踩过的一个坑:@ConditionalOnClass 和 @ConditionalOnBean 一起用时会出问题

java
@Configuration
@ConditionalOnClass(name = "com.example.MyService")
@ConditionalOnBean(SomeBean.class)
public class MyAutoConfiguration {
    // 期望:MyService 在类路径上 且 SomeBean 已注册时才生效
}

问题是 @ConditionalOnClass 在 PARSE 阶段评估,而 @ConditionalOnBean 在 REGISTER_BEAN 阶段评估。如果 MyAutoConfiguration 本身被 @ConditionalOnClass 跳过了,@ConditionalOnBean 根本不会执行。但反过来,如果 @ConditionalOnClass 通过但 @ConditionalOnBean 没通过,容器也不会报错——只是默默地不创建这个配置类中的 Bean。

正确的做法:要么只用 @ConditionalOnBean,要么把类路径判断用 @ConditionalOnClass 挪到方法级别,配合 @ConditionalOnBean 在类级别使用。

另一个常见踩坑:ConditionalOnBean 和 @Order 的关系

java
@Configuration
@AutoConfigureAfter(SomeOtherConfig.class)
@ConditionalOnBean(SomeBean.class)
public class MyConfig {
    @Bean
    public MyService myService() {
        return new MyService();
    }
}

@AutoConfigureAfter 只保证 SomeOtherConfigMyConfig 之前解析,但不保证 SomeOtherConfig 定义的 Bean 在 MyConfig 的条件评估时已经注册。如果 SomeOtherConfig 被跳过,MyConfig 的条件也失效。这才是 spring.factories 中 auto-configuration 排序的真正目的——不是保证 Bean 创建顺序,而是保证 BeanDefinition 注册顺序

生产事故:@ConditionalOnBean 和 @ConditionalOnMissingBean 同时存在导致死循环

Spring Boot 2.3 遇到过这样一个 case:某个自动配置类上同时有 @ConditionalOnBean(DataSource.class) 和类内部另一个 @Bean 方法上有 @ConditionalOnMissingBean(DataSource.class)。逻辑上看起来是"如果已经有 DataSource 则启用此配置,但如果还没有则创建默认 DataSource"——但实际运行时,两个条件都在,类级别的 @ConditionalOnBean 判断为 false 导致整个配置类被跳过,内部的 @ConditionalOnMissingBean 根本不会执行。最终 DataSource 一个都没创建,应用启动失败。

正确做法是拆成两个配置类,或者用 @Profile@ConditionalOnProperty 组合来做。

NestedConditions:组合多个条件

Spring Boot 提供了 NestedConditions 基类,支持 AllNestedConditions(所有子条件都满足)和 AnyNestedConditions(任一子条件满足):

java
public class OnMyFeatureCondition extends AnyNestedConditions {
    // 必须指定配置阶段
    public OnMyFeatureCondition() {
        super(ConfigurationPhase.PARSE_CONFIGURATION);
    }
    
    @ConditionalOnProperty(name = "my.feature.v1.enabled", havingValue = "true")
    static class UseV1 {}

    @ConditionalOnProperty(name = "my.feature.v2.enabled", havingValue = "true")
    static class UseV2 {}
}

这样,只要 my.feature.v1.enabled=truemy.feature.v2.enabled=true 任一满足,条件就通过。这个模式比在 @ConditionalOnExpression 里写复杂的 SpEL 表达式清晰得多,也更容易测试。

Spring Boot 自动配置中的条件注解实战

Spring Boot 的 spring-boot-autoconfigure 模块大量使用了条件注解。以 DataSourceAutoConfiguration 为例,它的 @ConditionalOnClass 链长这样:

DataSourceAutoConfiguration
  @ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class })
  → 只在 classpath 有 javax.sql.DataSource 时才加载
  → 内部调用 Hikari、Tomcat、DBCP2 三个池化配置类
  → 每个池化配置类上都有 @ConditionalOnClass(具体连接池类)
  → 同时在 @Bean 方法上有 @ConditionalOnMissingBean(DataSource.class)
  → 保证只有一个 DataSource 被创建

AutoConfiguration.imports 的排序机制

Spring Boot 2.7+ 用 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 替代了 spring.factories 中的自动配置声明。排序通过 @AutoConfigureBefore@AutoConfigureAfter@AutoConfigureOrder 控制:

java
@AutoConfiguration
@AutoConfigureAfter(DataSourceAutoConfiguration.class)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
public class MyCustomDataSourceConfig {
    // 在 DataSourceAutoConfiguration 之后解析
    // 优先级比默认高 10 个级别
}

注意@AutoConfigureOrder 的默认值是 Ordered.LOWEST_PRECEDENCEInteger.MAX_VALUE),值越小优先级越高。@AutoConfigureBefore@AutoConfigureAfter 只影响解析顺序,不影响条件评估阶段

面试题:Spring Boot 如何保证只创建一个 DataSource?

  1. 类级别 @ConditionalOnClass 过滤掉没有对应依赖 jar 的配置
  2. 方法级别 @ConditionalOnMissingBean(DataSource.class) 保证第一个创建后后面的都跳过
  3. 通过 spring.autoconfigure.exclude@EnableAutoConfiguration(exclude=...) 手动排除
  4. 通过 AutoConfiguration.imports 的排序机制 + @AutoConfigureOrder 控制解析优先级

排查条件注解不生效的调试方法

生产环境下条件注解不生效,最直接的办法是开启 debug 日志:

yaml
logging:
  level:
    org.springframework.boot.autoconfigure: DEBUG

启动后能看到类似这样的输出:

2026-07-23 14:00:00.123 DEBUG 12345 --- [main] o.s.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener :
============================
CONDITIONS EVALUATION REPORT
============================
Positive matches:
-----------------
   DataSourceAutoConfiguration matched:
      - @ConditionalOnClass found required class 'javax.sql.DataSource' (OnClassCondition)
      - @ConditionalOnProperty (spring.datasource.type) matched (OnPropertyCondition)

Negative matches:
-----------------
   DataSourceJmxConfiguration:
      - @ConditionalOnBean (types: javax.sql.DataSource; SearchStrategy: all) did not find any beans (OnBeanCondition)

排错步骤

  1. 看 Negative matches 中有没有你的配置类,以及为什么没匹配
  2. 确认条件注解的评估阶段是否正确(PARSE vs REGISTER_BEAN)
  3. 检查 @AutoConfigureAfter 依赖的配置类是否被其他条件跳过了
  4. 检查 Environment 中配置值是否被覆盖(spring.profiles.active 加载顺序)
  5. 如果用了 @ConditionalOnBean,确认目标 Bean 是否在同一个配置类中定义——同一个配置类中的 Bean 条件评估时可能还没注册

总结

  • 条件注解的核心是 Condition 接口 + @Conditional 元注解
  • 评估时机分两个阶段:PARSE_CONFIGURATION(配置解析)和 REGISTER_BEAN(Bean 注册),理解这个区别才能避免条件组合的坑
  • 10 个常用条件注解各自有不同的判断依据和评估阶段,面试能说出 8 个以上就算过关
  • @ConditionalOnClass 通过 ClassUtils.isPresent() 判断类是否存在,不加载类
  • @ConditionalOnProperty 读取 Environment 中的配置值,支持 matchIfMissing 兜底,但默认不兜底
  • matchIfMissing 默认 false 是一个常见陷阱,很多人忘记配置后 Bean 不创建
  • 自定义条件注解三步走:定义注解 → 实现 Condition → 在配置类上使用
  • 条件注解依赖评估顺序,组合使用时如果发现 Bean 未被创建,先检查评估阶段是否匹配
  • ConfigurationCondition 解决了跨阶段评估的问题,@ConditionalOnBean 等注解依赖它实现 REGISTER_BEAN 阶段判断
  • NestedConditions / AllNestedConditions / AnyNestedConditions 用于组合多个条件,替代复杂的 SpEL 表达式
  • 生产排错用 logging.level.org.springframework.boot.autoconfigure=DEBUG 看 ConditionEvaluationReport
  • @AutoConfigureAfter 只保证解析顺序,不保证 Bean 注册顺序——这是条件评估和 Bean 创建之间的常见误解来源

手撕 → 框架 → 生产化,一步步把 AI Agent 工程化搞透。