• 【翻译】Spring Boot 条件注解


    本文翻译自 https://codingnconcepts.com/spring-boot/conditional-annotations-in-spring-boot

    翻译过程中会对文中英语语序和不甚明确的地方进行微调,如有谬误欢迎评论指正。

    在这篇教程中,我们将通过一些示例带你了解 Spring Boot 条件注解

    概述

    Spring Boot 是自以为是的,当 Spring Boot 在classpath中找到相关依赖项时,它会为模块提供默认(自动)配置。

    举例,Spring Boot 提供了:

    1. 如 classpath 中未提供服务器组件依赖,则默认启用内嵌Tomcat作为服务端依赖。你也可以把它改成 Jetty 或 Undertow,仅需要添加相应的依赖到 classpath 中(译者注:常见的方式就是添加Maven或Gradle依赖)
    2. 当它在classpath找到 spring-cloud-starter-openfeign 依赖会提供默认的 HttpClient 配置,你可以通过在classpath中添加 OkHttpClient 或 ApacheHttpClient 依赖来改变HttpClient
    3. 当classpath中包含 spring-boot-starter-web 的依赖,则使用 Jackson 框架作为默认的 JSON 序列化传输请求和响应
    4. 当classpath中包含 spring-boot-starter-data-jpa 则会创建默认的数据源配置

    Spring Boot 是如何做到这一切的呢?

    Spring Boot 使用 @Conditional 注解实现了这一点。Spring Boot 大量使用 @Conditional 注解基于条件方式来加载默认配置、Bean对象的。

    这些条件可以是:

    • classpath中可用的依赖、配置文件内容 或 类
    • 定义在 application.yml 或 application.properties 文件、系统配置、环境变量中的配置
    • Java的版本、操作系统、云平台、web应用 等。

    它对我们有什么用处呢?

    类似于Spring如何神奇地读取默认的配置,有些时候我们想基于自定义的条件来读取 Bean对象、模块 到Spring应用上下文中。

    我们可以通过 @Conditional 注解+自定义的条件实现这个目标。

    @Conditional

    @Conditional 注解是用来匹配只有满足所有指定条件才能将Bean注册到Spring上下文中。

    @Conditional 注解有以下三种使用方式:

    1、作为方法级的注解

    我们可以在 @Bean 注解下添加 @Conditional 注解来达到满足条件注册Bean。

    @Configuration
    class ConditionalBeanConfiguration {
    
      @Bean
      @Conditional(CustomCondition.class) //<- method level condition
      ConditionalBean conditionalBean(){
        return new ConditionalBean();
      };
    }
    

    2、作为类级别注解

    我们可以在有 @Component, @Service, @Repository, @Controller, @RestController, 或 @Configuration 注解的类上标注 @Conditional,仅当满足条件才能将这个类型的 bean 注册到 Spring 上下文中。

    @Component
    @Conditional(CustomCondition.class) //<- class level condition
    class ConditionalComponent {
    }
    

    带@Conditional注解的@Configuration类

    如果一个 @Configuration 注解标识的类头包含 @Conditional 则这个类所有的 @Bean 方法、@Import 注解 和 @ComponentScan 注解都会被类头上的 @Conditional 影响,仅当条件满足时才会被加载。

    @Configuration
    @Conditional(CustomCondition.class) //<- class level condition
    class ConditionalConfiguration {
      
      @Bean //<- will be loaded only if class level condition is met
      Bean bean(){
        // Code for bean definition
      };
      
    }
    

    3、作为源注解

    我们可以创建一个自定义的注解,在注解上添加 @Conditional 作为新的条件注解

    @Target({ElementType.TYPE, ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    @Conditional(CustomCondition.class) //<- meta annotation condition
    public @interface CustomConditionAnnotation {
    }
    

    现在我们可在方法和类上使用自定义的 @CustomConditionAnnotation 注解代替@Conditional 就像这样:

    @Configuration
    class ConditionalBeanConfiguration {
    
      @Bean
      @CustomConditionAnnotation  //<- custom annotation at method level
      ConditionalBean conditionalBean(){
        return new ConditionalBean();
      };
    }
    
    @Component
    @CustomConditionAnnotation //<- custom annotation at class level
    class ConditionalComponent {
    }
    

    自定义条件

    在之前的例子中,我们使用 CustomCondition.class 中的 @Conditional 注解实现了条件匹配的逻辑。

    让我们通过实现 Spring 的 Condition 接口的 matches 方法创建自定义条件吧:

    public class CustomCondition implements Condition {
    
        @Override
        public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
            return context.getEnvironment().getProperty("custom.condition.enabled", Boolean.class, false);
        }
    }
    
    application.yml
    
    custom.condition.enabled: true
    

    我们可以看到当 custom.condition.enabled 设为 true 时,CustomCondition 可匹配到。

    组合条件-任一条件成立匹配

    我们可组合多个条件,实现任一条件符合,换句话讲:类似于实现 OR 的逻辑操作。

    再创建一个注解来组合

    我们已经创建了一个自定义注解CustomCondition,让我们快速创建一个AnotherCustomCondition注解:

    public class AnotherCustomCondition implements Condition {
    
        @Override
        public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
            return context.getEnvironment().getProperty("another-custom.condition.enabled", Boolean.class, false);
        }
    }
    
    application.yml
    
    another-custom.condition.enabled: true
    

    我们看到 another-custom.condition.enabled 条件为 true 时, AnotherCustomCondition 能匹配到。

    AnyNestedCondition(任意嵌套条件)

    现在让我们通过扩展 Spring 的 AnyNestedCondition 类来组合上边两个注解,做到任一匹配:

    public class CombinedConditionsWithAnyMatch extends AnyNestedCondition {
    
        public CombinedConditionsWithAnyMatch() {
            super(ConfigurationPhase.PARSE_CONFIGURATION);
            // super(ConfigurationPhase.REGISTER_BEAN);
        }
    
        @Conditional(CustomCondition.class)
        static class OnCustomCondition {}
    
        @Conditional(AnotherCustomCondition.class)
        static class OnAnotherCustomCondition {}
    }
    

    ConfigurationPhase(配置阶段)

    注意 ConfigurationPhase 参数被传递到了构建方法:

    • 如果你想组合的条件到一个 @Configuration 类,使用 ConfigurationPhase.PARSE_CONFIGURATION

    • 如果你想把条件应用到 @Bean 对象上,使用 ConfigurationPhase.REGISTER_BEAN

    Spring Boot 需要区分这以上两种作用域以便在合适的时间应用这些条件。

    使用任一条件匹配

    让我们来应用组合条件到一个配置类吧:

    @Configuration
    @Conditional(CombinedConditionsWithAnyMatch.class)
    class ConditionalConfiguration {
      
      @Bean
      Bean bean(){
        // TODO
      };
      
    }
    

    当任一条件满足时,这个配置类对象将被加载到Spring应用上下文中

    application.yml
    
    custom.condition.enabled: true
    another-custom.condition.enabled: false
    

    组合条件-所有条件成立匹配

    我们可以组合多个条件来达到仅当所有条件都满足时才匹配的目的,类似于 AND 的逻辑操作。

    AllNestedConditions

    这次我们组合 CustomConditionAnotherCustomCondition 这两个注解,通过扩展Spring的 AllNestedConditions 创建 匹配所有 (完全匹配)的条件类

    public class CombinedConditionsWithAllMatch extends AllNestedConditions {
    
        public CombinedConditionsWithAllMatch() {
            // super(ConfigurationPhase.PARSE_CONFIGURATION);
            super(ConfigurationPhase.REGISTER_BEAN);
        }
    
        @Conditional(CustomCondition.class)
        static class OnCustomCondition {}
    
        @Conditional(AnotherCustomCondition.class)
        static class OnAnotherCustomCondition {}
    }
    

    使用完全匹配条件

    这次我们将 ConfigurationPhase.REGISTER_BEAN 传入构造方法,我们就能使用这个组合条件放到 @Bean 对象,就像下边这样:

    @Configuration
    class ConditionalBeanConfiguration {
    
      @Bean
      @Conditional(CombinedConditionsWithAllMatch.class) //<- as method level annotation
      ConditionalBean conditionalBean(){
        return new ConditionalBean();
      };
    }
    

    这个Bean仅当所有条件都满足时才会加载到Spring应用上下文中:

    application.yml
    
    custom.condition.enabled: true
    another-custom.condition.enabled: true
    

    组合条件-所有条件不成立匹配

    我们可以通过扩展 Spring 的 NoneNestedCondition 类来组合出所有条件都成立的条件:

    public class CombinedConditionsWithNoneMatch extends NoneNestedConditions {
    
        public CombinedConditionsWithNoneMatch() {
            super(ConfigurationPhase.PARSE_CONFIGURATION);
            // or super(ConfigurationPhase.REGISTER_BEAN);
        }
    
        @Conditional(CustomCondition.class)
        static class OnCustomCondition {}
    
        @Conditional(AnotherCustomCondition.class)
        static class OnAnotherCustomCondition {}
    }
    

    以上组合条件将会成功匹配如下配置:

    application.yml
    
    custom.condition.enabled: false
    another-custom.condition.enabled: false
    

    预定义的条件注解

    Spring Boot 提供了一系列预定义的 @ConditionalOn... 便利注解供我们使用。让我们看看有哪些:

    @ConditionalOnProperty

    这个是Spring Boot中最常用的注解,它允许通过指定的配置来条件式加载类或Bean:

    @Configuration
    @ConditionalOnProperty(
        value="api.doc.enabled", 
        havingValue = "true", 
        matchIfMissing = true)
    class ApiDocConfig {
      // TODO
    }
    

    ApiDocConfig 仅当 api.doc.enabled: true 时才会加载,如果没有配置这个参数,它也会被加载,因为定义了 matchIfMissing = true,通过这种方式,我们可以在没配置指定参数时创建默认配置,设置为false时才禁用。

    一个常见的例子是当我们想去在开发环境开启一个指定配置,而生产环境禁用,反之亦然:

    application-dev.yml
    
    api.doc.enabled: true
    
    application-prod.yml
    
    api.doc.enabled: false
    

    (译者注:通过不同配置文件区分环境,由条件注解判断是否加载类或对象到Spring上下文)

    另一个常见的例子是定义了一个公共组件,通过不同项目的配置文件可以在开启或禁用这个功能,配置示例如下:

    Project A -> application.yml
    api.doc.enabled: true
    
    Project B -> application.yml
    api.doc.enabled: false
    

    @ConditionalOnExpression

    如果我们需要多个配置项组合更复杂的条件,我们可以使用 @ConditionalOnExpression (译者注:表达式条件注解):

    @Configuration
    @ConditionalOnExpression(
      "${api.doc.enabled:true} and '${spring.profile.active}'.equalsIgnoreCase('DEV')"
    )
    class ApiDocConfig {
      // TODO
    }
    

    这个 ApiDocConfig 仅当 api.doc.enabled: true 并且 spring.profile.active: dev 时才会启用。我们告诉 Spring Boot 使用 true作为默认值适用于参数未设置的场景。

    我们可以在这个注解中更充分地发挥 Spring Expression Language 的作用。

    @ConditionalOnBean

    我们可能需要仅当某个Bean依赖的Bean存在应用上下文中才创建这个Bean:

    @Service
    @ConditionalOnBean(ApiDocConfig.class)
    class ApiDocService {
        // TODO
    }
    

    这个ApiDocService 仅当 ApiDocConfig 这个类的对象在应用上下文中才会加载。这个方法使我们可以定义依赖其他Bean的对象。(译者注:依赖对象存在则创建,否则不创建)

    @ConditionalOnMissingBean

    类似于上边的例子,可以使用 @ConditionalOnMissingBean 来确保应用上下文中没有某个对象才创建:

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

    这个例子演示了当应用上下文中没有其他 DataSource 对象时,创建 InMemoryDataSource这个对象,这和Spring Boot 在测试上下文创建内存数据源是类似的。

    @ConditionalOnResource

    这个注解仅当指定资源文件在 classpath 中是能找到的,才会加载这个配置

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

    这个 LoggingConfig 仅当 logback.xml 配置文件存在于 classpath 时才会加载。这样一来,我们能创建相似配置类,仅当它们的配置文件存在配置类才可用。


    上面描述的条件注解是我们在 Spring Boot 项目中通常使用的最常见的注解。 Spring Boot 提供了许多其他的条件注解。 然而,它们并不常见,更适合框架开发而不是应用程序开发(Spring Boot 在后台大量使用其中的一些)。 所以,让我们在这里简单地看一下它们。

    @ConditionalOnClass

    仅当指定类在classpath下才会加载这个bean,我们可指定类的全路径或这个类的名称

    @Service
    @ConditionalOnClass(name = "com.example.config.LoggingConfig")
    class LoggingService {
      // Code runs when class in available in classpath
    }
    
    @Service
    @ConditionalOnClass(LoggingConfig.class)
    class LoggingService {
      // Code runs when class in available in classpath
    }
    

    相似的,我们也可以用 @ConditionalOnMissingClass 注解来加载一个需要某个类在classpath 下时才加载的bean。

    @ConditionalOnWebApplication

    仅当此应用是一个web应用时加载这个bean,默认只要是web容器都会匹配,也可以使用type 属性来缩小范围。(译者注:type 共有3种,ANY 表示任何 web 容器,范围最大;SERVLET 表示仅 servlet 容器;``REACTIVE` 表示仅响应式容器)

    @Configuration
    @ConditionalOnWebApplication
    class RunsOnAnyWebApplication {
      // Code runs on web application
    }
    
    @Configuration
    @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
    class RunsOnServletBasedWebApplication {
      // Code runs on Servlet based web application
    }
    

    类似地,也可以用 @ConditionalOnNotWebApplication 来达到非 Web 容器才加载 bean 的目的。

    @ConditionalOnJava

    仅在等于或高于指定JVM版本时才加载某个 bean。默认是等于或高于指定版本,也可以设置为 range 属性表示一个版本区间。

    @Configuration
    @ConditionalOnJava(JavaVersion.EIGHT)
    class RunsOnJavaEightAndAbove {
      // Code runs on Java-8 and above versions
    }
    
    @Configuration
    @ConditionalOnJava(value = JavaVersion.ELEVEN, range = ConditionalOnJava.Range.OLDER_THAN)
    class RunsOnBelowJavaEleven {
      // Code runs below Java-11. 
      // Code doesn't run on Java-11 and above versions
    }
    

    @ConditionalOnCloudPlatform

    仅在指定云平台上运行应用时才会加载此注解标记的 bean

    @Configuration
    @ConditionalOnCloudPlatform(CloudPlatform.KUBERNETES)
    class RunsOnKubernetesCloudPlatform {
      // Code runs on application running on Kubernetes
    }
    

    @ConditionalOnWarDeployment

    仅在传统 War 包打包部署时加载该注解标记的 bean。在应用以内嵌服务器运行时这个条件返回false。

    @Configuration
    @ConditionalOnWarDeployment
    class RunsWithWarPackages {
      // Code runs with WAR package deployment
    }
    

    @ConditionalOnJndi

    仅在指定的 JNDI 路径存在时加载该注解标记的 bean。如果没有路径指定,则条件匹配仅基于 javax.naming.InitialContext 的存在。

    @Configuration
    @ConditionalOnJndi("java:comp/env/ejb/myEJB")
    class RunsWithJndiLocationAvailability {
      // Code runs when JNDI location is available
    }
    

    @ConditionalOnSingleCandidate

    @ConditionalOnBean 类似,但它是仅在这个 bean 是唯一的候选时启用。如果 BeanFactory 中已包含多个匹配的 bean 实例但已定义主 @Primary 候选者,则条件也将匹配;本质上,如果自动装配具有定义类型的 bean,则条件匹配将成功。 强烈建议仅在自动配置类上使用此条件。(译者注:有一个或多个中有主候选的 bean 将匹配成功)

    @Configuration
    @ConditionalOnSingleCandidate(DataSource.class)
    class RunsWithSingleDataSourceBean {
      // Code runs when single data source bean is determined
    }
    

    @ConditionalOnManagementPort

    仅当 management.server.port 条件与 server.port 具体某种关系时加载bean。

    有三种关系类型:

    • ManagementPortType.DISABLED:禁用或没定义管理端口
    • ManagementPortType.SAME:服务端口与管理端口相同
    • ManagementPortType.DIFFERENT:服务与管理端口不同
    @Configuration
    @ConditionalOnManagementPort(ManagementPortType.DISABLED)
    class ManagementPortIsDisabled {
      // Code runs when management port is disabled
    }
    
    @Configuration
    @ConditionalOnManagementPort(ManagementPortType.SAME)
    class ManagementPortIsSameAsServerPort {
      // Code runs when management port is same as server port
    }
    
    @Configuration
    @ConditionalOnManagementPort(ManagementPortType.DIFFERENT)
    class ManagementPortIsDifferentFromServerPort {
      // Code runs when management port is different from server port
    }
    

    @ConditionalOnAvailableEndpoint

    当指定的管理端点(译者注:接口地址)可用时加载 bean。如果一个端点被单独启用或使用 management.endpoints.web.exposure.include 暴露出来,都视为可用。

    @Configuration
    @ConditionalOnAvailableEndpoint(endpoint = InfoEndpoint.class)
    class InfoEndpointIsAvailable {
      // Code runs when info management endpoint in enabled and exposed
    }
    

    端点是由 @Endpoint@EndpointExtension 标记的bean。info 端点由 Spring Boot 提供,开箱即用。

    Custom Management Endpoint

    你也可以创建自定义的管理端点,就像这个例子:

    @Component
    @Endpoint(id = "custom-endpoint")
    public class CustomEndpoint {
    
        @ReadOperation
        public String print() {
            return "This is custom management endpoint";
        }
    }
    

    现在让我们写一个仅当自定义端点可用时加载的配置类:

    @Configuration
    @ConditionalOnAvailableEndpoint(endpoint = CustomEndpoint.class)
    class CustomEndpointConfiguration {
      // Configuration for custom endpoint
    }
    

    @ConditionalOnEnabledHealthIndicator

    仅当健康指示器配置 management.health.<name>.enabled 启用时才加载此健康检测指示器类,<name> 需要指定为具体的值。

    @Configuration
    @ConditionalOnEnabledHealthIndicator(value = "heartbeat")
    class HeatbeatHealthIndicator {
      // Code runs when management.health.heartbeat.enabled property is set to true.
    }
    

    总结

    Conditional 注解给了 Spring Boot 提供了自以为是的配置(译者注:默认配置),给我们提供基于 @Conditional 自定义条件加载类的灵活性,以及非常便捷的 @ConditionalOn... 注解。

    我们也可通过 AllNestedConditionsAnyNestedConditionNoneNestedCondition 组合自定义的条件,它们为我们提供了基于环境和其他条件的模块化编码方式。

    我们应该谨慎地使用 Conditional 注解,因为过度使用他们会导致难于调试与管理。

    本文中代码的示例在github/springboot-config

  • 相关阅读:
    (转)es进行聚合操作时提示Fielddata is disabled on text fields by default
    (转)Elasticsearch分析聚合
    搜索引擎-倒排索引基础知识(摘自《这就是搜索引擎:核心技术详解》)
    Elasticsearch5.x Head插件安装
    Java开发规范总结
    (转)ElasticSearch Java Api-检索索引库
    QTGraphics-View拖拽以及鼠标指针操作
    Qt Graphics-View的打印功能实现
    Qt使用QPainter绘制矢量图并保存为svg文件
    QT制作一个位图画图板程序
  • 原文地址:https://www.cnblogs.com/hellxz/p/16253857.html
Copyright © 2020-2023  润新知