在构建spring boot项目时,有时我们希望只有在满足某些条件时,一些bean或模块才会被加载进应用上下文(application context)中。目的也许是为了在测试时禁用某些bean, 也许是对运行环境中的某些属性做出反应。
Spring 为此引入了@Conditional
注解,方便我们为应用上下文的一部分提供自定义条件。Spring Boot 以此为基础,提供了一些预定义好的条件,避免我们重造轮子。
本文将给出一些使用场景,说明为什么要使用条件化加载的bean。我们将看到如何应用这些条件以及Spring Boot提供了哪些条件。为使本文更完整,我们也将自己动手实现一个自定义的条件。
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。
后续讨论中将看到更多例子。
任何声明Spring bean的地方,我们都可以加一个条件(condition)。除非条件满足,否则bean不会被加载进应用上下文中。要声明一个条件,我们可以使用下面描述的任何一个@Conditional...
注解。
我们先看看如何对一个特定的bean添加条件。
如果我们对单个 @Bean
添加条件,这个bean只会在条件满足时被加载:
@Configuration
class ConditionalBeanConfiguration {
@Bean
@Conditional... // <--
ConditionalBean conditionalBean(){
return new ConditionalBean();
};
}
如果我们对@Configuration添加注解,该配置中的所有 bean
,只会在条件满足时被加载:
@Configuration
@Conditional... // <--
class ConditionalConfiguration {
@Bean
Bean bean(){
...
};
}
最后,条件可以加给@Component
, @Service, @Repository 或 @Controller 这类构造型注解:
@Component
@Conditional... // <--
class ConditionalComponent {
}
Spring Boot提供了一些预先定义好的 @ConditionalOn...
注解,开箱即用
。让我们依次看看它们。
@ConditionalOnProperty 注解,在我的经验中,是Spring Boot项目中最常用的条件注解
。它允许根据特定的环境参数条件化地加载bean:
@Configuration
@ConditionalOnProperty(
value="module.enabled",
havingValue = "true",
matchIfMissing = true)
class CrossCuttingConcernModule {
...
}
CrossCuttingConcernModule 只有在 module.enabled
属性的值为true
时才会被加载。如果该属性不存在也会被加载,因为我们定义了matchIfMissing 为 true。以这种方式,我们创建了一个默认会被加载的模块,除非我们有别的决定。
同样,我们也可以为诸如安全或定时任务这样的横切关注点创建可在特定(测试)环境中禁用的模块。
如果我们需要基于多个参数的复杂条件,我们可以使用@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)的全部功能。
按此方式,我们可以创建这样一种模块: 当父模块被禁用时,它也被禁用;也可以父模块启用时,它被禁用。
有时,我们希望只有在某些bean存在时,才加载另一些bean:
@Configuration
@ConditionalOnBean(OtherModule.class)
class DependantModule {
...
}
只有在应用上下文中有OtherModule这个类的bean存在时,DependantModule才会被加载。除了用bean class外,我们也可以使用bean的名称(bean name)。
这样,我们可以定义特定模块间的依赖。只有在一模块的某个特定bean存在时,另一个模块才允许被加载。
类似地,我们使用@ConditionalOnMissingBean来表达只有在某个bean不存在时,才加载另一个bean:
@Configuration
class OnMissingBeanModule {
@Bean
@ConditionalOnMissingBean
DataSource dataSource() {
return new InMemoryDataSource();
}
}
在本例中,只有应用上下文中没有datasource时,我们才会注入 in-memory datasource(内存数据库)。这和Spring Boot在测试环境中会提供内存数据库是相似的。
如果我们希望一个bean只有在类路径中存在某个资源时才加载,我们可以使用 @ConditionalOnResource:
@Configuration
@ConditionalOnResource(resources = "/logback.xml")
class LogbackModule {
...
}
只有当类路径中发现logback的配置文件时,才会加载LogbackModule。这样,我们可以创建类似的模块,只有相应的配置文件存在时,这类模块才会被加载。
上面描述的条件注解是任何Spring Boot应用中都较常用的。Spring Boot还提供了更多的条件注解。我们很少使用它们,它们更适合于框架开发(framework development)而非应用开发(application development)。 实际上Spring Boot本身就依赖于这些注解。我们简单的看一下。
只有在类路径中有某个class时,才加载给定的bean:
@Configuration
@ConditionalOnClass(name = "this.clazz.does.not.Exist")
class OnClassModule {
...
}
只有在类路径中没有某个class时,才加载给的bean:
@Configuration
@ConditionalOnMissingClass(value = "this.clazz.does.not.Exist")
class OnMissingClassModule {
...
}
只有在某个JNDI资源存在时,才加载bean:
@Configuration
@ConditionalOnJndi("java:comp/env/foo")
class OnJndiModule {
...
}
只有在给版本的java下,才加载bean:
@Configuration
@ConditionalOnJava(JavaVersion.EIGHT)
class OnJavaModule {
...
}
有点像 @ConditionalOnBean, 但是只有在给定class的代表只有一个时(即该类,包括子类,只有一个实例时),bean才会被加载。通常用在auto-configurations中。
@Configuration
@ConditionalOnSingleCandidate(DataSource.class)
class OnSingleCandidateModule {
...
}
当前项目是web应用时,才加载该bean:
@Configuration
@ConditionalOnWebApplication
class OnWebApplicationModule {
...
}
当前项目不是web应用时,加载该bean:
@Configuration
@ConditionalOnNotWebApplication
class OnNotWebApplicationModule {
...
}
在特定的云平台下才加载该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"将多个条件合并为一个条件,我们可以继承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"合并,我们只需在一个bean上简单地用上多个@Conditional…注解。它们会被自动地用"AND"合并,这样,只要其中一个条件不满足,bean就不会被加载:
@Bean
@ConditionalOnUnix
@Conditional(OnWindowsCondition.class)
WindowsAndUnixBean windowsAndUnixBean() {
return new WindowsAndUnixBean();
}
这个bean怕是永远不会被加载了,除非有人创造出了我还未听说过的Windows/Unix杂合体。
注意,@Conditional注解在方法或类上最多只能用一次。如果想合并多个注解,必须使用(自定义的)@ConditionalOn…注解,它们无此限制。下面将探索怎么创建@ConditionalOnUnix注解。
另外,如果想用"AND"合并多个条件,我们可以继承AllNestedConditions类,它和上面说过的AnyNestedConditions的实现方式一样。
与 AnyNestedCondition 以及 AllNestedConditions类似,我们可以继承 NoneNestedCondition,这样,只有当所有条件都不满足时,bean才会被加载。
我们可以为任何条件创建一个自定义注解。只需要在自己的注解上注解上@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上
本文译自: Conditional Beans with Spring Boot
转载自:公众号logically