《Spring Boot 入门》4.3 条件装配与开关

逐个讲透 @ConditionalOnClass、@ConditionalOnMissingBean、@ConditionalOnProperty、@ConditionalOnWebApplication、@ConditionalOnBean 等条件注解,每类配一个最小可运行示例,并说明评估顺序的坑与如何用条件装配实现「按配置开关功能」。

本节目标:掌握条件装配家族,理解 @ConditionalOnMissingBean 为何能实现「默认配置可被覆盖」,并亲手用条件装配写一个功能开关。
适用版本:Spring Boot 4.1.x(Java 21)

上一节留下的问题

上一节的 --debug 报告里,DataSourceAutoConfiguration 明明在清单文件里,可你平时不接数据库也不会出问题;而你一旦自己定义了某个同类型的 bean,默认配置就自动让位。这两件事背后是同一个机制——条件装配。它回答的是「一个自动配置类/一个 bean,到底该不该生效」。

条件注解家族总览

@Conditional 是根,下面派生出一大批语义化的子注解。常用的这些:

注解生效条件典型用途
@ConditionalOnClassclasspath 上存在指定类有依赖才启用
@ConditionalOnMissingClassclasspath 上不存在指定类缺失时降级
@ConditionalOnBean容器中已存在指定 bean依赖先行 bean
@ConditionalOnMissingBean容器中不存在指定 bean提供默认实现
@ConditionalOnProperty配置属性满足条件功能开关
@ConditionalOnWebApplication当前是 Web 应用Web 专属配置
@ConditionalOnNotWebApplication当前不是 Web 应用非 Web 逻辑
@ConditionalOnResource指定资源存在有配置文件才启用
@ConditionalOnExpressionSpEL 表达式为真复杂组合条件
@ConditionalOnJavaJava 版本满足版本分支

下面逐个给出最小可运行示例。

@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.enabledAuditService 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 。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「java」更多文章

  1. 《Spring Boot 入门》18.3 打包与运行
  2. 《Spring Boot 入门》18.2 实现
  3. 《Spring Boot 入门》18.1 需求与设计