转载

SpringBoot的@Conditional使用 - reflectoring

在开发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
<b>class</b> ConditionalBeanConfiguration {

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

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

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

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

@Component
@Conditional... <font><i>// <--</i></font><font>
<b>class</b> ConditionalComponent {
}
</font>

预先定义的条件

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

@ConditionalOnProperty

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

@Configuration
@ConditionalOnProperty(
    value=<font>"module.enabled"</font><font>, 
    havingValue = </font><font>"true"</font><font>, 
    matchIfMissing = <b>true</b>)
<b>class</b> CrossCuttingConcernModule {
  ...
}
</font>

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

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

@ConditionalOnExpression

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

@Configuration
@ConditionalOnExpression(
    <font>"${module.enabled:true} and ${module.submodule.enabled:true}"</font><font>
)
<b>class</b> SubModule {
  ...
}
</font>

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

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

@ConditionalOnBean

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

@Configuration
@ConditionalOnBean(OtherModule.<b>class</b>)
<b>class</b> DependantModule {
  ...
}

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

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

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

@ConditionalOnMissingBean

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

@Configuration
<b>class</b> OnMissingBeanModule {

  @Bean
  @ConditionalOnMissingBean
  DataSource dataSource() {
    <b>return</b> <b>new</b> InMemoryDataSource();
  }
}

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

@ConditionalOnResource

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

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

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

其他条件

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

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

@Configuration
@ConditionalOnClass(name = <font>"this.clazz.does.not.Exist"</font><font>)
<b>class</b> OnClassModule {
  ...
}
</font>

@ConditionalOnMissingClass:仅当某个类不在类路径上时才加载bean :

@Configuration
@ConditionalOnMissingClass(value = <font>"this.clazz.does.not.Exist"</font><font>)
<b>class</b> OnMissingClassModule {
  ...
}
</font>

@ConditionalOnJndi:仅当通过JNDI提供某个资源时才加载bean:

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

@ConditionalOnJava:仅在运行特定版本的Java时加载bean:

@Configuration
@ConditionalOnJava(JavaVersion.EIGHT)
<b>class</b> OnJavaModule {
  ...
}

@ConditionalOnSingleCandidate:类似于@ConditionalOnBean,但只有在确定了给定bean类的单个候选项时才会加载bean。可能没有自动配置之外的用例:

@Configuration
@ConditionalOnSingleCandidate(DataSource.<b>class</b>)
<b>class</b> OnSingleCandidateModule {
  ...
}

@ConditionalOnWebApplication:仅当我们在Web应用程序中运行时才加载bean:

@Configuration
@ConditionalOnWebApplication
<b>class</b> OnWebApplicationModule {
  ...
}

@ConditionalOnNotWebApplication:仅当我们没有在Web应用程序中运行时才加载bean :

@Configuration
@ConditionalOnNotWebApplication
<b>class</b> OnNotWebApplicationModule {
  ...
}

@ConditionalOnCloudPlatform:仅当我们在某个云平台上运行时才加载bean:

@Configuration
@ConditionalOnCloudPlatform(CloudPlatform.CLOUD_FOUNDRY)
<b>class</b> OnCloudPlatformModule {
  ...
}

自定义条件

除了条件注释,我们可以创建自己的注释,并将多个条件与逻辑运算符组合在一起。

想象一下,我们有一些Spring bean本身可以与操作系统对话。只有在我们在相应的操作系统上运行应用程序时才应加载这些bean。

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

<b>class</b> OnUnixCondition implements Condition {

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

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

现在可以将条件与Spring的@Conditional注释结合使用了:

@Bean
@Conditional(OnUnixCondition.<b>class</b>)
UnixBean unixBean() {
  <b>return</b> <b>new</b> UnixBean();
}

将条件与OR结合:

如果我们想要将多个条件与逻辑“OR”运算符组合成一个条件,我们可以扩展AnyNestedCondition:

<b>class</b> OnWindowsOrUnixCondition <b>extends</b> AnyNestedCondition {

  OnWindowsOrUnixCondition() {
    <b>super</b>(ConfigurationPhase.REGISTER_BEAN);
  }

  @Conditional(OnWindowsCondition.<b>class</b>)
  <b>static</b> <b>class</b> OnWindows {}

  @Conditional(OnUnixCondition.<b>class</b>)
  <b>static</b> <b>class</b> OnUnix {}

}

在这里,我们创建了一个条件,如果应用程序在Windows或unix上运行,则满足该条件。

在AnyNestedCondition父类将评估@Conditional的方法说明和使用OR运算符将它们结合起来。

我们可以像任何其他条件一样使用这个条件:

@Bean
@Conditional(OnWindowsOrUnixCondition.<b>class</b>)
WindowsOrUnixBean windowsOrUnixBean() {
  <b>return</b> <b>new</b> WindowsOrUnixBean();
}

注:你AnyNestedCondition还是AllNestedConditions不工作?

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

将条件与AND结合起来:

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

@Bean
@ConditionalOnUnix
@Conditional(OnWindowsCondition.<b>class</b>)
WindowsAndUnixBean windowsAndUnixBean() {
  <b>return</b> <b>new</b> 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.<b>class</b>)
<b>public</b> @<b>interface</b> ConditionalOnUnix {}

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

@Bean
@ConditionalOnUnix
LinuxBean linuxBean(){
  <b>return</b> <b>new</b> LinuxBean();
}

结论

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

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

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

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

原文  https://www.jdon.com/51918
正文到此结束
Loading...