北屋教程网

专注编程知识分享,从入门到精通的编程学习平台

Spring 中的@Conditional 注解:让 Bean 加载更加灵活智能

在日常开发中,你是不是经常遇到这样的场景:不同环境需要不同配置,测试环境要用内存数据库,生产环境必须连接 MySQL?或者某些功能只想在特定条件下才启用?Spring 的@Conditional 注解就是为解决这些问题而生的,它让你的应用变得更加智能。

@Conditional 注解是什么?

@Conditional 是 Spring 4.0 引入的一个核心注解,它可以根据特定条件决定是否创建某个 Bean 或启用某个配置。通俗点说,它就像一个"看门人"(条件匹配器),只有条件满足了,才允许相关 Bean 进入 Spring 容器。

基本用法

@Conditional 注解的基本用法很简单,你只需要实现 Condition 接口,并在 matches 方法中编写判断逻辑:

public class LinuxCondition implements Condition {
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        // 获取当前系统名称
        String osName = context.getEnvironment().getProperty("os.name");
        // 如果包含Linux字样,就返回true
        return osName != null && osName.contains("Linux");
    }
}

public class WindowsCondition implements Condition {
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        // 获取当前系统名称
        String osName = context.getEnvironment().getProperty("os.name");
        // 如果包含Windows字样,就返回true
        return osName != null && osName.contains("Windows");
    }
}

@Configuration
public class SystemConfig {

    @Bean
    @Conditional(LinuxCondition.class)
    public CommandRunner linuxCommandRunner() {
        return new LinuxCommandRunner();
    }

    @Bean
    @Conditional(WindowsCondition.class)
    public CommandRunner windowsCommandRunner() {
        return new WindowsCommandRunner();
    }
}

在这个例子中,Spring 会根据当前操作系统类型,自动选择合适的 CommandRunner 实现。

Spring Boot 中的常用条件注解

Spring Boot 在@Conditional 基础上,提供了一系列更便捷的派生注解:

@ConditionalOnProperty

这个注解允许根据配置属性的值决定是否创建 Bean:

@Configuration
public class CacheConfig {

    @Bean
    @ConditionalOnProperty(
        prefix = "app",
        name = "cache.enabled",
        havingValue = "true",
        matchIfMissing = false // 默认值为true,设置为false表示属性不存在时条件不满足
    )
    public CacheManager cacheManager() {
        return new ConcurrentMapCacheManager();
    }
}

在这个例子中:

  • 只有当app.cache.enabled=true时,才会创建这个缓存管理器
  • prefix参数设置了配置属性的前缀
  • matchIfMissing = false表示如果属性不存在,条件不满足(注意默认值为 true)

@ConditionalOnClass 和@ConditionalOnMissingClass

这两个注解用于根据类路径(classpath)中是否存在特定类来判断:

@Configuration
public class RedisConfig {

    @Bean
    @ConditionalOnClass(RedisOperations.class)
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);
        return template;
    }
}

这段配置会在项目依赖了 Redis 相关库时自动创建 RedisTemplate,如果没有引入 Redis 依赖,这个 Bean 就不会被创建。

@ConditionalOnBean 和@ConditionalOnMissingBean

这两个注解根据容器中是否存在某个 Bean 来判断:

@Configuration
public class DataSourceConfig {

    @Bean
    @ConditionalOnMissingBean
    public DataSource defaultDataSource() {
        return new EmbeddedDatabaseBuilder()
                .setType(EmbeddedDatabaseType.H2)
                .build();
    }
}

这个例子中,只有当容器中没有其他 DataSource 类型的 Bean 时,才会创建这个默认数据源。

@ConditionalOnResource

这个注解用于检查类路径或文件系统中是否存在特定资源文件:

@Configuration
public class AppConfig {

    @Bean
    @ConditionalOnResource(resources = "classpath:config/app-config.properties")
    public AppSettings appSettings() {
        return new AppSettings();
    }

    @Bean
    @ConditionalOnResource(resources = {"file:/etc/app/security.yml", "classpath:default-security.yml"})
    public SecurityConfig securityConfig() {
        return new SecurityConfig();
    }
}

资源解析原理

在底层,@ConditionalOnResource使用 Spring 的ResourceLoader机制来解析和检查资源是否存在:

// OnResourceCondition内部简化逻辑
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
    ResourceLoader loader = context.getResourceLoader();
    for (String resource : resources) {
        if (loader.getResource(resource).exists()) {
            return true; // 只要有一个资源存在,条件就满足
        }
    }
    return false;
}

值得注意的是,resources数组是 OR 关系——只要有一个资源存在,条件就满足。这在提供默认配置时特别有用。

支持的资源路径前缀

@ConditionalOnResource支持多种资源路径格式:

  • classpath: - 类路径下的资源(如 JAR 包或 classes 目录中的文件)
  • file: - 文件系统中的资源(如服务器磁盘上的文件)
  • classpath*: - 类路径下所有匹配的资源(支持通配符)

实际应用场景

  1. 特定功能的配置文件检测
@Configuration
@ConditionalOnResource(resources = "classpath:features/payment-gateway.properties")
public class PaymentGatewayConfig {
    // 当存在支付网关配置文件时,才启用相关功能
    // ...
}
  1. 根据环境选择配置源
@Bean
@ConditionalOnResource(resources = {
    "file:/etc/myapp/config.yml",  // 先检查外部配置
    "classpath:config/default.yml"  // 再检查默认配置
})
public ConfigurationLoader configLoader(ResourceLoader resourceLoader) {
    Resource resource = null;
    // 按优先级顺序查找第一个存在的资源
    for (String path : new String[] {"file:/etc/myapp/config.yml", "classpath:config/default.yml"}) {
        Resource r = resourceLoader.getResource(path);
        if (r.exists()) {
            resource = r;
            break;
        }
    }
    return new ConfigurationLoader(resource);
}
  1. 结合其他条件注解使用
@Bean
@ConditionalOnClass(name = "io.micrometer.core.instrument.MeterRegistry")
@ConditionalOnResource(resources = "classpath:metrics-config.properties")
public MetricsCollector metricsCollector() {
    // 同时满足两个条件:
    // 1. 类路径中有度量库
    // 2. 存在度量配置文件
    return new MetricsCollector();
}

注意事项

  • 资源路径无效(如格式错误)不会导致应用启动失败,但条件会被视为不满足
  • 网络资源(如http:前缀)不受官方支持,可能导致不可预期的行为
  • 对于大量资源检查,性能可能受影响,建议只检查关键资源

自定义条件注解

除了使用 Spring 提供的条件注解,我们还可以创建自己的条件注解。只需两步:

  1. 实现 Condition 接口
  2. 创建自定义注解
// 步骤1:实现Condition接口
public class OnEnvCondition implements Condition {

    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        Map<String, Object> attrs = metadata.getAnnotationAttributes(ConditionalOnEnv.class.getName());
        String[] envs = (String[]) attrs.get("value");
        String[] activeProfiles = context.getEnvironment().getActiveProfiles();

        // 增加非空检查,避免数组越界
        if (activeProfiles.length == 0) return false;
        String activeProfile = activeProfiles[0];

        for (String env : envs) {
            if (env.equalsIgnoreCase(activeProfile)) {
                return true;
            }
        }
        return false;
    }
}

// 步骤2:创建自定义注解
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(OnEnvCondition.class)
public @interface ConditionalOnEnv {
    String[] value() default {};
}

使用自定义条件注解:

@Configuration
public class DataSourceConfig {

    @Bean
    @ConditionalOnEnv({"dev", "test"})
    public DataSource h2DataSource() {
        return new EmbeddedDatabaseBuilder()
                .setType(EmbeddedDatabaseType.H2)
                .build();
    }

    @Bean
    @ConditionalOnEnv("prod")
    public DataSource mysqlDataSource() {
        return DataSourceBuilder.create()
                .url("jdbc:mysql://localhost:3306/prod_db")
                .username("root")
                .password("password")
                .build();
    }
}

条件注解的底层机制

Spring 框架如何处理@Conditional 注解的?来看看它的内部工作原理:

核心流程:

  1. 在 Spring 容器解析 Bean 定义时(而非创建 Bean 实例时),遇到带有@Conditional 注解的组件
  2. ConfigurationClassPostProcessor处理配置类时,使用ConditionEvaluator判断条件
  3. 调用每个Condition实现类的matches()方法进行条件检查
  4. 只有所有条件都满足,才会注册 Bean 定义到容器中

简化的 Spring 源码逻辑:

// Spring内部逻辑简化示意
public boolean shouldSkip(AnnotatedTypeMetadata metadata) {
    if (metadata.isAnnotated(Conditional.class.getName())) {
        for (Condition condition : conditions) {
            if (!condition.matches(context, metadata)) {
                return true; // 跳过此Bean
            }
        }
    }
    return false; // 不跳过,正常注册Bean
}

条件注解的组合使用

@Conditional 注解支持多种组合方式,增加了使用的灵活性:

  1. 多个条件注解组合(AND 关系):
@Bean
@ConditionalOnProperty(name = "feature.new-ui", havingValue = "true")
@ConditionalOnClass(NewUIRenderer.class)
public UIService newUIService() {
    return new NewUIServiceImpl();
}
  1. 在@Conditional 中使用多个条件类
@Bean
@Conditional({LinuxCondition.class, JDK11Condition.class})
public MyBean myBean() {
    return new MyBean();
}
  1. 自定义复合条件(如 OR 关系):
public class DevOrTestEnvCondition implements Condition {
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        String[] activeProfiles = context.getEnvironment().getActiveProfiles();
        for (String profile : activeProfiles) {
            if ("dev".equals(profile) || "test".equals(profile)) {
                return true;
            }
        }
        return false;
    }
}

实战案例:动态切换缓存实现

下面通过一个实际案例,看看如何使用条件注解灵活切换缓存实现:

首先,定义一个缓存接口:

public interface CacheService {
    void put(String key, Object value);
    Object get(String key);
    void remove(String key);
}

然后,实现两种不同的缓存方案:

public class RedisCacheService implements CacheService {
    private final RedisTemplate<String, Object> redisTemplate;

    public RedisCacheService(RedisTemplate<String, Object> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    @Override
    public void put(String key, Object value) {
        redisTemplate.opsForValue().set(key, value);
    }

    @Override
    public Object get(String key) {
        return redisTemplate.opsForValue().get(key);
    }

    @Override
    public void remove(String key) {
        redisTemplate.delete(key);
    }
}

public class LocalCacheService implements CacheService {
    private final Map<String, Object> cacheMap = new ConcurrentHashMap<>();

    @Override
    public void put(String key, Object value) {
        cacheMap.put(key, value);
    }

    @Override
    public Object get(String key) {
        return cacheMap.get(key);
    }

    @Override
    public void remove(String key) {
        cacheMap.remove(key);
    }
}

最后,通过条件注解智能选择缓存实现:

@Configuration
public class CacheConfig {

    @Bean
    @ConditionalOnProperty(prefix = "app", name = "cache.type", havingValue = "redis")
    @ConditionalOnClass(RedisTemplate.class)
    @Primary
    public CacheService redisCacheService(RedisTemplate<String, Object> redisTemplate) {
        return new RedisCacheService(redisTemplate);
    }

    @Bean
    @ConditionalOnMissingBean(CacheService.class)
    public CacheService localCacheService() {
        return new LocalCacheService();
    }
}

这样配置后,系统会根据配置和环境自动选择合适的缓存实现:

  • 如果配置了app.cache.type=redis且引入了 Redis 依赖,使用 Redis 缓存
  • 否则,使用本地内存缓存作为默认实现

条件注解使用技巧

1. 条件注解的执行顺序

当一个组件上有多个条件注解时,Spring 会按照一定的先后顺序评估这些条件:

  • 类路径检查(如@ConditionalOnClass)通常优先于 Bean 存在性检查(如@ConditionalOnBean)
  • 可以使用@Order注解控制自定义条件的执行顺序(数值越小优先级越高)
  • 先满足基础环境条件,再检查依赖条件是一种常见模式

2. 调试条件评估

在开发过程中,如果发现条件没有按预期工作,可以通过以下方式进行调试:

方法 1:在application.properties中添加:

debug=true

Spring Boot 会输出详细的条件评估报告,帮助你找出问题所在。输出大概是这样的:

CONDITIONS EVALUATION REPORT
Positive matches:
-----------------
   CacheAutoConfiguration matched:
      - @ConditionalOnClass found required class 'org.springframework.cache.CacheManager' (OnClassCondition)

Negative matches:
-----------------
   RedisAutoConfiguration:
      Did not match:
         - @ConditionalOnClass did not find required class 'org.springframework.data.redis.core.RedisOperations' (OnClassCondition)

方法 2:在 IDE 中对Condition#matches方法设置断点,进行调试。

3. @ConditionalOnWebApplication 的多种模式

@
ConditionalOnWebApplication 支持不同的 Web 应用类型:

// 仅在传统Servlet环境中生效
@Bean
@ConditionalOnWebApplication(type = WebApplicationType.SERVLET)
public ServletFilter securityFilter() {
    return new SecurityFilter();
}

// 仅在响应式Web环境中生效
@Bean
@ConditionalOnWebApplication(type = WebApplicationType.REACTIVE)
public WebFilter reactiveFilter() {
    return new ReactiveSecurityFilter();
}

// 在任何Web环境中都生效(默认行为)
@Bean
@ConditionalOnWebApplication
public WebLogger webLogger() {
    return new WebLogger();
}

4. 用好@ConditionalOnExpression

@ConditionalOnExpression 支持 SpEL 表达式,可以构建复杂的条件判断:

// 基本用法
@Bean
@ConditionalOnExpression("${app.feature.enabled:false} and ${app.environment} == 'production'")
public FeatureManager productionFeatureManager() {
    return new EnhancedFeatureManager();
}

// 复杂示例:结合系统属性和容器引用
@Bean
@ConditionalOnExpression("#{systemProperties['java.version'].startsWith('17') && @environment.getProperty('app.mode') == 'advanced'}")
public AdvancedService advancedService() {
    return new AdvancedServiceImpl();
}

第二个例子演示了如何:

  • 检查 Java 版本(systemProperties['java.version'])
  • 引用 Spring 容器中的 bean(@environment)
  • 组合多个条件判断

@Profile 与@Conditional 的比较

很多开发者会困惑什么时候该用@Profile,什么时候该用@Conditional:

// 使用@Profile
@Configuration
@Profile("dev")
public class DevConfig {
    // dev环境专用配置
}

// 使用@Conditional
@Configuration
@ConditionalOnProperty(name = "app.mode", havingValue = "development")
public class DevelopmentConfig {
    // 开发模式专用配置
}

二者的区别和选择:

  • @Profile更简单,专注于环境分组管理,通过spring.profiles.active激活
  • @Conditional更灵活,可以基于任何条件,不仅限于环境
  • 复杂场景下可以组合使用:
@Bean
@Profile("prod") // 仅在prod环境
@ConditionalOnProperty("app.metrics.enabled") // 且启用了指标收集
public MetricsCollector metricsCollector() {
    return new MetricsCollector();
}

性能注意事项

条件注解是把双刃剑,用好了能大幅提升灵活性,用不好会影响性能:

  1. 避免重型操作:在matches()方法中避免执行耗时操作
// 不推荐:条件检查中执行网络请求
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
    return httpClient.checkServerStatus().isOk(); // 可能导致启动缓慢
}
  1. 缓存条件结果:对于频繁使用的条件,考虑缓存结果
// 优化:缓存检查结果
public class CachingCondition implements Condition {
    private static Boolean result = null;

    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        if (result == null) {
            // 计算结果(仅首次执行)
            result = computeResult(context);
        }
        return result;
    }
}
  1. 控制条件数量:过多的条件注解会增加启动时间,适度使用

常见问题与注意事项

使用条件注解时,有几个常见问题需要注意:

  1. Bean 定义阶段 vs 实例化阶段
@Configuration
public class Config {
    @Bean
    @ConditionalOnMissingBean // 检查时还未创建其他Bean
    public Service defaultService() {
        return new DefaultService();
    }

    @Bean
    public Service customService() { // 此Bean定义已被注册,但可能未实例化
        return new CustomService();
    }
}

在这个例子中,由于customService()已被注册为 Bean 定义,defaultService()的条件会失败,即使customService()还未被实例化。

// 避免这样的循环依赖
@Bean
@ConditionalOnBean(name = "beanB")
public BeanA beanA() { return new BeanA(); }

@Bean
@ConditionalOnBean(name = "beanA")
public BeanB beanB() { return new BeanB(); }
  1. 这种循环依赖的条件配置会导致两个 Bean 都无法创建。

总结

条件注解

用途

应用场景

@ConditionalOnProperty

根据配置属性判断

功能开关、环境配置

@ConditionalOnClass

根据类路径判断

依赖检测

@ConditionalOnMissingClass

根据类路径不存在判断

依赖检测

@ConditionalOnBean

根据 Bean 存在判断

依赖其他组件

@ConditionalOnMissingBean

根据 Bean 不存在判断

提供默认实现

@ConditionalOnWebApplication

在 Web 应用中判断

Web 环境检测(SERVLET/REACTIVE/ANY)

@ConditionalOnResource

根据资源存在判断

配置文件/静态资源/证书文件等存在性检测

@ConditionalOnExpression

根据 SpEL 表达式判断

复杂条件判断

控制面板
您好,欢迎到访网站!
  查看权限
网站分类
最新留言