转载文章:在Spring Boot中使用条件化的Bean

在Spring Boot 中使用 条件化的Bean

    • 1. 为什么我们需要 条件化的bean ?
    • 2. 声明条件化的bean
        • 2.1 条件化的 @Bean
        • 2.2 条件化的 @Configuration
        • 2.3 条件化的 @Component
    • 3. 预定义的条件(Pre-Defined Conditions)
        • 3.1 @ConditionalOnProperty
        • 3.2 @ConditionalOnExpression
        • 3.3 @ConditionalOnBean
        • 3.4 @ConditionalOnMissingBean
        • 3.5 @ConditionalOnResource
    • 4. 其它条件
        • 4.1 @ConditionalOnClass
        • 4.2 @ConditionalOnMissingClass
        • 4.3 @ConditionalOnJndi
        • 4.4 @ConditionalOnJava
        • 4.5 @ConditionalOnSingleCandidate
        • 4.6 @ConditionalOnWebApplication
        • 4.7 @ConditionalOnNotWebApplication
        • 4.8 @ConditionalOnCloudPlatform
    • 5. 自定义条件注解
        • 5.1 定义自己的条件
        • 5.2 使用 逻辑或(OR) 合并多个条件
        • 5.3 使用逻辑与(AND)合并条件
        • 5.4 使用逻辑非(NOT)合并条件
        • 5.5 自定义一个 @ConditionalOn… 注解
    • 6. 总结

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

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

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

1. 为什么我们需要 条件化的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。

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

2. 声明条件化的bean

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

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

2.1 条件化的 @Bean

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

@Configuration
class ConditionalBeanConfiguration {

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

2.2 条件化的 @Configuration

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

@Configuration
@Conditional... // <--
class ConditionalConfiguration {
  
  @Bean
  Bean bean(){
    ...
  };
  
}

2.3 条件化的 @Component

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

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

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

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

3.1 @ConditionalOnProperty

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

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

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

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

3.2 @ConditionalOnExpression

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

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

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

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

3.3 @ConditionalOnBean

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

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

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

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

3.4 @ConditionalOnMissingBean

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

@Configuration
class OnMissingBeanModule {

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

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

3.5 @ConditionalOnResource

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

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

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

4. 其它条件

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

4.1 @ConditionalOnClass

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

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

4.2 @ConditionalOnMissingClass

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

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

4.3 @ConditionalOnJndi

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

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

4.4 @ConditionalOnJava

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

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

4.5 @ConditionalOnSingleCandidate

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

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

4.6 @ConditionalOnWebApplication

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

@Configuration
@ConditionalOnWebApplication
class OnWebApplicationModule {
  ...
}

4.7 @ConditionalOnNotWebApplication

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

@Configuration
@ConditionalOnNotWebApplication
class OnNotWebApplicationModule {
  ...
}

4.8 @ConditionalOnCloudPlatform

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

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

5. 自定义条件注解

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

5.1 定义自己的条件

想象我们有一些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();
}

5.2 使用 逻辑或(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需要这个区分,以便在应用启动时在合适的时间判断条件。

5.3 使用逻辑与(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的实现方式一样。

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

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

5.5 自定义一个 @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();
}

6. 总结

有了@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

你可能感兴趣的:(SpringBoot,spring)