当前位置: 首页 > news >正文

转载文章:在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

相关文章:

  • 《赢在用户》8月19日北京书友会报道!
  • kryo浅析
  • 六招彻底防范ARP病毒反复发作
  • 转载:面试前必须要知道的Redis面试题
  • 如何检测网内IP地址是否被占用
  • 转载:Java并发编程-原子类实现
  • Java 中的四种引用类型
  • 美国国家安全局在监视全球网民?
  • 评论:微软的SOA战略
  • SpringBoot 以字符串格式传入时间
  • 基于JAVA的开源工作流引擎(Open Source Workflow Engines Written in Java)
  • SpringBoot WebFlux 官网文档阅读笔记
  • 拿到好牌就出昏招——郝玺龙妙语录
  • 关于 SkipList (跳表)的一些资料梳理
  • 重构的修炼——从重构命令行操作的实践来谈论
  • 【node学习】协程
  • 【跃迁之路】【699天】程序员高效学习方法论探索系列(实验阶段456-2019.1.19)...
  • 【跃迁之路】【735天】程序员高效学习方法论探索系列(实验阶段492-2019.2.25)...
  • Android 控件背景颜色处理
  • Docker容器管理
  • ECMAScript6(0):ES6简明参考手册
  • export和import的用法总结
  • github指令
  • javascript从右向左截取指定位数字符的3种方法
  • Map集合、散列表、红黑树介绍
  • React16时代,该用什么姿势写 React ?
  • React-Native - 收藏集 - 掘金
  • 阿里研究院入选中国企业智库系统影响力榜
  • 动态规划入门(以爬楼梯为例)
  • 分享一份非常强势的Android面试题
  • 开发了一款写作软件(OSX,Windows),附带Electron开发指南
  • 马上搞懂 GeoJSON
  • 强力优化Rancher k8s中国区的使用体验
  • 携程小程序初体验
  • 学习HTTP相关知识笔记
  • $分析了六十多年间100万字的政府工作报告,我看到了这样的变迁
  • (Redis使用系列) Springboot 使用redis实现接口幂等性拦截 十一
  • (附源码)ssm经济信息门户网站 毕业设计 141634
  • (免费领源码)python#django#mysql校园校园宿舍管理系统84831-计算机毕业设计项目选题推荐
  • (源码版)2024美国大学生数学建模E题财产保险的可持续模型详解思路+具体代码季节性时序预测SARIMA天气预测建模
  • *++p:p先自+,然后*p,最终为3 ++*p:先*p,即arr[0]=1,然后再++,最终为2 *p++:值为arr[0],即1,该语句执行完毕后,p指向arr[1]
  • .jks文件(JAVA KeyStore)
  • .Net MVC + EF搭建学生管理系统
  • .net 程序 换成 java,NET程序员如何转行为J2EE之java基础上(9)
  • .net(C#)中String.Format如何使用
  • .net程序集学习心得
  • .NET和.COM和.CN域名区别
  • .net解析传过来的xml_DOM4J解析XML文件
  • .NET平台开源项目速览(15)文档数据库RavenDB-介绍与初体验
  • @ 代码随想录算法训练营第8周(C语言)|Day57(动态规划)
  • [ 隧道技术 ] cpolar 工具详解之将内网端口映射到公网
  • []AT 指令 收发短信和GPRS上网 SIM508/548
  • []使用 Tortoise SVN 创建 Externals 外部引用目录
  • [20171113]修改表结构删除列相关问题4.txt
  • [23] GaussianAvatars: Photorealistic Head Avatars with Rigged 3D Gaussians