我們在 Spring/ Spring Boot Starter 或者一些框架的原始碼裡經常能看到類似如下的註解宣告,可能作用在類上,也可能在某個方法上:
@ConditionalOnProperty(name = "spring.cloud.refresh.enabled", matchIfMissing = true) @ConditionalOnProperty(prefix = "management.metrics.export.atlas", name = "enabled", havingValue = "true", matchIfMissing = true)
我們一眼都能看出來,這是來「談條件」的。需要滿足某個屬性存在,或者屬性值是xx這一類的。
對於屬性的匹配,是會在 Environment 裡查詢是否包含當前需要的屬性,如果沒指定 havingValue 的話,那需要同時屬性的值不為「false」這個字串,其它的東西都視為true。
今天的這篇做為鋪墊,先來描述一下註解的工作原理,後面一篇我會寫寫與此有關的一個有趣的案例。
工作原理
濃縮版
在SpringBoot 啟動過程中,會掃描當前依賴裡的 @Configuration,然後遍歷的過程中會判斷其中哪些是要講條件的。對於講條件的這些,會判斷
shouldSkip ,這裡的是否跳過,會根據註解作用在類上,方法上,轉向不同的Metadata,提取對應的實現類,但本質上還是透過 resolver 去Environment 裡找找這個屬性在不在,不在跳過,在的話是否值匹配。從而決定 Confirutaion 是否生效。
原始碼版
我們知道 Spring 啟動的過程,也是建立和初始化Bean 的過程,在這個過程中,會先拿到BeanNames,並一個個的去建立和初始化。
此時,對於Configuration,是透過BeanPostProcessor的方式來處理的.
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) { int registryId = System.identityHashCode(registry); this.registriesPostProcessed.add(registryId); processConfigBeanDefinitions(registry);// 對,是這裡 }
部分呼叫棧如下:
java.lang.Thread.State: RUNNABLE at org.springframework.boot.autoconfigure.condition.OnPropertyCondition.getMatchOutcome(OnPropertyCondition.java:65) at org.springframework.boot.autoconfigure.condition.SpringBootCondition.matches(SpringBootCondition.java:47) at org.springframework.context.annotation.ConditionEvaluator.shouldSkip(ConditionEvaluator.java:108) at org.springframework.context.annotation.ConfigurationClassBeanDefinitionReader.loadBeanDefinitionsForBeanMethod(ConfigurationClassBeanDefinitionReader.java:181) at org.springframework.context.annotation.ConfigurationClassBeanDefinitionReader.loadBeanDefinitionsForConfigurationClass(ConfigurationClassBeanDefinitionReader.java:142) at org.springframework.context.annotation.ConfigurationClassBeanDefinitionReader.loadBeanDefinitions(ConfigurationClassBeanDefinitionReader.java:118) at org.springframework.context.annotation.ConfigurationClassPostProcessor.processConfigBeanDefinitions(ConfigurationClassPostProcessor.java:328) at org.springframework.context.annotation.ConfigurationClassPostProcessor.postProcessBeanDefinitionRegistry(ConfigurationClassPostProcessor.java:233)
這裡對於 Class 和 Method,都在該方法中,處理入口不一樣,傳入的Meta也有所區別
/** * Build and validate a configuration model based on the registry of * {@link Configuration} classes. */ public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) { List<BeanDefinitionHolder> configCandidates = new ArrayList<>(); String[] candidateNames = registry.getBeanDefinitionNames(); for (String beanName : candidateNames) { BeanDefinition beanDef = registry.getBeanDefinition(beanName); if (ConfigurationClassUtils.isFullConfigurationClass(beanDef) || ConfigurationClassUtils.isLiteConfigurationClass(beanDef)) { } else if (ConfigurationClassUtils.checkConfigurationClassCandidate(beanDef, this.metadataReaderFactory)) { configCandidates.add(new BeanDefinitionHolder(beanDef, beanName)); } } // Return immediately if no @Configuration classes were found if (configCandidates.isEmpty()) { return; } // Parse each @Configuration class ConfigurationClassParser parser = new ConfigurationClassParser( this.metadataReaderFactory, this.problemReporter, this.environment, this.resourceLoader, this.componentScanBeanNameGenerator, registry); Set<BeanDefinitionHolder> candidates = new LinkedHashSet<>(configCandidates); Set<ConfigurationClass> alreadyParsed = new HashSet<>(configCandidates.size()); do { parser.parse(candidates); // 這裡處理class parser.validate(); Set<ConfigurationClass> configClasses = new LinkedHashSet<>(parser.getConfigurationClasses()); configClasses.removeAll(alreadyParsed); // Read the model and create bean definitions based on its content if (this.reader == null) { this.reader = new ConfigurationClassBeanDefinitionReader( registry, this.sourceExtractor, this.resourceLoader, this.environment, this.importBeanNameGenerator, parser.getImportRegistry()); } this.reader.loadBeanDefinitions(configClasses); // 這裡處理Method alreadyParsed.addAll(configClasses); while (!candidates.isEmpty()); }
裡面的邏輯,則都是在判斷這些Condition 是否match,重點看這一行
condition.matches(this.context, metadata)
for (Condition condition : conditions) { ConfigurationPhase requiredPhase = null; if (condition instanceof ConfigurationCondition) { requiredPhase = ((ConfigurationCondition) condition).getConfigurationPhase(); } if ((requiredPhase == null || requiredPhase == phase) && !condition.matches(this.context, metadata)) { return true; } }
透過觀察 Condition 這個介面你也能發現,和我們上面說的一樣,這裡不同的處理metadata是不同的。
在 SpringBoot 裡,ConditionalOnProperty 的 Condition 實現,運用了一個模板方法模式, SpringBootCondition 做為模板,再呼叫各子類的實現方法。
public final boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { String classOrMethodName = getClassOrMethodName(metadata); ConditionOutcome outcome = getMatchOutcome(context, metadata);// 這裡交給了抽象方法 recordEvaluation(context, classOrMethodName, outcome); return outcome.isMatch(); }
來看子類的實現
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")); }
有了這個判斷,對於 OnClass 之類的,你也能猜個八九不離十。
同樣會有一個子類的實現
只不過判斷的從屬性,換成了在classloader裡查詢已載入的類。