暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

在Spring Boot中使用条件化的Bean

背井 2021-03-03
3139

在构建spring boot项目时,有时我们希望只有在满足某些条件时,一些bean或模块才会被加载进应用上下文(application context)中。目的也许是为了在测试时禁用某些bean, 也许是对运行环境中的某些属性做出反应。

Spring 为此引入了@Conditional
注解,方便我们为应用上下文的一部分提供自定义条件。Spring Boot 以此为基础,提供了一些预定义好的条件,避免我们重造轮子。

本文将给出一些使用场景,说明为什么要使用条件化加载的bean。我们将看到如何应用这些条件以及Spring Boot提供了哪些条件。为使本文更完整,我们也将自己动手实现一个自定义的条件。


 为什么我们需要条件化的bean?


Spring的应用上下文(application context)包含了运行时需要的所有bean的对象关系图。而Spring提供的@Conditional
注解,则允许我们定义在何种条件下某个特定的bean会被包含进对象关系中。

那么,为什么在某些条件下我们要包含或排除掉一些bean?

从我的经验看,最常见的情况,是某些bean在测试环境下不能正常工作。它们可能需要连接到远程系统或服务器,而在测试时,这些环境不可用。所以,我们想要对测试代码进行模块化(https://reflectoring.io/testing-verticals-and-layers-spring-boot/), 在测试时屏蔽或替换掉一些bean。

Another use case is that we want to enable or disable a certain cross-cutting concern. Imagine that we have built a module that configures security. During developer tests, we don’t want to type in our usernames and passwords every time, so we flip a switch and disable the whole security module for local tests.

另一种情况,是我们想启用/禁用一个特定的横切关注点(cross-cutting concern)。假使我们创建了一个用来作安全配置的模块。在开发测试阶段,我们不想每次都输入用户名和密码,我们便设置了一个开关,为本地测试关闭这个安全模块。

同样,有些bean我们希望当某些外部资源可用时才加载,否则它们没法工作。例如,当logback.xml
文件在类路径(classpath)中时,我们才配置自己的logback logger。

后续讨论中将看到更多例子。

 声明条件化的bean


任何声明Spring bean的地方,我们都可以加一个条件(condition)。除非条件满足,bean不会被加载进应用上下文中。要声明一个条件,我们可以使用下面描述的任何一个@Conditional...
注解。

我们先看看如何对一个特定的bean添加条件。


 条件化的@Bean

如果我们对单个@Bean
添加条件,这个bean只会在条件满足时被加载:

@Configuration
class ConditionalBeanConfiguration {

@Bean
@Conditional... // <--
ConditionalBean conditionalBean(){
return new ConditionalBean();
};
}


 条件化的 @Configuration

如果我们对@Configuration
添加注解,该配置中的所有bean,只会在条件满足时被加载:

@Configuration
@Conditional... // <--
class ConditionalConfiguration {

@Bean
Bean bean(){
...
};

}


 条件化的 @Component

最后,条件可以加给@Component
@Service
@Repository
 或 @Controller
 这类构造型注解:

@Component
@Conditional... // <--
class ConditionalComponent {
}

 预定义的条件(Pre-Defined Conditions)


Spring Boot提供了一些预先定义好的 @ConditionalOn...
 注解,开箱即用。让我们依次看看它们。

@ConditionalOnProperty

@ConditionalOnProperty
 注解,在我的经验中,是Spring Boot项目中最常用的条件注解。它允许根据特定的环境参数条件化地加载bean:

@Configuration
@ConditionalOnProperty(
value="module.enabled",
havingValue = "true",
matchIfMissing = true)
class CrossCuttingConcernModule {
...
}


CrossCuttingConcernModule
 只有在 module.enabled
属性的值为true
时才会被加载。如果该属性不存在也会被加载,因为我们定义了matchIfMissing
 为 true
以这种方式,我们创建了一个默认会被加载的模块,除非我们有别的决定

同样,我们也可以为诸如安全或定时任务这样的横切关注点创建可在特定(测试)环境中禁用的模块。

@ConditionalOnExpression

如果我们需要基于多个参数的复杂条件,我们可以使用@ConditionalOnExpression

@Configuration
@ConditionalOnExpression(
"${module.enabled:true} and ${module.submodule.enabled:true}"
)
class SubModule {
...
}


只有在module.enabled
 和 module.submodule.enabled
的值都为true
时,SubModule
才会被加载。在属性后附上:true
是为了告诉Spring当属性不存在时,true
作为默认值。我们可以使用Spring表达式(Spring Expression Language)的全部功能。

按此方式,我们可以创建这样一种模块: 当父模块被禁用时,它也被禁用;也可以父模块启用时,它被禁用。

@ConditionalOnBean

有时,我们希望只有在某些bean存在时,才加载另一些bean:

@Configuration
@ConditionalOnBean(OtherModule.class)
class DependantModule {
...
}


只有在应用上下文中有OtherModule
这个类的bean存在时,DependantModule
才会被加载。除了用bean class外,我们也可以使用bean的名称(bean name)。

这样,我们可以定义特定模块间的依赖。只有在一模块的某个特定bean存在时,另一个模块才允许被加载。

@ConditionalOnMissingBean

类似地,我们使用@ConditionalOnMissingBean
来表达只有在某个bean不存在时,才加载另一个bean:

@Configuration
class OnMissingBeanModule {

@Bean
@ConditionalOnMissingBean
DataSource dataSource() {
return new InMemoryDataSource();
}
}


在本例中,只有应用上下文中没有datasource时,我们才会注入 in-memory datasource(内存数据库)。这和Spring Boot在测试环境中会提供内存数据库是相似的。

@ConditionalOnResource

如果我们希望一个bean只有在类路径中存在某个资源时才加载,我们可以使用 @ConditionalOnResource

@Configuration
@ConditionalOnResource(resources = "/logback.xml")
class LogbackModule {
...
}


只有当类路径中发现logback的配置文件时,才会加载LogbackModule
。这样,我们可以创建类似的模块,只有相应的配置文件存在时,这类模块才会被加载

 其它条件

上面描述的条件注解是任何Spring Boot应用中都较常用的。Spring Boot还提供了更多的条件注解。我们很少使用它们,它们更适合于框架开发(framework development)而非应用开发(application development)。 实际上Spring Boot本身就依赖于这些注解。我们简单的看一下。

@ConditionalOnClass

只有在类路径中有某个class时,才加载给定的bean:

@Configuration
@ConditionalOnClass(name = "this.clazz.does.not.Exist")
class OnClassModule {
...
}


@ConditionalOnMissingClass

只有在类路径中没有某个class时,才加载给的bean:

@Configuration
@ConditionalOnMissingClass(value = "this.clazz.does.not.Exist")
class OnMissingClassModule {
...
}


@ConditionalOnJndi

只有在某个JNDI资源存在时,才加载bean:

@Configuration
@ConditionalOnJndi("java:comp/env/foo")
class OnJndiModule {
...
}

@ConditionalOnJava

只有在给版本的java下,才加载bean:

@Configuration
@ConditionalOnJava(JavaVersion.EIGHT)
class OnJavaModule {
...
}

@ConditionalOnSingleCandidate

有点像 @ConditionalOnBean
, 但是只有在给定class的代表只有一个时(即该类,包括子类,只有一个实例时),bean才会被加载。通常用在auto-configurations中。

@Configuration
@ConditionalOnSingleCandidate(DataSource.class)
class OnSingleCandidateModule {
...
}


@ConditionalOnWebApplication

当前项目是web应用时,才加载该bean:

@Configuration
@ConditionalOnWebApplication
class OnWebApplicationModule {
...
}


@ConditionalOnNotWebApplication

当前项目不是web应用时,加载该bean:

@Configuration
@ConditionalOnNotWebApplication
class OnNotWebApplicationModule {
...
}


@ConditionalOnCloudPlatform

在特定的云平台下才加载该bean:

@Configuration
@ConditionalOnCloudPlatform(CloudPlatform.CLOUD_FOUNDRY)
class OnCloudPlatformModule {
...
}

 自定义条件注解

除了上面讲到的条件注解,我们也可以创建自己的,还可以通过逻辑操作合并多个条件。

 定义自己的条件

想象我们有一些Spring beans需要和操作系统进行原生地通信。不同的操作系统,我们要加载不同的bean。

我们做一个这样的条件,让bean只在unix机器上加载。为此,我们得实现org.springframework.context.annotation.Condition
接口:

class OnUnixCondition implements Condition {

@Override
public boolean matches(
ConditionContext context,
AnnotatedTypeMetadata metadata) {
return SystemUtils.IS_OS_LINUX;
}
}


我们只是简单地使用了Apache Commons库的SystemUtils
类,来决定我们是否在类unix机器上。如果需要,我们还可以包含更复杂的逻辑,使用当前应用上下文(ConditionContext
)或注解(AnnotatedTypeMetadata
)的信息。

现在就能把上面的条件用于@Conditional
注解了:

@Bean
@Conditional(OnUnixCondition.class)
UnixBean unixBean() {
return new UnixBean();
}

 使用 逻辑或(OR) 合并多个条件

如果我们想要使用"OR"将多个条件合并为一个条件,我们可以继承AnyNestedCondition

class OnWindowsOrUnixCondition extends AnyNestedCondition {

OnWindowsOrUnixCondition() {
super(ConfigurationPhase.REGISTER_BEAN);
}

@Conditional(OnWindowsCondition.class)
static class OnWindows {}

@Conditional(OnUnixCondition.class)
static class OnUnix {}

}


这里,我们的创建了一个条件,它既适用于windows,也适用于unix。

AnyNestedCondition
 会分析 @Conditional
,将它们用"OR"进行合并。

可以像使用其它条件一样使用这个条件:

@Bean
@Conditional(OnWindowsOrUnixCondition.class)
WindowsOrUnixBean windowsOrUnixBean() {
return new WindowsOrUnixBean();
}

 如果发现你写的AnyNestedCondition
AllNestedConditions
不生效

检查传递给super()
ConfigurationPhase
参数。如果你想把合并后的条件用于@Configuration
 bean,使用PARSE_CONFIGURATION
。如果只是想传给普通的bean,使用上例中的REGISTER_BEAN
。Spring Boot需要这个区分,以便在应用启动时在合适的时间判断条件。

 使用逻辑与(AND)合并条件

如果想把多个条件用"AND"合并,我们只需在一个bean上简单地用上多个@Conditional...
注解。它们会被自动地用"AND"合并,这样,只要其中一个条件不满足,bean就不会被加载:

@Bean
@ConditionalOnUnix
@Conditional(OnWindowsCondition.class)
WindowsAndUnixBean windowsAndUnixBean() {
return new WindowsAndUnixBean();
}


这个bean怕是永远不会被加载了,除非有人创造出了我还未听说过的Windows/Unix杂合体。

注意,@Conditional
注解在方法或类上最多只能用一次。如果想合并多个注解,必须使用(自定义的)@ConditionalOn...
注解,它们无此限制。下面将探索怎么创建@ConditionalOnUnix
注解。

另外,如果想用"AND"合并多个条件,我们可以继承AllNestedConditions
类,它和上面说过的AnyNestedConditions
的实现方式一样。

 使用逻辑非(NOT)合并条件

与 AnyNestedCondition
 以及 AllNestedConditions
类似,我们可以继承 NoneNestedCondition
,这样,只有当所有条件都不满足时,bean才会被加载。

 自定义一个 @ConditionalOn… 注解

我们可以为任何条件创建一个自定义注解。只需要在自己的注解上注解上@Conditional
(哈哈):

@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(OnLinuxCondition.class)
public @interface ConditionalOnUnix {}


当给bean配置上这个新注解后,Spring会去分析注解上的@Conditional
注解:

@Bean
@ConditionalOnUnix
LinuxBean linuxBean(){
return new LinuxBean();
}

 总结

有了@Conditional
,又能创建自定义的@Conditional...,
Spring给了我们控制应用上下文的强大能力。

Spring Boot 基于@Conditional
提供了很多便捷的@ConditionalOn...
注解,AllNestedConditions
,AnyNestedCondition
或 NoneNestedCondition
让条件合并也成为可能。这些工具一起,允许我们对生产和测试等进行模块化的开发。(见 https://reflectoring.io/spring-boot-modules/ 及 https://reflectoring.io/testing-verticals-and-layers-spring-boot/)


权力越大,责任越大。我们要注意别把项目中加的到处都是条件,以免自己都不知道有哪些bean被加载了。

项目代码在github上: https://github.com/thombergs/code-examples/tree/master/spring-boot/conditionals


本文译自: Conditional Beans with Spring Boot, https://reflectoring.io/spring-boot-conditionals/


文章转载自背井,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论