SpringBoot的@Conditional使用 - reflectoring

19-03-18 banq
    

在开发Spring Boot应用程序时,如果满足某些条件,我们有时只想将bean或模块加载到应用程序上下文中。然后在测试期间禁用某些bean,或者在运行时环境中对某个属性做出反应。

Spring引入了@Conditional注释,允许我们定义自定义条件以应用于应用程序上下文的各个部分。Spring Boot构建于此之上,并提供一些预定义的条件,因此我们不必自己实现它们。

在本教程中,我们将看一些用例,解释为什么我们需要条件加载的bean。然后,我们将看到如何应用条件以及Spring Boot提供的条件。为了解决问题,我们还将实现自定义条件。

为什么我们需要有条件的豆?

Spring应用程序上下文包含一个对象图,它构成了我们的应用程序在运行时需要的所有bean。Spring的@Conditional注释允许我们定义将某个bean包含在该对象图中的条件。

为什么我们需要在某些条件下包含或排除bean?

根据我的经验,最常见的用例是某些bean在测试环境中不起作用。它们可能需要连接到远程系统或测试期间不可用的应用程序服务器。因此,我们希望模块化我们的测试 以在测试期间排除或替换这些bean。

另一个用例是我们想要启用或禁用某个跨领域的问题。想象一下,我们已经构建了一个配置安全性的模块。在开发人员测试期间,我们不希望每次都输入我们的用户名和密码,因此我们使用一个开关并禁用整个安全模块进行本地测试。

此外,我们可能只想在某些外部资源可用时才加载某些bean ,否则它们将无法工作。例如,我们只想logback.xml在类路径中找到文件时配置我们的Logback记录器。

我们将在下面的讨论中看到更多用例。

定义有条件的Bean

在我们定义Spring bean的任何地方,我们都可以选择添加条件。只有满足此条件,才会将bean添加到应用程序上下文中。要声明条件,我们可以使用下面@Conditional...描述的任何注释。

但首先,让我们看一下如何将条件应用于某个Spring bean。

如果我们向单个@Bean定义添加条件,则仅在满足条件时才加载此bean:

@Configuration
class ConditionalBeanConfiguration {

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

如果我们向Spring添加一个条件@Configuration,那么只有在满足条件时才会加载此配置中包含的所有bean:

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

我们可以添加一个条件到@Component,@Service,@Repository,或@Controller:

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

预先定义的条件

Spring Boot提供了一些@ConditionalOn...我们可以开箱即用的预定义注释。让我们依次看看每一个。

@ConditionalOnProperty

根据我的经验,@ConditionalOnProperty注释是Spring Boot项目中最常用的条件注释。它允许根据特定的环境属性有条件地加载bean:

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

这个CrossCuttingConcernModule只载入module.enabled属性取值为true的Bean。如果没有设置该属性,它仍将被加载,因为我们已定义matchIfMissing 为true。这样,我们创建了一个默认加载的模块,直到我们另行决定。

同样地,我们可能会创建其他模块来解决我们可能希望在某个(测试)环境中禁用的安全性或调度等交叉问题。

@ConditionalOnExpression

如果我们有基于多个属性的更复杂的条件,我们可以使用@ConditionalOnExpression:

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

如果module.enabled和module.submodule.enabled 都具价值true,则加载。通过附加:true到属性,我们告诉Spring true 在未设置属性的情况下将其用作默认值。我们可以使用Spring Expression Language的完整扩展。

这样,我们可以创建子模块,如果父模块被禁用,则应该禁用这些子模块,但如果启用了父模块,也可以禁用子模块。

@ConditionalOnBean

有时,我们可能只想在应用程序上下文中某个其他bean可用时才加载bean:

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

DependantModule 只有在上下文存在OtherModule 时才加载。

我们也可以定义bean名称而不是bean类。

这样,我们可以定义某些模块之间的依赖关系。仅当另一个模块的某个bean可用时才加载一个模块。

@ConditionalOnMissingBean

类似地,如果我们只想在某个其他bean 不在应用程序上下文中时加载bean ,我们就可以使用@ConditionalOnMissingBean:

@Configuration
class OnMissingBeanModule {

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

在此示例中,如果还没有可用的数据源,我们只会将内存中的数据源注入应用程序上下文。这与Spring Boot在内部提供的测试上下文中的内存数据库非常相似。

@ConditionalOnResource

如果我们想根据类路径上某个资源可用的事实加载bean,我们可以使用@ConditionalOnResource:

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

如果在类路径中配置了logback文件就加载LogbackModule。这样,我们可能会创建类似的模块,只有在找到相应的配置文件时才会加载这些模块。

其他条件

上面描述的条件注释是我们可能在任何Spring Boot应用程序中使用的更常见的注释。Spring Boot提供了更多的条件注释。但是,它们并不常见,有些更适合框架开发而不是应用程序开发(Spring Boot大量使用它们)。所以,我们在这里只是简单地看一下它们。

@ConditionalOnClass:仅当类路径上有某个类时才加载bean:

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

@ConditionalOnMissingClass:仅当某个类不在类路径上时才加载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,但只有在确定了给定bean类的单个候选项时才会加载bean。可能没有自动配置之外的用例:

@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 bean本身可以与操作系统对话。只有在我们在相应的操作系统上运行应用程序时才应加载这些bean。

让我们实现一个条件,只有当我们在unix机器上运行代码时才加载bean。为此,我们实现了Spring的Condition 接口:

class OnUnixCondition implements Condition {

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

我们只是使用Apache Commons的SystemUtils类来确定我们是否在类似unix的系统上运行。如果需要,我们可以包含更复杂的逻辑,它使用有关当前应用程序上下文(ConditionContext)或有关注释类(AnnotatedTypeMetadata)的信息。

现在可以将条件与Spring的@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不工作?

检查ConfigurationPhase传入的参数super()。如果要将组合条件应用于@Configurationbean,请使用该值 PARSE_CONFIGURATION。如果要将条件应用于简单bean,请使用REGISTER_BEAN上面的示例中所示。Spring Boot需要进行区分,以便它可以在应用程序上下文启动期间的适当时间应用条件。

将条件与AND结合起来:

如果我们想要将条件与“AND”逻辑结合起来,我们可以简单地@Conditional...在单个bean上使用多个 注释。它们将自动与逻辑“AND”运算符组合,这样如果至少有一个条件失败,则不会加载bean:

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

这个bean永远不应该加载,除非有人创建了我不知道的Windows / Unix混合。

请注意,@Conditional注释不能在单个方法或类上多次使用。因此,如果我们想以这种方式组合多个注释,我们必须使用@ConditionalOn...没有此限制的自定义注释。下面,我们将探讨如何创建@ConditionalOnUnix注释。

或者,如果我们想将条件与AND组合成一个 @Conditional注释,我们可以扩展Spring Boot的AllNestedConditions 类,其工作方式与AnyNestedConditions上述完全相同。

结合条件与NOT:

与AnyNestedCondition和类似AllNestedConditions,NoneNestedCondition如果组合条件中的NONE匹配,我们可以扩展到仅加载bean。

定义定制的@ ConditionalOn ...注释

我们可以为任何条件创建自定义注释。我们只需要使用以下方法对此注释进行元注释@Conditional:

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

当我们用新的注释注释bean时,Spring将评估使用这个元注释:

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

结论

通过@Conditional注释和创建自定义@Conditional... 注释的可能性,Spring已经为我们提供了很多控制应用程序上下文内容的能力。

春天引导建立在最重要的是通过将一些方便的@ConditionalOn...注解表,并通过允许我们使用条件相结合AllNestedConditions,AnyNestedCondition或NoneNestedCondition。这些工具允许我们模块化我们的生产代码以及我们的测试

然而,权力是责任,所以我们应该注意不要在条件下乱丢我们的应用程序上下文,以免我们忘记何时加载。

本文的代码可以在github上找到