2024-12-18  阅读(57)
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 本文链接:https://www.skjava.com/mianshi/baodian/detail/1377010615

回答

@Conditional 用于基于某些条件动态地注册或不注册一个 bean。

在使用 @Conditional 注解时,我们需要指定一个实现了 Condition 接口的类,并实现它的 matches()。在 Spring 容器启动时,Spring 会调用这个类的 matches() 来决定是否要创建和注册标有 @Conditional 注解的 bean,只有 matches() 返回 true 的 bean 才会注册。

@Conditional 详解

简介

@Conditional 注解位于 org.springframework.context.annotation 包中,定义如下:

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Conditional {
  Class<? extends Condition>[] value();
}

可以传多个参数。Spring Boot 中大量使用了 @Conditional

@Conditional 我们需要制定一个实现了 Condition 接口的类,Condition 接口定义如下:

@FunctionalInterface
public interface Condition {
  boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);
}

只有 matches() 返回 true 的时候,该 bean 才会被注入。

使用 @Conditional

我们先看一个简单的示例:假如我们有三套环境:Linux、Windows、MacOS,每套环境的配置都不一样,我们应用程序会在每个环境上面部署。如果在 Linux 环境我们就注册 Linux 的配置文件,如果在 Windows 环境我们就注册 Windows 的配置文件。

  • 首先我们新建三个环境的配置类:
public class LinuxConfig {
}

public class MacOSConfig {
}

public class WindowsConfig {
}

  • 创建三个 ConfigCondition ,分别对应 Linux、Windows、MacOS
public class LinuxConfigCondition implements Condition {
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        String systemName = context.getEnvironment().getProperty("os.name");
        return "Linux".equals(systemName);
    }
}

public class MacOSConfigCondition implements Condition {
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        String systemName = context.getEnvironment().getProperty("os.name");
        return "Mac OS X".equals(systemName);
    }
}

public class WindowsConfigCondition implements Condition {
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        String systemName = context.getEnvironment().getProperty("os.name");
        return "Windows".equals(systemName);
    }
}

  • 新建一个 AppConfig,它用于匹配注册环境,如果系统是 Linux 环境,就注册 LinuxConfig,如果是 Windows 环境就注册 WindowsConfig,如果是 MacOS 环境,就注册 MacOSConfig。
@Configuration
public class AppConfig {

    /**
     * Linux 环境
     * @return
     */
    @Conditional(LinuxConfigCondition.class)
    @Bean
    public LinuxConfig linuxConfig() {
        return new LinuxConfig();
    }

    /**
     * Windows 环境
     * @return
     */
    @Conditional(WindowsConfigCondition.class)
    @Bean
    public WindowsConfig windowsConfig() {
        return new WindowsConfig();
    }

    /**
     * MacOS 环境
     * @return
     */
    @Conditional(MacOSConfigCondition.class)
    @Bean
    public MacOSConfig macOSConfig() {
        return new MacOSConfig();
    }
}
  • 新建测试类
public class SkApplication {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
        for (String definitioonName : applicationContext.getBeanDefinitionNames()) {
            System.out.println("name = " + definitioonName);
        }
    }
}

我的是 MacBook,所以执行结果如下:

我们也可以通过配置:

然后执行

上面的 @Conditional 是放在方法上面在,放在类上面也是一样的:

@Component
@Conditional(LinuxConfigCondition.class)
public class LinuxConfig {
}
....

多条件 @conditional

@Conditional 的定义中可以看出 value() 默认传递的是一个数组,所以它是可以接受多个 Condition。还是上面的例子:

@Configuration
public class AppConfig {

    /**
     * Linux 环境
     * @return
     */
    @Conditional(value = {LinuxConfigCondition.class, FlagCondition.class})
    @Bean
    public LinuxConfig linuxConfig() {
        return new LinuxConfig();
    }
    
    //....
}

新增一个 FlagCondition:

public class FlagCondition implements Condition {
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        String flag = context.getEnvironment().getProperty("flag");
        return "1".equals(flag);
    }
}

我们现在 VM options 中配置:-Dos.name=Linux -Dflag=0

再将其调整为-Dos.name=Linux -Dflag=1

总体来说,@Conditional 在实际开发中应用场景还是蛮多的,用处很大。

@Conditional 的衍生注解

Spring 还提供了基于 @Conditional 的衍生注解,用于处理更具体的情形,包括:

注解 描述
@ConditionalOnBean 当容器中存在指定的 Bean 时,条件成立
@ConditionalOnMissingBean 当容器中不存在指定的 Bean 时,条件成立
@ConditionalOnClass 当类路径下存在指定的类时,条件成立
@ConditionalOnMissingClass 当类路径下不存在指定的类时,条件成立
@ConditionalOnProperty 当指定的配置属性有一个明确的值时,条件成立
@ConditionalOnResource 当类路径下存在指定的资源时,条件成立
@ConditionalOnExpression 基于 SpEL 表达式的评估结果,条件成立
@ConditionalOnWebApplication 当应用是一个 Web 应用时,条件成立
@ConditionalOnNotWebApplication 当应用不是一个 Web 应用时,条件成立
@ConditionalOnJndi 当指定的 JNDI 存在时,条件成立

@ConditionalOnProperty

这里面的 @ConditionalOnProperty 是一个很常用的注解,它的使用场景有如下几个:

  • 功能开发:基于配置文件开启或关闭特定的功能
  • 环境适应:根据不同的环境(开发、测试、生产)激活不同的配置

下面是他的定义:

@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE, ElementType.METHOD })
@Documented
@Conditional(OnPropertyCondition.class)
public @interface ConditionalOnProperty {

  String[] value() default {};

  String prefix() default "";

  String[] name() default {};

  String havingValue() default "";

  boolean matchIfMissing() default false;
}
  • value :数组,该属性与下面的 name 属性不可同时使用,表示要检查的属性值。
  • prefix:用于定义属性名称的前缀
  • name:作用与 value 一致
  • havingValue:与 value 或者 name 配合使用,只有当 value 或者 name 的值与 havingValue 相等时,才会注册成功。
  • matchIfMissing:如果设置为 true,配置文件中缺少对应的value或name的对应的属性值,也会注入成功。默认为 false。

比如:

@Configuration
@ConditionalOnProperty(prefix = "distributed.lock",value = "redis",havingValue = "1")
public class RedisDistributedLock {
}

在配置文件 distributed.lock.redis=1 时,才会注入成功。

@Conditional 源码分析

在 Spring 中,@Conditional 的处理主要是由ConditionEvaluator 和 ConfigurationClassPostProcessor 完成。ClassPathBeanDefinitionScanner 作为扫描器用来扫描 @Component 标注的类,创建 BeanDefinition 并注册到 BeanFactory。ConfigurationClassPostProcessor 用来处理 @Configuration,解析出其中包含 @Bean 的方法,创建 BeanDefinition 并注册到 BeanFactory 中。在注册时发现 Class 或者 Method 包含 @Conditional,会创建配置的 Condition 实现类对象,根据 matches() 方法来决定是否注册当前 BeanDefinition。

我们直接看 ClassPathBeanDefinitionScanner 的 doScan()

  protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
    Assert.notEmpty(basePackages, "At least one base package must be specified");
    Set<BeanDefinitionHolder> beanDefinitions = new LinkedHashSet<>();
    for (String basePackage : basePackages) {
      Set<BeanDefinition> candidates = findCandidateComponents(basePackage);
      for (BeanDefinition candidate : candidates) {
        // 省略代码...
      }
    }
    return beanDefinitions;
  }

findCandidateComponents() 扫描类路径查找符合要求的 Bean 组件:

  public Set<BeanDefinition> findCandidateComponents(String basePackage) {
    if (this.componentsIndex != null && indexSupportsIncludeFilters()) {
      return addCandidateComponentsFromIndex(this.componentsIndex, basePackage);
    }
    else {
      return scanCandidateComponents(basePackage);
    }
  }

调用 scanCandidateComponents()

  private Set<BeanDefinition> scanCandidateComponents(String basePackage) {
    Set<BeanDefinition> candidates = new LinkedHashSet<>();
    try {
      // ...
      for (Resource resource : resources) {
        if (traceEnabled) {
          logger.trace("Scanning " + resource);
        }
        try {
          MetadataReader metadataReader = getMetadataReaderFactory().getMetadataReader(resource);
          if (isCandidateComponent(metadataReader)) {
            // ...
          }
          else {
            // ...
          }
        }
        // ...
      }
    }
    catch (IOException ex) {
      throw new BeanDefinitionStoreException("I/O failure during classpath scanning", ex);
    }
    return candidates;
  }

调用 isCandidateComponent() 进行一次过滤:

  protected boolean isCandidateComponent(MetadataReader metadataReader) throws IOException {
    for (TypeFilter tf : this.excludeFilters) {
      if (tf.match(metadataReader, getMetadataReaderFactory())) {
        return false;
      }
    }
    for (TypeFilter tf : this.includeFilters) {
      if (tf.match(metadataReader, getMetadataReaderFactory())) {
        return isConditionMatch(metadataReader);
      }
    }
    return false;
  }

isConditionMatch() 则是校验是否包含 @Conditional

  private boolean isConditionMatch(MetadataReader metadataReader) {
    if (this.conditionEvaluator == null) {
      this.conditionEvaluator =
          new ConditionEvaluator(getRegistry(), this.environment, this.resourcePatternResolver);
    }
    return !this.conditionEvaluator.shouldSkip(metadataReader.getAnnotationMetadata());
  }

如果 conditionEvaluator 为空,则构建一个,然后调用其 shouldSkip()

  public boolean shouldSkip(@Nullable AnnotatedTypeMetadata metadata, @Nullable ConfigurationPhase phase) {
    // 是否有 @Conditional 标注,没有就直接返回false
    if (metadata == null || !metadata.isAnnotated(Conditional.class.getName())) {
      return false;
    }

    if (phase == null) {
      if (metadata instanceof AnnotationMetadata &&
          ConfigurationClassUtils.isConfigurationCandidate((AnnotationMetadata) metadata)) {
        return shouldSkip(metadata, ConfigurationPhase.PARSE_CONFIGURATION);
      }
      return shouldSkip(metadata, ConfigurationPhase.REGISTER_BEAN);
    }
    
    // 解析 @Conditional 中的 value 属性,也就是实现 Condition 接口的类
    List<Condition> conditions = new ArrayList<>();
    for (String[] conditionClasses : getConditionClasses(metadata)) {
      for (String conditionClass : conditionClasses) {
        Condition condition = getCondition(conditionClass, this.context.getClassLoader());
        conditions.add(condition);
      }
    }
    
    // 排序
    AnnotationAwareOrderComparator.sort(conditions);

    for (Condition condition : conditions) {
      ConfigurationPhase requiredPhase = null;
      // 是否为 ConfigurationCondition 
      if (condition instanceof ConfigurationCondition) {
        requiredPhase = ((ConfigurationCondition) condition).getConfigurationPhase();
      }
      // 这里调用 Condition 的 matches()
      if ((requiredPhase == null || requiredPhase == phase) && !condition.matches(this.context, metadata)) {
        return true;
      }
    }

    return false;
  }

Java 面试宝典是大明哥全力打造的 Java 精品面试题,它是一份靠谱、强大、详细、经典的 Java 后端面试宝典。它不仅仅只是一道道面试题,而是一套完整的 Java 知识体系,一套你 Java 知识点的扫盲贴。

它的内容包括:

  • 大厂真题:Java 面试宝典里面的题目都是最近几年的高频的大厂面试真题。
  • 原创内容:Java 面试宝典内容全部都是大明哥原创,内容全面且通俗易懂,回答部分可以直接作为面试回答内容。
  • 持续更新:一次购买,永久有效。大明哥会持续更新 3+ 年,累计更新 1000+,宝典会不断迭代更新,保证最新、最全面。
  • 覆盖全面:本宝典累计更新 1000+,从 Java 入门到 Java 架构的高频面试题,实现 360° 全覆盖。
  • 不止面试:内容包含面试题解析、内容详解、知识扩展,它不仅仅只是一份面试题,更是一套完整的 Java 知识体系。
  • 宝典详情:https://www.yuque.com/chenssy/sike-java/xvlo920axlp7sf4k
  • 宝典总览:https://www.yuque.com/chenssy/sike-java/yogsehzntzgp4ly1
  • 宝典进展:https://www.yuque.com/chenssy/sike-java/en9ned7loo47z5aw

目前 Java 面试宝典累计更新 400+ 道,总字数 42w+。大明哥还在持续更新中,下图是大明哥在 2024-12 月份的更新情况:

想了解详情的小伙伴,扫描下面二维码加大明哥微信【daming091】咨询

同时,大明哥也整理一套目前市面最常见的热点面试题。微信搜[大明哥聊 Java]或扫描下方二维码关注大明哥的原创公众号[大明哥聊 Java] ,回复【面试题】 即可免费领取。

阅读全文