在开发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上找到 。