在日常开发过程中,我们的应用一般有3个最基本的环境:开发环境、测试环境和线上环境,不同的环境会有不同的配置,假设3个环境在SpringBoot应用中分别对应3个配置application-dev.properties、application-test.properties和application-prod.properties。你可能遇到过类似下面的场景:

1. 有个操作,只能在开发环境执行
2. 又有个操作,不能在开发环境执行,只能在其他非开发环境执行
3. 又又有个操作,需要在开发或测试环境执行,线上环境不能做
4. 叒有个操作,不论是什么环境,在同时满足A和B两个配置条件的时候才能执行
5. ... ...

我们可能最先想到通过类似硬编码判断的方式:在包含对应操作的Bean中通过@Value注入配置,再通过判断该值是否满足条件确定逻辑是否应该执行。 这样也可以实现,但是至少有两个硬伤:

1. 不易维护
假如有多个类似的操作,当执行条件发生变化的时候,要对应修改多个地方,容易遗漏;尤其是当条件变得稍微复杂的时候,要将每个对应的地方改成一致的条件判断也容易产生错误。
2. 不优雅
对于一个开发者来说,产出的代码不优雅,这不能忍,其它就什么都不用说了。

下面记录一下基于@Conditional优雅 的实现方式,并实现一些简单案例。案例使用 之前文章 的源码,并在此基础上进行改造。

1 初级用法

根据配置文件确定是否初始化Bean,解决上面第一个问题:1.有个操作,只能在开发环境执行。这里我们通过@ConditionalOnProperty注解实现,创建自定义组件类ActionInDevEnv并添加@ConditionalOnProperty注解,完整代码如下:

/**
 * 自定义Bean组件,只在配置 spring.profiles.active 值为 dev 的情况下加载此组件,
 * 如果没有配置spring.profiles.active默认也看做是满足要求
 */
@ConditionalOnProperty(value = "spring.profiles.active", havingValue = "dev", matchIfMissing = true)
@Component
public class ActionInDevEnv {
    private Logger logger = LoggerFactory.getLogger(this.getClass());
    @Value("${spring.profiles.active:none}")
    private String env;

    /**
     * 组件Bean创建完成后执行的操作
     */
    @PostConstruct
    private void run() {
        this.logger.info("当前操作正在 [ {} ] 环境中执行。。。", this.env);
    }
}

application.properties配置文件中,我们指定了启动配置使用application-dev.properties配置文件,如下图:
在这里插入图片描述
启动SpringBoot应用程序,可看到组件初始化完成后执行run方法,打印日志如下:在这里插入图片描述
假如在application.properties配置文件中指定的不是application-dev.properties配置文件(不满足ConditionalOnProperty指定的条件),则无法看到上面的日志信息,因为在应用启动过程中并没有创建这个Bean。

我们可以验证一下没有创建这个Bean,这次我们将注解加到之前创建的UserRepository接口上,并设定在prod环境下生效,在 前面的文章 中我们创建了ApplicationRunnerInit类,该类会在应用启动后查询用户数量,这时会使用到UserRepository接口,调整接口如下:

注:@ConditionalOnProperty包含元注解@Target({ ElementType.TYPE, ElementType.METHOD }),表示可以作用在任何类和方法上,在应用初始化上下文的时候使用了@ConditionalOnProperty注解的类或方法都会根据设定检查组件类或者Bean方法是否满足创建Bean的条件

@ConditionalOnProperty(value = "spring.profiles.active", havingValue = "prod", matchIfMissing = true) //添加了这一行
@Repository
public interface UserRepository extends PagingAndSortingRepository<User, Integer>, JpaRepository<User, Integer> {
    Optional<User> findByUsername(String userName);

    int countAllByUsername(String userName);

    @Query("select u from User u where u.realName = ?#{principal.realName}")
    List<User> findCurrentUserBySpel();

    @Query(value = "select * from h_user u where u.real_name = ?#{principal.realName}", nativeQuery = true)
    List<User> findCurrentUserByNativeQuery();
}

此时SpringBoot应用将会启动失败,报错如下,在初始化上下文的时候出现异常,原因是找不到UserRepositoryBean,因为不满足ConditionalOnProperty设定的条件所以没有创建这个Bean到应用上下文中。假如将注解的havingValue调整为dev,再重新启动则启动成功。
在这里插入图片描述

2 组合Conditional

介绍逻辑的Conditional。

2.1 非

解决上面提到的第二个问题 2. 又有个操作,不能在开发环境执行,只能在其他非开发环境执行。这里通过@ConditionalOnExpression注解实现,该注解接收一个SpEL表达式参数,满足表达式设定条件即满足要求,关于SpEL表达式的更多说明可参考 官方文档

为了达到模拟效果,我们执行以下步骤:

2.1.1 创建测试环境的应用配置

创建一个测试环境的应用配置application-test.properties,内容和application-dev.properties完全相同,然后在application.properties中指定启动环境为test:spring.profiles.active=test

2.1.2 创建ActionInNoneDevEnv类

和第1步类似,创建自定义组件类ActionInNoneDevEnv并添加@ConditionalOnExpression注解,完整代码如下:

/**
 * 自定义只在配置 spring.profiles.active 值不为 dev 的情况下加载此组件,
 * 如果没有配置spring.profiles.active默认也看做是dev
 */
@ConditionalOnExpression("'${spring.profiles.active:dev}' != 'dev'")
@Component
public class ActionInNoneDevEnv {
    private Logger logger = LoggerFactory.getLogger(this.getClass());
    @Value("${spring.profiles.active:none}")
    private String env;

    /**
     * 组件Bean创建完成后执行的操作
     */
    @PostConstruct
    private void run() {
        this.logger.info("ActionInNoneDevEnv 当前操作正在 [ {} ] 环境中执行。。。", this.env);
    }
}

启动应用,可通过日志查看ActionInNoneDevEnvrun方法正常运行,如下图。如果在application.properties中指定启动环境为dev则该组件不会被加载到上下文中。
在这里插入图片描述

2.2 与

即多个条件同时满足时看做是满足要求,解决第四个问题 4. 叒有个操作,不论是什么环境,在同时满足A和B两个配置条件的时候才能执行

表示关系的条件同样可以通过ConditionalOnExpression注解用SpEL表达式实现,如下,表示crane.condition.a配置的值为a并且crane.condition.b配置的值为b的时候才加载对应的Bean。

@ConditionalOnExpression("'${crane.condition.a}' == 'a' and '${crane.condition.b}' == 'b'")

2.3 或

即有多个条件只需要满足一个条件即可,解决上面第三个问题 3. 又又有个操作,需要在开发或测试环境执行,线上环境不能做

条件不能通过多个Conditional并列实现,可通过SpEL表达式方式实现,如下:

@ConditionalOnExpression("'${spring.profiles.active}' == 'dev' or '${spring.profiles.active}' == 'test'")

3 自定义Conditional

对于一些基于配置的单一条件可通过ConditionalOnProperty实现,复杂的多条件可通过强大的ConditionalOnExpression实现。但是从重用的角度考虑,如果有某些Conditional需要在多处重用,而执行逻辑又不方便写在一个组件中的情况,自定义Conditional的方式将是最佳的选择。

3.1 自定义单一条件

自定义单一条件Conditional可通过在自定义注解上添加框架现有Conditional注解作为元注解的方式实现。比如这样一个场景:在某个配置条件满足的时候需要执行一些检查。可通过下面步骤实现自定义Conditional。

3.1.1 添加配置

在当前环境配置文件下添加配置crane.condition.check=crane,如下图:
在这里插入图片描述

3.1.2 创建自定义注解

创建注解ConditionalOnRunCheck,在自定义注解上添加元注解@ConditionalOnProperty(value = "crane.condition.check", havingValue = "crane"),表示在配置crane.condition.check的值为crane生效。完整代码如下:

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
//添加元注解,设定在配置crane.condition.check的值为crane生效
@ConditionalOnProperty(value = "crane.condition.check", havingValue = "crane")
public @interface ConditionalOnRunCheck {
}

3.1.3 创建检查类组件

创建ConditionalCheck组件,模拟满足条件时执行一些检查(这里打印一条日志信息)。在组件上使用上一步创建的自定义注解ConditionalOnRunCheck,因为自定义注解中元注解已经包含具体的条件,所以这里只做自定义注解声明即可,完整代码如下:


/**
 * 条件检查类组件
 */
@ConditionalOnRunCheck //使用自定义的Conditional
@Component
public class ConditionalCheck {
    private Logger logger = LoggerFactory.getLogger(this.getClass());
    @Value("${spring.profiles.active:none}")
    private String env;

    @Value("${crane.condition.check:none}")
    private String checkPhase;
    
    /**
     * 组件Bean创建完成后执行的操作
     */
    @PostConstruct
    private void check() {
        this.logger.info("ConditionalCheck 正在 [ {} ] 环境中执行验证,验证条件: {}", this.env, this.checkPhase);
    }
}

3.1.4 执行效果

完成上述步骤后,启动SpringBoot应用,日志打印信息如下,满足预期要求。这种情况,当有多处使用当前自定义注解,而注解对应的检查条件发生变化时,只需要修改自定义注解中元注解的条件这一处即可
在这里插入图片描述

3.2 自定义多条件(与)

当实现自定义多个条件同时满足的Conditional时,也可参照上一步,只需要在自定义注解上添加多个对应条件的元注解即可;另外也可实现框架的AllNestedConditions抽象类。

3.2.1 多个元注解方式

这种方式有一个局限性,即目标组件不能使用多个同种的Conditional,如下图使用两个ConditionalOnProperty会报错,所以这种只适合多个不同类型注解的方式。
在这里插入图片描述

3.2.2 AllNestedConditions

AllNestedConditions支持组合多个逻辑的Conditional,如果有其中任意一个不满足条件也视作不匹配。对于2.2中的通过ConditionalOnExpression实现两个组合条件的情况可通过下面方式实现。

3.2.2.1 添加配置

如下图,添加两个配置表示AB两个条件:
在这里插入图片描述

3.2.2.2 添加自定义Condition

创建OnCustomAndCondition条件类继承AllNestedConditions,其中包含两个内部静态方法(方法可自定义名称)分别添加Conditional注解对应AB两个条件。完整代码如下:


public class OnCustomAndCondition extends AllNestedConditions {
    public OnCustomAndCondition() {
        super(ConfigurationPhase.REGISTER_BEAN);
    }

    @ConditionalOnProperty(value = "crane.condition.a", havingValue = "a")
    static class conditionA {

    }

    @ConditionalOnProperty(value = "crane.condition.b", havingValue = "b")
    static class conditionB {

    }
}
3.2.2.3 添加自定义Conditional注解

创建ConditionalOnCustomAnd注解,使用上一步中的OnCustomAndCondition作为约束条件,完整代码如下:

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(OnCustomAndCondition.class)
public @interface ConditionalOnCustomAnd {
}
3.2.2.4 使用自定义Conditional

创建ConditionalCustomAnd测试组件类,模拟AB两个条件满足时执行操作。完整代码如下:

/**
 * 自定义与多条件组件
 */
//@Conditional(OnCustomAndCondition.class) //使用自定义的Conditional
@ConditionalOnCustomAnd
@Component
public class ConditionalCustomAnd {
    private Logger logger = LoggerFactory.getLogger(this.getClass());
    @Value("${spring.profiles.active:none}")
    private String env;

    /**
     * 组件Bean创建完成后执行的操作
     */
    @PostConstruct
    private void action() {
        this.logger.info("ConditionalCustomAnd 正在 [ {} ] 环境中执行", this.env);
    }
}
3.2.2.5 查看效果

完成以上步骤,重新启动SpringBoot应用,可查看日志打印信息如下,当A B任意一个条件不满足时,组件Bean也不会加载到应用上下文中。
在这里插入图片描述

注:步骤3.2.2.3创建自定义Conditional这一步可省略,然后在组件Bean上直接使用@Conditional(OnCustomAndCondition.class)也可以实现同样效果。但是为了更规范化,通常会在3.2.2.3这一步创建一个自定义的Conditional,然后在组件Bean上使用自定义的Conditional,元注解就不需要携带任何参数了。

3.3 自定义多条件(或)(非)

实现过程及原理和步骤3.2.2完全相同,区别是多个逻辑的Condition实现于AnyNestedCondition类,多个逻辑的Condition实现于NoneNestedConditions类,而不是AllNestedConditions

以上主要通过应用的配置实现按需加载Bean组件,另外框架内还有其他多种类型的内置Conditional,如下图,可根据实际情况使用。
在这里插入图片描述

常用Conditional

Conditional 作用
ConditionalOnBean 匹配应用上下文已存在某个Bean的情况
ConditionalOnClass 匹配在classpath中存在某个类的情况
ConditionalOnCloudPlatform 匹配应用在某个具体平台运行时的情况
ConditionalOnExpression 基于SpEL表达式判断是否满足条件
ConditionalOnJava 匹配使用某个Java版本的情况
ConditionalOnJndi 匹配某个资源通过 JNDI 加载后的情况
ConditionalOnMissingBean 匹配应用上下文不存在某个Bean的情况
ConditionalOnMissingClass 匹配在classpath中不存在某个类的情况
ConditionalOnNotWebApplication 匹配非Web应用
ConditionalOnProperty 匹配properties或yml配置文件中包含指定配置值的情况,使用比较频繁
ConditionalOnResource 匹配在classpath 中存在某个资源文件的时候
ConditionalOnSingleCandidate 一般用于自动配置类中通过Bean工厂方法创建Bean的情况
ConditionalOnWebApplication 匹配Web应用

参考

[1] 创建自己的自动配置
[2] SpEL

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐