天天看點

SpringBoot系列:用配置影響Bean加載 @ConditionalOnProperty

作者:小小怪下士的架構攻略

故事背景

故事發生在幾個星期前,自動化平台代碼開放給整個測試團隊以後,越來越多的同僚開始探索平台代碼。為了保障自動化測試相關的資料和沉澱能不被污染,把資料庫進行了隔離。終于有了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裡面還有其他的一些特性,這裡隻是抛磚引玉,簡單的梳理一下最近的一個使用場景。

作者:樹葉小記

連結:https://juejin.cn/post/7218362171965472827

繼續閱讀