Spring Boot:让你的应用优雅的按需加载Bean
在Spring Boot中,如何优雅的按需加载Bean,执行相应的操作?你可能遇到过类似下面的场景:1. 有个操作,只能在开发环境执行2. 又有个操作,不能在开发环境执行,只能在其他非开发环境执行3. 又又有个操作,需要在开发或测试环境执行,线上环境不能做4. 叒有个操作,不论是什么环境,在同时满足A和B两个配置条件的时候才能执行5. ... ...
在日常开发过程中,我们的应用一般有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);
}
}
启动应用,可通过日志查看ActionInNoneDevEnv的run方法正常运行,如下图。如果在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 添加配置
如下图,添加两个配置表示A和B两个条件:
3.2.2.2 添加自定义Condition
创建OnCustomAndCondition条件类继承AllNestedConditions,其中包含两个内部静态方法(方法可自定义名称)分别添加Conditional注解对应A和B两个条件。完整代码如下:
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测试组件类,模拟A和B两个条件满足时执行操作。完整代码如下:
/**
* 自定义与多条件组件
*/
//@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应用 |
参考
更多推荐

所有评论(0)