@Conditional注解有什么用?

 作者简介:大家好,我是码炫码哥,前中兴通讯、美团架构师,现任某互联网公司CTO,兼职码炫课堂主讲源码系列专题


代表作:《jdk源码&多线程&高并发》,《深入tomcat源码解析》,《深入netty源码解析》,《深入dubbo源码解析》,《深入springboot源码解析》,《深入spring源码解析》,《深入redis源码解析》等


联系qq:184480602,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬。码炫课堂的个人空间-码炫码哥个人主页-面试,源码等

回答

@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;
  }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值