本节目标:掌握条件装配家族,理解
@ConditionalOnMissingBean为何能实现「默认配置可被覆盖」,并亲手用条件装配写一个功能开关。
适用版本:Spring Boot 4.1.x(Java 21)
上一节留下的问题
上一节的 --debug 报告里,DataSourceAutoConfiguration 明明在清单文件里,可你平时不接数据库也不会出问题;而你一旦自己定义了某个同类型的 bean,默认配置就自动让位。这两件事背后是同一个机制——条件装配。它回答的是「一个自动配置类/一个 bean,到底该不该生效」。
条件注解家族总览
@Conditional 是根,下面派生出一大批语义化的子注解。常用的这些:
| 注解 | 生效条件 | 典型用途 |
|---|---|---|
@ConditionalOnClass | classpath 上存在指定类 | 有依赖才启用 |
@ConditionalOnMissingClass | classpath 上不存在指定类 | 缺失时降级 |
@ConditionalOnBean | 容器中已存在指定 bean | 依赖先行 bean |
@ConditionalOnMissingBean | 容器中不存在指定 bean | 提供默认实现 |
@ConditionalOnProperty | 配置属性满足条件 | 功能开关 |
@ConditionalOnWebApplication | 当前是 Web 应用 | Web 专属配置 |
@ConditionalOnNotWebApplication | 当前不是 Web 应用 | 非 Web 逻辑 |
@ConditionalOnResource | 指定资源存在 | 有配置文件才启用 |
@ConditionalOnExpression | SpEL 表达式为真 | 复杂组合条件 |
@ConditionalOnJava | Java 版本满足 | 版本分支 |
下面逐个给出最小可运行示例。
@ConditionalOnClass:有依赖才生效
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.context.annotation.Bean;
@AutoConfiguration
@ConditionalOnClass(name = "com.example.probe.OptionalLibrary")
public class OptionalLibraryAutoConfiguration {
@Bean
public OptionalLibraryClient optionalLibraryClient() {
return new OptionalLibraryClient();
}
}
只有当 classpath 上真的存在 com.example.probe.OptionalLibrary 时,这个自动配置才会被处理。这就是「加了某个 starter,行为才变化」的底层机制。
这里用 name 写字符串而不是直接写 OptionalLibrary.class,是为了避免在条件评估之前就把这个类加载进来。这正是它的坑:
把
@ConditionalOnClass写在@Bean方法上时,如果方法的返回类型或参数类型引用了那个可能不存在的类,JVM 解析方法签名时就会尝试加载它,条件还没评估就先抛NoClassDefFoundError。稳妥做法是把条件提到类级别,或把 bean 拆进一个嵌套配置类,让外层类不含任何对缺失类的直接引用。
一个安全的写法是拆两层:
@AutoConfiguration
public class OptionalLibraryAutoConfiguration {
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(OptionalLibrary.class)
static class OptionalLibraryBeans {
@Bean
OptionalLibraryClient client() {
return new OptionalLibraryClient();
}
}
}
外层类不碰 OptionalLibrary,内层才引用它。这样即使类不存在,外层也能正常加载,条件判断得以执行。
@ConditionalOnMissingBean:默认配置可被覆盖
这是「用户覆盖默认配置」的实现原理,也是自动配置能被放心使用的根本原因:
@AutoConfiguration
@ConditionalOnClass(GreetingService.class)
public class GreetingAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public GreetingService greetingService() {
return new GreetingService("default");
}
}
容器里没有 GreetingService 时,自动配置提供一个默认实现;一旦你自己定义了同类型的 bean,这个默认 bean 就不会创建,你的实现生效。整个过程不需要任何额外配置,这就是「约定优于配置 + 可覆盖」的完整闭环。
@ConditionalOnMissingBean 可以细化到类型或名字:
@Bean
@ConditionalOnMissingBean(name = "customGreetingService")
public GreetingService greetingService() {
return new GreetingService("default");
}
@ConditionalOnProperty:按配置开关
最常见的「功能开关」就是它:
@AutoConfiguration
@ConditionalOnProperty(name = "feature.audit.enabled", havingValue = "true")
public class AuditAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public AuditService auditService() {
return new AuditService();
}
}
三个关键参数:
name:属性名;havingValue:期望值,属性等于它才生效;matchIfMissing:属性完全缺失时是否算匹配,默认false。
matchIfMissing = true 常用于「默认开启、可显式关闭」:
@ConditionalOnProperty(name = "feature.audit.enabled", havingValue = "true", matchIfMissing = true)
注意一个易错点:havingValue 与属性值比较时,只要属性存在但值不等于 havingValue,条件就不成立;只有属性不存在时才看 matchIfMissing。很多「开关关不掉」的问题,是 matchIfMissing 写成了 true 又指望用 false 关掉——实际上 havingValue="true" 下写成 false 仍能关掉,但若没写 havingValue,则只要属性存在(哪怕是 false)就算匹配,这才是真正的坑。
@ConditionalOnWebApplication:只在 Web 环境生效
@AutoConfiguration
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
public class WebOnlyAutoConfiguration {
@Bean
public WebTracingFilter webTracingFilter() {
return new WebTracingFilter();
}
}
type 可取 SERVLET、REACTIVE、ANY。同一个自动配置在非 Web 应用(例如命令行任务)里不会生效,避免注册无意义的 Filter。
@ConditionalOnBean:依赖已存在的 bean
@AutoConfiguration
@ConditionalOnBean(DataSource.class)
public class AuditRepositoryAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public AuditRepository auditRepository(DataSource dataSource) {
return new AuditRepository(dataSource);
}
}
这里有个评估顺序的坑:@ConditionalOnBean 只能看到「在它之前已经被处理的配置类」所定义的 bean。如果 DataSource 的自动配置排在它后面,条件会误判为不成立。解决办法是显式声明顺序:
@AutoConfiguration(after = DataSourceAutoConfiguration.class)
@ConditionalOnBean(DataSource.class)
public class AuditRepositoryAutoConfiguration {
// ...
}
规则是:凡是依赖 @ConditionalOnBean 的自动配置,都必须用 before / after 明确它相对被依赖者的位置,否则行为随类名字母序漂移,时好时坏。
自定义 Condition
内置注解不够用时,可以实现 Condition 接口。推荐继承 SpringBootCondition,它能自动接入 --debug 报告:
import org.springframework.boot.autoconfigure.condition.ConditionOutcome;
import org.springframework.boot.autoconfigure.condition.SpringBootCondition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;
public class OnTenantModeCondition extends SpringBootCondition {
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
String mode = context.getEnvironment().getProperty("app.tenant-mode", "single");
boolean matched = "multi".equals(mode);
return ConditionOutcome.matchOrNot(matched, "app.tenant-mode=" + mode);
}
}
用法:
@AutoConfiguration
@Conditional(OnTenantModeCondition.class)
public class MultiTenantAutoConfiguration {
// 仅当 app.tenant-mode=multi 时生效
}
用 SpringBootCondition 的好处是,匹配结果会出现在 --debug 报告里,附带你返回的原因文本,排查时一目了然。
完整实例:用条件装配做「功能开关」
把上面几块拼起来,做一个按配置开关的审计功能。先写自动配置类:
package com.example.probe.audit;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
@AutoConfiguration
@ConditionalOnProperty(name = "feature.audit.enabled", havingValue = "true")
public class AuditAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public AuditService auditService(AuditProperties properties) {
return new AuditService(properties);
}
}
配套的属性绑定类:
package com.example.probe.audit;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "feature.audit")
public class AuditProperties {
private boolean enabled = false;
private String channel = "log";
// getter / setter 省略
public boolean isEnabled() { return enabled; }
public void setEnabled(boolean enabled) { this.enabled = enabled; }
public String getChannel() { return channel; }
public void setChannel(String channel) { this.channel = channel; }
}
在 application.yml 里开关它:
feature:
audit:
enabled: true
channel: db
行为对照表:
feature.audit.enabled | AuditService bean | 说明 |
|---|---|---|
| 缺失 | 不创建 | 默认关闭 |
false | 不创建 | 显式关闭 |
true | 创建(除非你自己定义了) | 开启 |
true 且你自定义了 AuditService | 用你的 | 默认被覆盖 |
这一套就是 Spring Boot 自动配置的标准套路:用 @ConditionalOnProperty 控制「要不要启用」,用 @ConditionalOnMissingBean 保证「用户可覆盖」。 你自己写 starter 时,几乎会反复用到这两个组合。
条件评估的通用规则
- 条件是在容器刷新阶段逐个配置类评估的,顺序影响
@ConditionalOnBean的结果。 - 条件注解既可以标在类上,也可以标在**
@Bean方法**上,方法级更细粒度。 - 多个条件同时存在时,全部成立才生效(隐式 AND)。
- 想排查条件为何不成立,永远先看
--debug的Negative matches。
小结
- 条件装配决定「自动配置类 / bean 该不该生效」,是自动配置能安全默认开启的前提。
@ConditionalOnClass判断依赖是否存在;写在@Bean方法上引用缺失类会触发NoClassDefFoundError,应提到类级别或用name。@ConditionalOnMissingBean是「默认配置可被用户覆盖」的实现原理。@ConditionalOnProperty是最常用的功能开关,注意havingValue与matchIfMissing的区别。- 依赖
@ConditionalOnBean时必须用@AutoConfiguration(after = ...)固定顺序,否则行为不确定。 - 自定义条件继承
SpringBootCondition,可把匹配原因输出到--debug报告。
至此,第 4 章回答完了「Tomcat 为什么自己起来」的完整链路:入口注解 → 清单文件 → 条件装配。下一章我们换个角度,从容器内部看这些被装配出来的对象——bean 到底是怎么被创建、注入和管理的。
阅读导航:上一节:4.2 自动配置如何生效 · 下一节:5.1 容器与 Bean 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。