🎭 Spring的@Conditional:代码界的"看人下菜碟"大师!

44 阅读8分钟

副标题:让你的Bean像变色龙一样智能地出现和消失 🦎


🎬 开场白:Bean的"选秀"现场

嘿,小伙伴们!👋 今天我们要聊一个超级有趣的话题——Spring的@Conditional注解。

想象一下,你是一档选秀节目的导演,手里有一堆选手(Bean),但不是所有选手都能上台表演。你得根据不同的条件来决定:

  • 🎤 如果是唱歌节目,就让歌手上台
  • 🎸 如果是乐器演奏,就让乐手上台
  • 💃 如果观众太少,就取消演出

Spring的@Conditional注解就是这样一个"智能导演",它能根据各种条件来决定要不要创建某个Bean!


📚 第一幕:什么是@Conditional?

基本概念

@Conditional是Spring 4.0引入的一个超级灵活的注解,它的作用就像一个"门卫大叔"🚪:

@Configuration
public class MyConfig {
    
    @Bean
    @Conditional(OnWindowsCondition.class)
    public MessageService windowsService() {
        return new WindowsMessageService();
    }
    
    @Bean
    @Conditional(OnLinuxCondition.class)
    public MessageService linuxService() {
        return new LinuxMessageService();
    }
}

在这个例子中:

  • 如果运行在Windows系统上,就创建windowsService
  • 如果运行在Linux系统上,就创建linuxService

就像你去餐厅点菜,服务员会问:"您是吃辣还是不吃辣?" 🌶️


🎪 第二幕:生活中的比喻

让我用一个超级接地气的例子来解释:

📱 智能家居场景

想象你家有个智能管家系统:

早上7点:
- 如果是工作日 → 播放闹钟 ⏰
- 如果是周末 → 让你继续睡 😴

天气情况:
- 如果下雨 ☔ → 提醒你带伞
- 如果晴天 ☀️ → 建议你出去运动

温度情况:
- 如果 < 10°C → 开启暖气 🔥
- 如果 > 30°C → 开启空调 ❄️

Spring的@Conditional就是这样的"智能管家",根据不同条件来决定要不要创建某些Bean!


🔧 第三幕:Condition接口的核心原理

Condition接口长这样

@FunctionalInterface
public interface Condition {
    boolean matches(ConditionContext context, 
                   AnnotatedTypeMetadata metadata);
}

这个接口超级简单! 就一个方法:

  • 返回true → "老板,这个Bean可以要!" ✅
  • 返回false → "算了,这个Bean不要了。" ❌

实战案例:自定义一个"操作系统"条件

public class OnWindowsCondition implements Condition {
    
    @Override
    public boolean matches(ConditionContext context, 
                          AnnotatedTypeMetadata metadata) {
        // 获取当前操作系统
        String os = context.getEnvironment()
                          .getProperty("os.name")
                          .toLowerCase();
        
        // 判断是否包含"windows"
        return os.contains("windows");
    }
}

就像门卫大叔检查你的工牌:

  • "哦,你是Windows部门的?进!" 🚪✅
  • "什么?你是Mac?不好意思,走错门了。" 🚪❌

🎨 第四幕:Spring Boot提供的常用条件注解

Spring Boot贴心地给我们准备了一堆"现成的条件",就像外卖平台的"套餐"🍱:

1. @ConditionalOnClass 📦

作用: 当classpath中存在指定的类时,才创建Bean

@Configuration
@ConditionalOnClass(DataSource.class)
public class DatabaseConfig {
    
    @Bean
    public DataSource dataSource() {
        return new HikariDataSource();
    }
}

生活比喻:

"只有你家有烤箱🔥,我才教你做蛋糕🎂。没烤箱?那就算了。"


2. @ConditionalOnMissingBean 🕳️

作用: 当容器中不存在指定Bean时,才创建

@Configuration
public class DefaultConfig {
    
    @Bean
    @ConditionalOnMissingBean(DataSource.class)
    public DataSource defaultDataSource() {
        return new SimpleDataSource();
    }
}

生活比喻:

"如果你没带雨伞☂️,我就借你一把。你已经有了?那就不用了。"


3. @ConditionalOnProperty 🔧

作用: 根据配置文件中的属性决定是否创建

@Configuration
@ConditionalOnProperty(
    name = "app.feature.enabled",
    havingValue = "true"
)
public class FeatureConfig {
    
    @Bean
    public AwesomeFeature awesomeFeature() {
        return new AwesomeFeature();
    }
}

在application.properties中:

app.feature.enabled=true

生活比喻:

"你在菜单上勾选了'加辣'🌶️,我才给你加辣椒。没勾?那就不加。"


4. @ConditionalOnBean 🤝

作用: 当容器中存在指定Bean时,才创建

@Configuration
public class ServiceConfig {
    
    @Bean
    @ConditionalOnBean(DataSource.class)
    public UserService userService(DataSource dataSource) {
        return new UserService(dataSource);
    }
}

生活比喻:

"只有你买了游戏机🎮,我才给你配手柄🕹️。没游戏机?手柄也没用。"


5. @ConditionalOnExpression 🧮

作用: 根据SpEL表达式的结果决定

@Configuration
@ConditionalOnExpression("${app.mode} == 'dev' and ${app.debug} == true")
public class DebugConfig {
    
    @Bean
    public DebugTool debugTool() {
        return new DebugTool();
    }
}

生活比喻:

"如果你是VIP会员💎 并且 今天是你生日🎂,就送你礼物🎁。"


🎯 第五幕:手把手教你自定义Condition

场景:只在开发环境启用某个功能

Step 1:创建自定义注解

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Conditional(OnDevEnvironmentCondition.class)
public @interface ConditionalOnDevEnvironment {
}

Step 2:实现Condition接口

public class OnDevEnvironmentCondition implements Condition {
    
    @Override
    public boolean matches(ConditionContext context, 
                          AnnotatedTypeMetadata metadata) {
        
        // 获取环境信息
        Environment env = context.getEnvironment();
        String[] activeProfiles = env.getActiveProfiles();
        
        // 判断是否包含"dev"环境
        for (String profile : activeProfiles) {
            if ("dev".equalsIgnoreCase(profile)) {
                System.out.println("🎉 检测到开发环境,启用调试功能!");
                return true;
            }
        }
        
        System.out.println("❌ 非开发环境,禁用调试功能。");
        return false;
    }
}

Step 3:使用自定义注解

@Configuration
public class AppConfig {
    
    @Bean
    @ConditionalOnDevEnvironment
    public DebugController debugController() {
        return new DebugController();
    }
}

效果:

  • 开发环境(dev):✅ 创建DebugController
  • 生产环境(prod):❌ 不创建DebugController

🌟 第六幕:高级玩法 - ConfigurationCondition

什么是ConfigurationCondition?

普通的Condition有个小问题:它不关心Bean的创建顺序。

ConfigurationCondition是个升级版,它可以指定什么时候检查条件!

public interface ConfigurationCondition extends Condition {
    
    ConfigurationPhase getConfigurationPhase();
    
    enum ConfigurationPhase {
        PARSE_CONFIGURATION,  // 解析@Configuration时检查
        REGISTER_BEAN         // 注册Bean时检查
    }
}

实战案例:

public class OnSpecialBeanCondition 
       implements ConfigurationCondition {
    
    @Override
    public ConfigurationPhase getConfigurationPhase() {
        // 在注册Bean阶段检查,确保依赖的Bean已经注册
        return ConfigurationPhase.REGISTER_BEAN;
    }
    
    @Override
    public boolean matches(ConditionContext context, 
                          AnnotatedTypeMetadata metadata) {
        // 检查容器中是否存在某个Bean
        return context.getBeanFactory()
                     .containsBean("specialBean");
    }
}

生活比喻:

普通Condition: "我现在就要检查!" 🏃‍♂️
ConfigurationCondition: "我等一会儿,等其他人都到齐了再检查。" 🧘‍♂️


🎬 第七幕:ConditionContext提供了哪些信息?

ConditionContext就像一个"信息情报员"🕵️,它能告诉你:

public interface ConditionContext {
    
    // 1. 获取Bean定义注册表(查看已注册的Bean)
    BeanDefinitionRegistry getRegistry();
    
    // 2. 获取Bean工厂(查看Bean实例)
    ConfigurableListableBeanFactory getBeanFactory();
    
    // 3. 获取环境信息(配置、系统属性)
    Environment getEnvironment();
    
    // 4. 获取资源加载器(读取文件)
    ResourceLoader getResourceLoader();
    
    // 5. 获取类加载器(检查类是否存在)
    ClassLoader getClassLoader();
}

实战案例:根据多个条件综合判断

public class SmartCondition implements Condition {
    
    @Override
    public boolean matches(ConditionContext context, 
                          AnnotatedTypeMetadata metadata) {
        
        // 条件1:检查类是否存在
        try {
            context.getClassLoader()
                  .loadClass("com.mysql.cj.jdbc.Driver");
        } catch (ClassNotFoundException e) {
            System.out.println("❌ MySQL驱动不存在");
            return false;
        }
        
        // 条件2:检查配置属性
        String dbUrl = context.getEnvironment()
                             .getProperty("spring.datasource.url");
        if (dbUrl == null || dbUrl.isEmpty()) {
            System.out.println("❌ 数据库URL未配置");
            return false;
        }
        
        // 条件3:检查是否已存在DataSource Bean
        if (context.getBeanFactory().containsBean("dataSource")) {
            System.out.println("❌ DataSource已存在");
            return false;
        }
        
        System.out.println("✅ 所有条件满足,可以创建Bean!");
        return true;
    }
}

🎪 第八幕:Spring Boot自动配置的秘密武器

Spring Boot的自动配置魔法,核心就是@Conditional!

案例:DataSource自动配置

@Configuration
@ConditionalOnClass(DataSource.class)  // 1. 有DataSource类
@EnableConfigurationProperties(DataSourceProperties.class)
public class DataSourceAutoConfiguration {
    
    @Bean
    @ConditionalOnMissingBean  // 2. 用户没自定义DataSource
    @ConditionalOnProperty(    // 3. 配置文件中指定了URL
        name = "spring.datasource.url"
    )
    public DataSource dataSource(DataSourceProperties properties) {
        return DataSourceBuilder
                .create()
                .url(properties.getUrl())
                .username(properties.getUsername())
                .password(properties.getPassword())
                .build();
    }
}

工作流程图:

检查DataSource类是否存在?
    ↓ YES
检查用户是否自定义了DataSource?
    ↓ NO
检查配置文件中是否有spring.datasource.url?
    ↓ YES
✅ 自动创建DataSource!

生活比喻:

就像智能家居系统:

  1. 你家有智能灯泡吗?💡 → 有
  2. 你手动开灯了吗? → 没有
  3. 现在天黑了吗?🌙 → 是的
    ✅ 好的,我帮你自动开灯!

🎯 第九幕:实战案例 - 多数据源自动切换

场景描述

假设我们的系统需要:

  • 生产环境:使用MySQL
  • 测试环境:使用H2内存数据库
  • 开发环境:使用本地MySQL

实现步骤

1. 定义条件注解

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Conditional(OnProductionCondition.class)
public @interface ConditionalOnProduction {
}

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Conditional(OnTestCondition.class)
public @interface ConditionalOnTest {
}

2. 实现Condition

public class OnProductionCondition implements Condition {
    
    @Override
    public boolean matches(ConditionContext context, 
                          AnnotatedTypeMetadata metadata) {
        String[] profiles = context.getEnvironment()
                                  .getActiveProfiles();
        return Arrays.asList(profiles).contains("prod");
    }
}

public class OnTestCondition implements Condition {
    
    @Override
    public boolean matches(ConditionContext context, 
                          AnnotatedTypeMetadata metadata) {
        String[] profiles = context.getEnvironment()
                                  .getActiveProfiles();
        return Arrays.asList(profiles).contains("test");
    }
}

3. 配置不同环境的数据源

@Configuration
public class MultiDataSourceConfig {
    
    @Bean
    @ConditionalOnProduction
    public DataSource productionDataSource() {
        System.out.println("🏭 创建生产环境MySQL数据源");
        HikariDataSource dataSource = new HikariDataSource();
        dataSource.setJdbcUrl("jdbc:mysql://prod-server:3306/db");
        dataSource.setUsername("prod_user");
        dataSource.setPassword("prod_pass");
        return dataSource;
    }
    
    @Bean
    @ConditionalOnTest
    public DataSource testDataSource() {
        System.out.println("🧪 创建测试环境H2数据源");
        return new EmbeddedDatabaseBuilder()
                .setType(EmbeddedDatabaseType.H2)
                .build();
    }
    
    @Bean
    @ConditionalOnMissingBean(DataSource.class)
    public DataSource defaultDataSource() {
        System.out.println("💻 创建默认开发环境数据源");
        HikariDataSource dataSource = new HikariDataSource();
        dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/dev_db");
        dataSource.setUsername("root");
        dataSource.setPassword("root");
        return dataSource;
    }
}

效果:

  • java -jar app.jar --spring.profiles.active=prod → 🏭 生产数据源
  • java -jar app.jar --spring.profiles.active=test → 🧪 测试数据源
  • java -jar app.jar → 💻 开发数据源

🔍 第十幕:常见坑点与注意事项

⚠️ 坑点1:条件判断的时机

@Configuration
public class BadExample {
    
    @Bean
    public BeanA beanA() {
        return new BeanA();
    }
    
    @Bean
    @ConditionalOnBean(BeanB.class)  // ❌ BeanB还没创建呢!
    public BeanC beanC() {
        return new BeanC();
    }
    
    @Bean
    public BeanB beanB() {
        return new BeanB();
    }
}

解决方案: 使用ConfigurationCondition并指定REGISTER_BEAN阶段


⚠️ 坑点2:条件注解的组合

多个条件注解是AND关系

@Bean
@ConditionalOnClass(DataSource.class)
@ConditionalOnProperty(name = "db.enabled", havingValue = "true")
public DataSource dataSource() {
    // 必须同时满足:
    // 1. DataSource类存在
    // 2. db.enabled=true
    return new HikariDataSource();
}

⚠️ 坑点3:调试条件注解

启用自动配置报告:

# application.properties
debug=true

或者启动时加参数:

java -jar app.jar --debug

输出示例:

============================
CONDITIONS EVALUATION REPORT
============================

Positive matches:
-----------------
   DataSourceAutoConfiguration matched:
      - @ConditionalOnClass found required class 'javax.sql.DataSource' (OnClassCondition)
      - @ConditionalOnProperty (spring.datasource.url) matched (OnPropertyCondition)

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

🎓 第十一幕:最佳实践

✅ 1. 语义化的条件注解

不好的方式:

@Conditional(MyComplexCondition.class)

好的方式:

@ConditionalOnDevEnvironment
@ConditionalOnFeatureEnabled("awesome-feature")

✅ 2. 条件类要易于测试

public class OnDevEnvironmentCondition implements Condition {
    
    @Override
    public boolean matches(ConditionContext context, 
                          AnnotatedTypeMetadata metadata) {
        return isDevEnvironment(context.getEnvironment());
    }
    
    // 提取为独立方法,便于单元测试
    protected boolean isDevEnvironment(Environment env) {
        String[] profiles = env.getActiveProfiles();
        return Arrays.asList(profiles).contains("dev");
    }
}

✅ 3. 提供清晰的日志

public class SmartCondition implements Condition {
    
    private static final Logger log = LoggerFactory.getLogger(SmartCondition.class);
    
    @Override
    public boolean matches(ConditionContext context, 
                          AnnotatedTypeMetadata metadata) {
        boolean result = doCheck(context);
        
        if (result) {
            log.info("✅ 条件满足:{}", getDescription());
        } else {
            log.info("❌ 条件不满足:{}", getReason());
        }
        
        return result;
    }
}

🎉 总结:@Conditional的威力

特性说明表情
灵活性根据任意条件决定Bean是否创建🦎
可组合多个条件注解可以组合使用🧩
强大Spring Boot自动配置的核心💪
易扩展可以自定义各种条件🔧
智能让应用自动适应不同环境🧠

🚀 课后作业

  1. 初级: 创建一个@ConditionalOnWeekend注解,只在周末创建某个Bean
  2. 中级: 实现一个根据JDK版本决定Bean创建的条件注解
  3. 高级: 设计一个多条件组合的注解,支持OR和AND逻辑

📚 参考资料

  • Spring Framework官方文档
  • Spring Boot自动配置源码
  • 《Spring揭秘》

最后的彩蛋: 🎁

Spring的@Conditional就像生活中的"智能助手",它让你的应用变得超级聪明!

记住这句话:

"不是所有的Bean都要创建,而是根据条件智能地创建!" 💡


关注我,下期更精彩! 🌟

用代码改变世界,用幽默改变编程! 😎


#Spring #条件注解 #自动配置 #最佳实践