Spring Boot:让你的应用优雅的按需加载Bean

在日常开发过程中,我们的应用一般有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

  • 7
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值