在开发Spring Boot应用程序时,如果满足某些条件,我们有时只想将bean或模块加载到应用程序上下文中。然后在测试期间禁用某些bean,或者在运行时环境中对某个属性做出反应。
Spring引入了@Conditional注释,允许我们定义自定义条件以应用于应用程序上下文的各个部分。Spring Boot构建于此之上,并提供一些预定义的条件,因此我们不必自己实现它们。
在本教程中,我们将看一些用例,解释为什么我们需要条件加载的bean。然后,我们将看到如何应用条件以及Spring Boot提供的条件。为了解决问题,我们还将实现自定义条件。
为什么我们需要有条件的bean
Spring应用程序上下文包含一个对象图,它构成了我们的应用程序在运行时需要的所有bean。Spring的@Conditional注释允许我们定义将某个bean包含在该对象图中的条件。
为什么我们需要在某些条件下包含或排除bean?
根据我的经验,最常见的用例是某些bean在测试环境中不起作用。它们可能需要连接到远程系统或测试期间不可用的应用程序服务器。因此,我们希望模块化我们的测试 以在测试期间排除或替换这些bean。
另一个用例是我们想要启用或禁用某个跨领域的问题。想象一下,我们已经构建了一个配置安全性的模块。在开发人员测试期间,我们不希望每次都输入我们的用户名和密码,因此我们使用一个开关并禁用整个安全模块进行本地测试。
此外,我们可能只想在某些外部资源可用时才加载某些bean ,否则它们将无法工作。例如,我们只想logback.xml在类路径中找到文件时配置我们的Logback记录器。
我们将在下面的讨论中看到更多用例。
定义有条件的Bean
在我们定义Spring bean的任何地方,我们都可以选择添加条件。只有满足此条件,才会将bean添加到应用程序上下文中。要声明条件,我们可以使用下面@Conditional...描述的任何注释。
但首先,让我们看一下如何将条件应用于某个Spring bean。
如果我们向单个@Bean定义添加条件,则仅在满足条件时才加载此bean:
代码语言:javascript复制@Configuration
class ConditionalBeanConfiguration {
@Bean
@Conditional... // <--
ConditionalBean conditionalBean(){
return new ConditionalBean();
};
}
如果我们向Spring添加一个条件@Configuration,那么只有在满足条件时才会加载此配置中包含的所有bean:
代码语言:javascript复制@Configuration
@Conditional... // <--
class ConditionalConfiguration {
@Bean
Bean bean(){
...
};
}
我们可以添加一个条件到@Component,@Service,@Repository,或@Controller:
代码语言:javascript复制@Component
@Conditional... // <--
class ConditionalComponent {
}
预先定义的条件
Spring Boot提供了一些@ConditionalOn...我们可以开箱即用的预定义注释。让我们依次看看每一个。
@ConditionalOnProperty
根据我的经验,@ConditionalOnProperty注释是Spring Boot项目中最常用的条件注释。它允许根据特定的环境属性有条件地加载bean:
代码语言:javascript复制@Configuration
@ConditionalOnProperty(
value="module.enabled",
havingValue = "true",
matchIfMissing = true)
class CrossCuttingConcernModule {
...
}
这个CrossCuttingConcernModule只载入module.enabled属性取值为true的Bean。如果没有设置该属性,它仍将被加载,因为我们已定义matchIfMissing 为true。这样,我们创建了一个默认加载的模块,直到我们另行决定。
同样地,我们可能会创建其他模块来解决我们可能希望在某个(测试)环境中禁用的安全性或调度等交叉问题。
@ConditionalOnExpression
如果我们有基于多个属性的更复杂的条件,我们可以使用@ConditionalOnExpression:
代码语言:javascript复制@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:
代码语言:javascript复制@Configuration
@ConditionalOnBean(OtherModule.class)
class DependantModule {
...
}
DependantModule 只有在上下文存在OtherModule 时才加载。
我们也可以定义bean名称而不是bean类。
这样,我们可以定义某些模块之间的依赖关系。仅当另一个模块的某个bean可用时才加载一个模块。
@ConditionalOnMissingBean
类似地,如果我们只想在某个其他bean 不在应用程序上下文中时加载bean ,我们就可以使用@ConditionalOnMissingBean:
代码语言:javascript复制@Configuration
class OnMissingBeanModule {
@Bean
@ConditionalOnMissingBean
DataSource dataSource() {
return new InMemoryDataSource();
}
}
在此示例中,如果还没有可用的数据源,我们只会将内存中的数据源注入应用程序上下文。这与Spring Boot在内部提供的测试上下文中的内存数据库非常相似。
@ConditionalOnResource
如果我们想根据类路径上某个资源可用的事实加载bean,我们可以使用@ConditionalOnResource:
代码语言:javascript复制@Configuration
@ConditionalOnResource(resources = "/logback.xml")
class LogbackModule {
...
}
如果在类路径中配置了logback文件就加载LogbackModule。这样,我们可能会创建类似的模块,只有在找到相应的配置文件时才会加载这些模块。
其他条件
上面描述的条件注释是我们可能在任何Spring Boot应用程序中使用的更常见的注释。Spring Boot提供了更多的条件注释。但是,它们并不常见,有些更适合框架开发而不是应用程序开发(Spring Boot大量使用它们)。所以,我们在这里只是简单地看一下它们。
@ConditionalOnClass:仅当类路径上有某个类时才加载bean:
代码语言:javascript复制@Configuration
@ConditionalOnClass(name = "this.clazz.does.not.Exist")
class OnClassModule {
...
}
@ConditionalOnMissingClass:仅当某个类不在类路径上时才加载bean :
代码语言:javascript复制@Configuration
@ConditionalOnMissingClass(value = "this.clazz.does.not.Exist")
class OnMissingClassModule {
...
}
@ConditionalOnJndi:仅当通过JNDI提供某个资源时才加载bean:
代码语言:javascript复制@Configuration
@ConditionalOnJndi("java:comp/env/foo")
class OnJndiModule {
...
}
@ConditionalOnJava:仅在运行特定版本的Java时加载bean:
代码语言:javascript复制@Configuration
@ConditionalOnJava(JavaVersion.EIGHT)
class OnJavaModule {
...
}
@ConditionalOnSingleCandidate:类似于@ConditionalOnBean,但只有在确定了给定bean类的单个候选项时才会加载bean。可能没有自动配置之外的用例:
代码语言:javascript复制@Configuration
@ConditionalOnSingleCandidate(DataSource.class)
class OnSingleCandidateModule {
...
}
@ConditionalOnWebApplication:仅当我们在Web应用程序中运行时才加载bean:
代码语言:javascript复制@Configuration
@ConditionalOnWebApplication
class OnWebApplicationModule {
...
}
@ConditionalOnNotWebApplication:仅当我们没有在Web应用程序中运行时才加载bean :
代码语言:javascript复制@Configuration
@ConditionalOnNotWebApplication
class OnNotWebApplicationModule {
...
}
@ConditionalOnCloudPlatform:仅当我们在某个云平台上运行时才加载bean:
代码语言:javascript复制@Configuration
@ConditionalOnCloudPlatform(CloudPlatform.CLOUD_FOUNDRY)
class OnCloudPlatformModule {
...
}
自定义条件
除了条件注释,我们可以创建自己的注释,并将多个条件与逻辑运算符组合在一起。
想象一下,我们有一些Spring bean本身可以与操作系统对话。只有在我们在相应的操作系统上运行应用程序时才应加载这些bean。
让我们实现一个条件,只有当我们在unix机器上运行代码时才加载bean。为此,我们实现了Spring的Condition 接口:
代码语言:javascript复制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注释结合使用了:
代码语言:javascript复制@Bean
@Conditional(OnUnixCondition.class)
UnixBean unixBean() {
return new UnixBean();
}
将条件与OR结合:
如果我们想要将多个条件与逻辑“OR”运算符组合成一个条件,我们可以扩展AnyNestedCondition:
代码语言:javascript复制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运算符将它们结合起来。
我们可以像任何其他条件一样使用这个条件:
代码语言:javascript复制@Bean
@Conditional(OnWindowsOrUnixCondition.class)
WindowsOrUnixBean windowsOrUnixBean() {
return new WindowsOrUnixBean();
}
注:你AnyNestedCondition还是AllNestedConditions不工作?
检查ConfigurationPhase传入的参数super()。如果要将组合条件应用于@Configurationbean,请使用该值 PARSECONFIGURATION。如果要将条件应用于简单bean,请使用REGISTERBEAN上面的示例中所示。Spring Boot需要进行区分,以便它可以在应用程序上下文启动期间的适当时间应用条件。
将条件与AND结合起来:
如果我们想要将条件与“AND”逻辑结合起来,我们可以简单地@Conditional...在单个bean上使用多个 注释。它们将自动与逻辑“AND”运算符组合,这样如果至少有一个条件失败,则不会加载bean:
代码语言:javascript复制@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:
代码语言:javascript复制@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(OnLinuxCondition.class)
public @interface ConditionalOnUnix {}
当我们用新的注释注释bean时,Spring将评估使用这个元注释:
代码语言:javascript复制@Bean
@ConditionalOnUnix
LinuxBean linuxBean(){
return new LinuxBean();
}
结论
通过@Conditional注释和创建自定义@Conditional... 注释的可能性,Spring已经为我们提供了很多控制应用程序上下文内容的能力。
春天引导建立在最重要的是通过将一些方便的@ConditionalOn...注解表,并通过允许我们使用条件相结合AllNestedConditions,AnyNestedCondition或NoneNestedCondition。这些工具允许我们模块化我们的生产代码以及我们的测试。
然而,权力是责任,所以我们应该注意不要在条件下乱丢我们的应用程序上下文,以免我们忘记何时加载。
代码链接
https://github.com/thombergs/code-examples/tree/master/spring-boot/conditionals