前言
在构建 Spring Boot 应用时,我们经常会遇到这样的场景:某个功能只在特定环境启用,某个 Bean 只在某个依赖存在时才创建,某个配置只在某个开关打开时才生效。如果把这些逻辑硬编码在业务代码里,代码会变得臃肿且难以维护。
Spring Boot 的条件注解(Conditional Annotations)正是为了解决这个问题而生的。它们是 Spring Boot 自动配置机制的核心,也是构建可扩展、灵活的应用的关键技术。本文将系统梳理条件注解的完整知识体系,从底层原理到内置注解详解,再到与 Nacos 等配置中心配合时的热加载问题,结合大量实战代码,帮助你全面掌握这项技能。
一切的基础:@Conditional 注解
起源与设计
@Conditional 注解是 Spring Framework 4.0 引入的,它是所有条件注解的根注解。其核心设计思想是:组件仅在所有指定条件匹配时才有资格注册。
java
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Conditional {
Class<? extends Condition>[] value();
}
它可以用在三种地方:
- 类型级别:直接或间接标注在
@Component类上,包括@Configuration类 - 元注解级别:作为元注解用于组合自定义注解
- 方法级别:标注在
@Bean方法上
Condition 接口:真正的决策者
@Conditional 注解本身只是个"壳",真正做判断的是 Condition 接口的实现类:
java
@FunctionalInterface
public interface Condition {
boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);
}
matches 方法返回 true 时,被注解的组件才会被注册。
这里有两个关键参数:
ConditionContext:提供了丰富的上下文信息,包括:
BeanDefinitionRegistry:Bean 定义注册表ConfigurableListableBeanFactory:Bean 工厂Environment:环境信息(配置文件、系统属性等)ClassLoader:类加载器ResourceLoader:资源加载器
AnnotatedTypeMetadata:提供被注解元素的元数据,如注解属性值等。
工作流程
条件注解的评估发生在 Spring 容器初始化的两个阶段:
- 解析阶段(PARSE_CONFIGURATION) :处理
@Configuration类时 - 注册阶段(REGISTER_BEAN):实际注册 Bean 定义时
ConditionEvaluator 类负责调用 Condition.matches() 方法,根据返回结果决定是否跳过某个 Bean 的注册。
实战:自定义条件注解
让我们动手写一个自定义条件注解,加深理解:
java
// 1. 实现 Condition 接口
public class OnLinuxCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
String os = context.getEnvironment().getProperty("os.name");
return os != null && os.toLowerCase().contains("linux");
}
}
// 2. 定义自定义条件注解
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Conditional(OnLinuxCondition.class)
public @interface ConditionalOnLinux {
// 可以添加自定义属性
}
// 3. 使用自定义条件注解
@Configuration
public class OsConfig {
@Bean
@ConditionalOnLinux
public LinuxService linuxService() {
return new LinuxService();
}
}
Spring Boot 内置条件注解全家桶
Spring Boot 在 org.springframework.boot.autoconfigure.condition 包中提供了一套开箱即用的条件注解,覆盖了绝大多数场景。
@ConditionalOnClass / @ConditionalOnMissingClass
作用:根据类路径中是否存在(或不存在)指定的类来决定是否生效。
典型场景:自动配置类检查依赖库是否存在,按需加载。
java
@Configuration
@ConditionalOnClass({DataSource.class, EntityManager.class})
public class JpaAutoConfiguration {
// 当项目中存在 DataSource 和 EntityManager 时才加载 JPA 配置
}
@ConditionalOnBean / @ConditionalOnMissingBean
作用:根据 Spring 容器中是否存在(或不存在)指定类型的 Bean 来决定是否生效。
典型场景:避免重复注册 Bean,提供兜底默认实现。
java
@Configuration
public class DataSourceConfig {
@Bean
@ConditionalOnMissingBean(DataSource.class)
public DataSource defaultDataSource() {
// 如果用户没有自定义 DataSource,用默认的 Hikari
return new HikariDataSource();
}
}
@ConditionalOnProperty
作用:根据配置文件中的属性值决定是否生效。这是最常用的条件注解之一,也是本文重点。
java
@ConditionalOnProperty(
name = "saas.enabled",
havingValue = "true",
matchIfMissing = false
)
public class CustomLoginConfig {
// 仅当 saas.enabled=true 时才加载
}
参数详解:
| 参数 | 说明 |
|---|---|
name / value |
配置属性的名称,支持点分格式 |
havingValue |
期望的属性值,匹配时才生效。为空时只要属性存在且值不为 false/null 即满足 |
matchIfMissing |
当属性未定义时是否匹配,默认为 false |
@ConditionalOnExpression
作用:通过 SpEL 表达式组合复杂条件,是最灵活的条件注解。
java
@ConditionalOnExpression("${scene.version:private} || ${scene.version:saas}")
public class LoginAspect {
}
将获取到的配置值统一转成小写,再与全小写的常量比较。这样无论用户在 application.yml 里写什么大小写,都能正确匹配。
java
@ConditionalOnExpression(
"'${scene.version:}'.toLowerCase().equals('private') || '${scene.version:}'.toLowerCase().equals('saas')"
)
其他内置条件注解
| 注解 | 作用 | 典型场景 |
|---|---|---|
@ConditionalOnWebApplication |
当前应用是 Web 应用时生效 | Web 相关配置 |
@ConditionalOnNotWebApplication |
当前应用不是 Web 应用时生效 | 非 Web 环境(批处理) |
@ConditionalOnResource |
指定资源文件存在时生效 | 根据配置文件存在性加载 |
@ConditionalOnJava |
根据 JVM 版本决定 | Java 版本兼容 |
@ConditionalOnSingleCandidate |
容器中只有一个指定类型 Bean 时生效 | 自动配置依赖唯一 Bean |
@ConditionalOnWarDeployment |
WAR 包部署时生效 | 嵌入式 vs 外部容器 |
深度聚焦:@ConditionalOnProperty 与 @ConditionalOnExpression
@ConditionalOnProperty
作用:根据配置文件中的属性值决定是否生效。这是最常用的条件注解之一,也是实际项目中处理环境差异的首选工具。
java
@ConditionalOnProperty(
name = "scene.version",
havingValue = "private",
matchIfMissing = false
)
public class PrivateOnlyConfig {
// 仅当 scene.version=private 时才加载
}
参数详解:
| 参数 | 说明 |
|---|---|
name / value |
配置属性的名称,支持点分格式 |
havingValue |
期望的属性值,匹配时才生效。为空时只要属性存在且值不为 false/null 即满足 |
matchIfMissing |
当属性未定义时是否匹配 ,默认为 false |
实战场景:环境差异化配置
java
@Configuration
// 场景:不配置 scene.version 时,matchIfMissing=true 兜底,Bean 生效
// 配置 scene.version=private 时,值不等于 false,Bean 不生效
@ConditionalOnProperty(
name = "scene.version",
havingValue = "false",
matchIfMissing = true
)
public class DefaultEnvConfig {
// 未配置环境时使用默认逻辑
}
常见陷阱:matchIfMissing 的语义误解
很多开发者会把 matchIfMissing=true 误解为"设置默认值为 true",这是完全错误的。
| 配置情况 | matchIfMissing=true | matchIfMissing=false |
|---|---|---|
| 属性未定义 | ✅ 匹配 | ❌ 不匹配 |
| 属性值 = havingValue | ✅ 匹配 | ✅ 匹配 |
| 属性值 ≠ havingValue | ❌ 不匹配 | ❌ 不匹配 |
正确理解 :matchIfMissing 只控制"属性不存在"这一种特殊情况下的行为,与默认值无关。
@ConditionalOnExpression
作用 :通过 SpEL 表达式组合复杂条件,是最灵活的条件注解。适用于多属性组合、OR/AND 逻辑、字符串比较等 @ConditionalOnProperty 无法胜任的场景。
场景说明
scene.version 配置 |
代表环境 | 说明 |
|---|---|---|
private |
私有化环境 | 客户独立部署,数据隔离 |
saas |
SaaS 环境 | 多租户共享服务 |
paas |
PaaS 平台环境 | 平台即服务,提供底座能力 |
✅ 推荐写法:统一转小写后比较
java
@ConditionalOnExpression(
"'${scene.version:}'.toLowerCase().equals('private') || " +
"'${scene.version:}'.toLowerCase().equals('saas')"
)
public class MultiEnvConfig {
// scene.version 不区分大小写,只要值为 private 或 saas 即生效
}
参数详解:
| 场景 | 表达式解析 | 结果 |
|---|---|---|
| 未配置 | ''.toLowerCase().equals('private') → `false |
|
配置 private |
'private'.equals('private') → true |
✅ 匹配 |
配置 Private |
'private'.equals('private') → true |
✅ 匹配(忽略大小写) |
配置 PRIVATE |
'private'.equals('private') → true |
✅ 匹配(忽略大小写) |
配置 saas |
'saas'.equals('saas') → true |
✅ 匹配 |
配置 SaaS |
'saas'.equals('saas') → true |
✅ 匹配(忽略大小写) |
配置 paas |
'paas'.equals('private') → `false |
|
配置 PaaS |
'paas'.equals('private') → `false |
代码规范:抽取常量
当同一个表达式在多处使用时,建议在配置类中统一定义常量,避免魔法字符串散落各处:
java
public final class ConditionConstants {
/**
* 匹配私有化环境
* 适用场景:私有化独有功能
*/
public static final String SCENE_PRIVATE =
"'${scene.version:}'.toLowerCase().equals('private')";
/**
* 匹配 SaaS 环境
* 适用场景:SaaS 独有功能
*/
public static final String SCENE_SAAS =
"'${scene.version:}'.toLowerCase().equals('saas')";
/**
* 匹配 PaaS 平台环境
* 适用场景:PaaS 平台独有功能
*/
public static final String SCENE_PAAS =
"'${scene.version:}'.toLowerCase().equals('paas')";
/**
* 匹配私有化或 SaaS 环境(非 PaaS)
* 适用场景:私有化和 SaaS 共用的功能
*/
public static final String SCENE_PRIVATE_OR_SAAS =
"'${scene.version:}'.toLowerCase().equals('private') || " +
"'${scene.version:}'.toLowerCase().equals('saas')";
/**
* 匹配私有化或 PaaS 环境(非 SaaS)
* 适用场景:私有化和 PaaS 共用的功能
*/
public static final String SCENE_PRIVATE_OR_PAAS =
"'${scene.version:}'.toLowerCase().equals('private') || " +
"'${scene.version:}'.toLowerCase().equals('paas')";
/**
* 匹配 SaaS 或 PaaS 环境(非私有化)
* 适用场景:SaaS 和 PaaS 共用的功能
*/
public static final String SCENE_SAAS_OR_PAAS =
"'${scene.version:}'.toLowerCase().equals('saas') || " +
"'${scene.version:}'.toLowerCase().equals('paas')";
/**
* 匹配任意已定义环境(private / saas / paas)
* 适用场景:所有环境通用的功能
*/
public static final String SCENE_ANY =
"{'private', 'saas', 'paas'}.contains('${scene.version:}'.toLowerCase())";
/**
* 匹配非 SaaS 环境
* 适用场景:除 SaaS 外的所有环境
*/
public static final String SCENE_NOT_SAAS =
"!'${scene.version:}'.toLowerCase().equals('saas')";
private ConditionConstants() {}
}
使用方式:
java
@ConditionalOnExpression(ConditionConstants.SCENE_PRIVATE_OR_SAAS)
public class MultiEnvConfig {
// private 或 saas 环境加载
}
@ConditionalOnExpression(ConditionConstants.SCENE_PRIVATE)
public class PrivateOnlyConfig {
// 仅私有化环境加载
}
@ConditionalOnExpression(ConditionConstants.SCENE_PAAS)
public class PaasOnlyConfig {
// 仅 PaaS 平台环境加载
}
支持多个值的匹配(扩展写法)
如果需要同时匹配多个环境,使用集合 contains 写法更简洁:
java
// 匹配 private、saas、paas 三个环境
@ConditionalOnExpression(
"{'private', 'saas', 'paas'}.contains('${scene.version:}'.toLowerCase())"
)
public class AllEnvConfig {
// 所有已定义环境加载
}
// 匹配 private 或 paas 环境
@ConditionalOnExpression(
"{'private', 'paas'}.contains('${scene.version:}'.toLowerCase())"
)
public class PrivateOrPaasConfig {
// 私有化或 PaaS 环境加载
}
这种写法的好处:
- 可读性强:一眼看出允许哪些值
- 易于扩展:在集合中增删值即可
- 表达式简洁:不需要重复
equals拼接
多条件组合实战
java
@Configuration
// 条件:scene.version 必须是 private 或 saas,且开关开启
@ConditionalOnExpression(
"({'private', 'saas'}.contains('${scene.version:}'.toLowerCase())) && " +
"${scene.feature.enabled:true}"
)
public class FeatureConfig {
// 仅当环境匹配且功能开关打开时加载
}
不同场景下的配置示例
yaml
# application-private.yml(私有化环境)
scene:
version: private
feature:
enabled: true
# application-saas.yml(SaaS 环境)
scene:
version: saas
feature:
enabled: true
# application-paas.yml(PaaS 平台环境)
scene:
version: paas
feature:
enabled: false
多环境组合逻辑速查表
| 目标环境组合 | SpEL 表达式 |
|---|---|
| 仅 private | '${scene.version:}'.toLowerCase().equals('private') |
| 仅 saas | '${scene.version:}'.toLowerCase().equals('saas') |
| 仅 paas | '${scene.version:}'.toLowerCase().equals('paas') |
| private 或 saas | {'private', 'saas'}.contains('${scene.version:}'.toLowerCase()) |
| private 或 paas | {'private', 'paas'}.contains('${scene.version:}'.toLowerCase()) |
| saas 或 paas | {'saas', 'paas'}.contains('${scene.version:}'.toLowerCase()) |
| 任意环境 | {'private', 'saas', 'paas'}.contains('${scene.version:}'.toLowerCase()) |
| 非 saas | !'${scene.version:}'.toLowerCase().equals('saas') |
| 非 private | !'${scene.version:}'.toLowerCase().equals('private') |
小结:@ConditionalOnProperty vs @ConditionalOnExpression 选型指南
| 对比维度 | @ConditionalOnProperty | @ConditionalOnExpression |
|---|---|---|
| 适用场景 | 单个属性值匹配 | 多属性组合、复杂逻辑 |
| 大小写忽略 | ❌ 不支持 | ✅ 支持 .toLowerCase() |
| 默认值支持 | matchIfMissing |
${key:default} |
| 可读性 | ✅ 高 | ⚠️ 中等 |
| 灵活性 | ❌ 低 | ✅ 高 |
| 推荐使用 | 简单开关(true/false) | 多环境字符串比较、复杂组合 |
选择建议:
- 只需要判断
true/false→ 用@ConditionalOnProperty - 需要匹配多个字符串值(如
private/saas/paas)→ 用@ConditionalOnExpression+ 集合contains写法 - 需要多个属性组合判断(如
scene.version=private && scene.feature.enabled=true)→ 用@ConditionalOnExpression
条件注解与 Nacos 配置中心的联动
条件注解不支持热加载
这是一个最重要的结论 :@ConditionalOnProperty、@ConditionalOnExpression 等条件注解,不支持配置热加载。
它们只在 Spring 容器启动或初始化阶段 进行评估。一旦应用启动完成,Bean 是生是死已经定论,不会因为 Nacos 中配置的变化而改变。
换句话说:条件注解管的是"这个 Bean 能不能生",而配置热加载管的是"生了之后怎么变"。这两者是不同的维度。
为什么不能热加载?
Nacos 配置中心和 Spring 条件注解的工作机制有着本质区别:
| 维度 | 条件注解 | Nacos + @RefreshScope |
|---|---|---|
| 作用时机 | 容器启动阶段 | 运行时 |
| 影响对象 | Bean 是否被注册 | 已注册 Bean 的属性值 |
| 能否热加载 | ❌ 不能 | ✅ 能(需配合 @RefreshScope) |
@RefreshScope 的原理是:配置变更时销毁缓存的 Bean 实例,下次访问时重新创建,重新注入最新的 @Value。它依赖的前提是:这个 Bean 已经在容器中注册了。
如果这个 Bean 一开始就因为条件注解没满足而没被创建,那 @RefreshScope 也救不了它。
如何在运行时动态开关?
如果你的业务确实需要"运行时动态禁用/启用某个功能",最可靠的方案是放弃条件注解,把判断逻辑写到业务代码里。
改造前(条件注解控制):
java
@ConditionalOnExpression("'${scene.version:private}' != 'saas'")
@Component
public class DemoConsumer {
// 启动时决定生死,运行时无法改变
}
改造后(代码逻辑控制 + @RefreshScope):
java
@RefreshScope
@Component
@Slf4j
public class DemoConsumer {
@Value("${scene.version:private}")
private String sceneVersion;
public void consume() {
// 运行时判断,配置变了立即生效
if ("saas".equalsIgnoreCase(sceneVersion)) {
log.debug("saas 环境,Consumer 不执行");
return;
}
// private/paas等其他环境
}
}
这样配合 @RefreshScope,在 Nacos 中修改 scene.version 就能实时控制这个消费者是否"干活"。
@Value 的热加载注意点
单纯使用 @Value 注入配置属性,默认也不能热更新。因为 @Value 注入发生在 Bean 创建时,之后字段就不会变了。
要让 @Value 支持热加载,需要:
- 在类上添加
@RefreshScope - Nacos 配置变更后,
@RefreshScope会销毁旧 Bean,下次访问时重新创建并注入最新值
推荐使用 @ConfigurationProperties + @RefreshScope 管理多字段配置,更清晰。
最佳实践与避坑指南
选择合适的条件注解
| 场景 | 推荐注解 | 理由 |
|---|---|---|
| 依赖存在才启用 | @ConditionalOnClass |
精准匹配类路径 |
| 单一配置开关 | @ConditionalOnProperty |
可读性最好 |
| 复杂条件组合 | @ConditionalOnExpression |
灵活性最高 |
| 避免重复 Bean | @ConditionalOnMissingBean |
给用户留覆盖空间 |
常见踩坑点
坑1:matchIfMissing 语义理解错误
matchIfMissing = true是"不配置时也匹配",不是"默认值"- 配合
havingValue使用时,不配置和配置成havingValue的结果可能相同,但语义不同
坑2:条件注解不能热加载
- 如果需要运行时动态切换,请用业务代码判断 +
@RefreshScope
坑3:@ConditionalOnExpression 中默认值语法写错
- 正确:
'${key:default}' == 'value' - 错误:
${key:default} == 'value'(SpEL 中字符串比较需要引号)
坑4:多个条件注解的叠加规则
- 叠加在一起是 AND 关系,必须全部满足
- 同名注解不可重复使用,需要用
||或name数组变通
实战建议
- 启动时加日志确认 Bean 加载状态:
java
@PostConstruct
public void init() {
log.info("Bean 已加载,当前配置:scene.version = {}",
environment.getProperty("scene.version"));
}
-
单元测试验证条件逻辑 :
使用
@TestPropertySource模拟不同配置,验证条件注解行为。 -
文档化环境配置差异 :
清晰记录 SaaS/私有化等不同环境需要的配置值,避免运维误配。
结语
条件注解是 Spring Boot 自动配置的基石,掌握它们能让你写出更具弹性、更智能的应用。从 @Conditional 这个根源注解出发,Spring Boot 为我们提供了丰富的开箱即用注解,覆盖了类路径检查、Bean 存在性、配置属性、SpEL 表达式等绝大多数场景。
但也要时刻记得它们的边界:条件注解只在启动时生效,无法热加载 。如果需求涉及运行时的动态开关,请果断拥抱 @RefreshScope + 业务代码判断的组合方案。
希望这篇文章能帮你全面掌握 Spring Boot 条件注解,写出更优雅的代码。如果你有任何问题,欢迎在评论区交流讨论。