故事布景

故事发生在几个星期前,自动化渠道代码开放给整个测验团队今后,越来越多的同事开端探究渠道代码。为了保障自动化测验相关的数据和沉淀能不被污染,把数据库进行了隔离。总算有了2个数据库实例,一个给dev环境用,一个给test环境用。但是随着渠道的发展,越来越多的中间件被引用了。所以需求隔离的东西就越来越多了,比方MQ,Redis等。成本越来越高,假如像数据库实例相同全部分开搞一套,那在当下全域降本增效的大潮下,也是困难重重。
​经过线下调查和造访发现,这些探究的同学并不是需求全渠道的才能,其间有许多模块或许子系统,同学并不关心。因而就产生了一个主意,隔离掉这些类或许不运用这些和中间件相关的类应该就可以了 。然后由于咱们的渠道是根据springboot开发的,自然而然的想到了@Conditional注解。

调试&处理

以AWS SQS为例,先添加上了注解@ConditionalOnProperty根据装备信息中的coverage.aws.topic特点进行判别,假如存在这个装备就进行CoverageSQSConfig的Spring Bean的加载。

@Configuration
@ConditionalOnProperty(
        name = "coverage.aws.topic"
)
public class CoverageSQSConfig {
    @Value("${coverage.aws.region}")
    private String awsRegion;
    @Value("${coverage.aws.access.key}")
    private String accessKey;
    @Value("${coverage.aws.secret.key}")
    private String secretKey;
    @Bean(name = "coverageSQSListenerFactory")
    public DefaultJmsListenerContainerFactory sqsListenerContainerFactory(){
        return getDefaultJmsListenerContainerFactory(awsRegion, accessKey, secretKey);
    }
    private DefaultJmsListenerContainerFactory getDefaultJmsListenerContainerFactory(String awsRegion, String accessKey, String secretKey) {
        DefaultJmsListenerContainerFactory sqsFactory = new DefaultJmsListenerContainerFactory();
        sqsFactory.setConnectionFactory(new SQSConnectionFactory(
                new ProviderConfiguration(),
                AmazonSQSClientBuilder.standard()
                        .withRegion(Region.of(awsRegion).id())
                        .withCredentials(new AWSStaticCredentialsProvider(new BasicAWSCredentials(accessKey, secretKey)))
                        .build()));
        sqsFactory.setConcurrency("3-10");
        sqsFactory.setReceiveTimeout(10*1000L);
        sqsFactory.setRecoveryInterval(1000L);
        return sqsFactory;
    }
}

为调试这个内容的作用,这儿列出了2次调试的作用比照:首先是把补白字段全部都注释掉。

SpringBoot系列:用配置影响Bean加载 @ConditionalOnProperty

SpringBoot系列:用配置影响Bean加载 @ConditionalOnProperty

经过上图很明显,当coverage.aws.topic特点不存在的时候,不能找到被Spring统一管理的bean。

第二次是把补白的注释都取消掉,重启后能找到bean。

SpringBoot系列:用配置影响Bean加载 @ConditionalOnProperty

问题处理了吗?其时就想再看下SpringBoot是怎么做的经过这个注解就这么便利的过滤了这个bean的加载,以及是否有什么其他的用法或许特性。

SpringBoot 是怎么做的

经过@ConditionalOnProperty注解,很快能定位到它是位于 autoconfigure模块的特性。**
**

SpringBoot系列:用配置影响Bean加载 @ConditionalOnProperty

顺藤摸瓜,很快就能找到注解是在哪里进行运用的

package org.springframework.boot.autoconfigure.condition;
...
@Order(Ordered.HIGHEST_PRECEDENCE + 40)
class OnPropertyCondition extends SpringBootCondition {
  @Override
  public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
    // 经过获类原始数据上的ConditionalOnProperty注解的参数值
    List<AnnotationAttributes> allAnnotationAttributes = annotationAttributesFromMultiValueMap(
        metadata.getAllAnnotationAttributes(ConditionalOnProperty.class.getName()));
    List<ConditionMessage> noMatch = new ArrayList<>();
    List<ConditionMessage> match = new ArrayList<>();
    for (AnnotationAttributes annotationAttributes : allAnnotationAttributes) {
     // 经过特点值,逐一判别装备信息中的信息是否满意 , context.getEnvironment() 能获取到一切的装备信息
      ConditionOutcome outcome = determineOutcome(annotationAttributes, context.getEnvironment());
      (outcome.isMatch() ? match : noMatch).add(outcome.getConditionMessage());
    }
    if (!noMatch.isEmpty()) {
      return ConditionOutcome.noMatch(ConditionMessage.of(noMatch));
    }
    return ConditionOutcome.match(ConditionMessage.of(match));
  }
  private List<AnnotationAttributes> annotationAttributesFromMultiValueMap(
      MultiValueMap<String, Object> multiValueMap) {
    ...
    return annotationAttributes;
  }
  private ConditionOutcome determineOutcome(AnnotationAttributes annotationAttributes, PropertyResolver resolver) {
    Spec spec = new Spec(annotationAttributes);
    List<String> missingProperties = new ArrayList<>();
    List<String> nonMatchingProperties = new ArrayList<>();
    // 经过特点值,判别装备信息中的信息是否满意
    spec.collectProperties(resolver, missingProperties, nonMatchingProperties);
    if (!missingProperties.isEmpty()) {
      return ConditionOutcome.noMatch(ConditionMessage.forCondition(ConditionalOnProperty.class, spec)
          .didNotFind("property", "properties").items(Style.QUOTE, missingProperties));
    }
    if (!nonMatchingProperties.isEmpty()) {
      return ConditionOutcome.noMatch(ConditionMessage.forCondition(ConditionalOnProperty.class, spec)
          .found("different value in property", "different value in properties")
          .items(Style.QUOTE, nonMatchingProperties));
    }
    return ConditionOutcome
        .match(ConditionMessage.forCondition(ConditionalOnProperty.class, spec).because("matched"));
  }
  private static class Spec {
    private final String prefix;
    private final String havingValue;
    private final String[] names;
    private final boolean matchIfMissing;
    Spec(AnnotationAttributes annotationAttributes) {
      ...
    }
    private String[] getNames(Map<String, Object> annotationAttributes) {
      ...
    }
    private void collectProperties(PropertyResolver resolver, List<String> missing, List<String> nonMatching) {
      for (String name : this.names) {
        String key = this.prefix + name;
        if (resolver.containsProperty(key)) {
        // havingValue 默以为 "" 
          if (!isMatch(resolver.getProperty(key), this.havingValue)) {
            nonMatching.add(name);
          }
        }
        else {
          if (!this.matchIfMissing) {
            missing.add(name);
          }
        }
      }
    }
    private boolean isMatch(String value, String requiredValue) {
      if (StringUtils.hasLength(requiredValue)) {
        return requiredValue.equalsIgnoreCase(value);
      }
      // havingValue 默以为 "" ,因而只要对应的特点不为false,在注解中没填havingValue的情况下,都是会match上conditon,即都会被加载
      return !"false".equalsIgnoreCase(value);
    }
    @Override
    public String toString() {
      ...
    }
  }
}

用这种方法进行SpingBoot扩展的也特别多,SpingBoot自己的autoconfigure模块中有许多模块的增强用的也是这个注解。

SpringBoot系列:用配置影响Bean加载 @ConditionalOnProperty

那他是在哪个环节进行的这个condition的判别呢?简略标示如下:

SpringBoot系列:用配置影响Bean加载 @ConditionalOnProperty

其间判别过滤的总入口:

// org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider
  /**
   * Determine whether the given class does not match any exclude filter
   * and does match at least one include filter.
   * @param metadataReader the ASM ClassReader for the class
   * @return whether the class qualifies as a candidate component
   */
  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())) {
        // conditons 相关的入口,
        return isConditionMatch(metadataReader);
      }
    }
    return false;
  }

环顾整个流程,这儿比较好的一点便是一旦条件过滤后,那就对类元文件里边的其他内容也不进行加载,像下面的@Value和@Bean的填充也不会进行,能高雅高效的处理掉当时的问题。

    @Value("${coverage.aws.region}")
    private String awsRegion;
    @Value("${coverage.aws.access.key}")
    private String accessKey;
    @Value("${coverage.aws.secret.key}")
    private String secretKey;
    @Bean(name = "coverageSQSListenerFactory")
    public DefaultJmsListenerContainerFactory sqsListenerContainerFactory(){
        return getDefaultJmsListenerContainerFactory(awsRegion, accessKey, secretKey);
    }

故事的最终

做完这个改动今后,就提交了代码,妈妈再也不用忧虑由于其他人不小心运用某些只有一个实例的中间件导致数据污染了。用注解方法处理这个经过装备就能控制加载bean的这个才能的确很便利很Boot。比方中间件团队提供组件才能给团队,用condtion的这个特性也是能便利落地的。当然condition里边还有其他的一些特性,这儿只是抛砖引玉,简略的梳理一下最近的一个运用场景。


微信公众号:树叶小记