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:
@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 类 | 评估阶段 | 判断依据 | 典型用途 |
|---|---|---|---|---|
@ConditionalOnClass | OnClassCondition | PARSE_CONFIGURATION | ClassLoader 能否加载到指定类 | 根据依赖 jar 是否存在启用配置 |
@ConditionalOnMissingClass | OnClassCondition | PARSE_CONFIGURATION | 同上,取反 | 降级方案 |
@ConditionalOnBean | OnBeanCondition | REGISTER_BEAN | BeanFactory 中已有指定 BeanDefinition | 依赖其他 Bean 存在时才生效 |
@ConditionalOnMissingBean | OnBeanCondition | REGISTER_BEAN | 同上,取反 | 默认实现兜底 |
@ConditionalOnProperty | OnPropertyCondition | PARSE_CONFIGURATION | Environment 中配置值 | 开关控制 |
@ConditionalOnResource | OnResourceCondition | PARSE_CONFIGURATION | classpath 资源文件是否存在 | 发现配置文件 |
@ConditionalOnWebApplication | OnWebApplicationCondition | PARSE_CONFIGURATION | WebApplicationType 判断 | 区分 Web/非 Web 环境 |
@ConditionalOnExpression | OnExpressionCondition | PARSE_CONFIGURATION | SpEL 表达式求值 | 复杂逻辑组合 |
@ConditionalOnJava | OnJavaCondition | PARSE_CONFIGURATION | Java 版本 | 兼容旧版本 |
@ConditionalOnJndi | OnJndiCondition | PARSE_CONFIGURATION | JNDI 资源是否存在 | 传统 Java EE 迁移 |
关键点:评估阶段不同导致组合使用时行为不一致。PARSE 阶段的条件在配置类解析时执行,REGISTER_BEAN 阶段在 BeanDefinition 全部注册后执行。如果两个阶段的条件混用,先跳过的条件不会影响后一个条件的执行——但后一个条件的判断结果可能因为前一个跳过而变得不可预期。
评估时机:一个容易被忽略的细节
条件注解的评估发生在 ConfigurationClassPostProcessor.processConfigBeanDefinitions() 阶段。这个阶段在 refresh() 的 invokeBeanFactoryPostProcessors 步骤中触发,此时:
- ✅ Environment 已经就绪(可以读取配置值)
- ✅ BeanDefinition 正在注册中
- ❌ Bean 实例尚未创建
- ❌ 依赖注入尚未开始
这意味着 @ConditionalOnBean / @ConditionalOnMissingBean 和 @ConditionalOnClass 的评估时机不同。@ConditionalOnClass 在 BeanDefinition 注册时执行,而 @ConditionalOnBean 在注册后评估,组合使用可能导致预期外的跳过。
常见条件注解的实现分析
@ConditionalOnClass
底层是 OnClassCondition.matches():
// 简化版逻辑
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():
// 简化版逻辑
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 或者确保配置存在。
第二个坑:prefix 和 name 拼接时,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 容器状态,后者依赖类加载器——两者完全不同的维度
进阶用法:@ConditionalOnBean 的 parameterizedContainer 属性。Spring Boot 2.5+ 支持检查泛型容器 Bean 中的具体类型:
// 检查容器中是否有 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。
自定义一个条件注解
回到开头的场景:需要根据数据源驱动是否存在来自动切换配置类。
第一步:定义条件注解
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(OnDataSourceCondition.class)
public @interface ConditionalOnDataSource {
String value(); // 驱动类名,如 "com.mysql.cj.jdbc.Driver"
}第二步:实现 Condition 接口
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;
}
}
}第三步:在配置类中使用
@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():
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 接口解决了这个问题:
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 阶段:
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 一起用时会出问题。
@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 的关系
@Configuration
@AutoConfigureAfter(SomeOtherConfig.class)
@ConditionalOnBean(SomeBean.class)
public class MyConfig {
@Bean
public MyService myService() {
return new MyService();
}
}@AutoConfigureAfter 只保证 SomeOtherConfig 在 MyConfig 之前解析,但不保证 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(任一子条件满足):
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=true 或 my.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 控制:
@AutoConfiguration
@AutoConfigureAfter(DataSourceAutoConfiguration.class)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
public class MyCustomDataSourceConfig {
// 在 DataSourceAutoConfiguration 之后解析
// 优先级比默认高 10 个级别
}注意:@AutoConfigureOrder 的默认值是 Ordered.LOWEST_PRECEDENCE(Integer.MAX_VALUE),值越小优先级越高。@AutoConfigureBefore 和 @AutoConfigureAfter 只影响解析顺序,不影响条件评估阶段。
面试题:Spring Boot 如何保证只创建一个 DataSource?
- 类级别
@ConditionalOnClass过滤掉没有对应依赖 jar 的配置 - 方法级别
@ConditionalOnMissingBean(DataSource.class)保证第一个创建后后面的都跳过 - 通过
spring.autoconfigure.exclude或@EnableAutoConfiguration(exclude=...)手动排除 - 通过
AutoConfiguration.imports的排序机制 +@AutoConfigureOrder控制解析优先级
排查条件注解不生效的调试方法
生产环境下条件注解不生效,最直接的办法是开启 debug 日志:
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)排错步骤:
- 看 Negative matches 中有没有你的配置类,以及为什么没匹配
- 确认条件注解的评估阶段是否正确(PARSE vs REGISTER_BEAN)
- 检查
@AutoConfigureAfter依赖的配置类是否被其他条件跳过了 - 检查 Environment 中配置值是否被覆盖(
spring.profiles.active加载顺序) - 如果用了
@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 创建之间的常见误解来源