天天看點

助力SpringBoot自動配置的條件注解ConditionalOnXXX分析--SpringBoot源碼(三)1 前言2 SpringBoot的派生條件注解3 Condition接口4 SpringBootCondition源碼解析5 如何擴充SpringBootCondition

注:該源碼分析對應SpringBoot版本為2.1.0.RELEASE

1 前言

本篇接

如何分析SpringBoot源碼子產品及結構?--SpringBoot源碼(二)

上一篇分析了SpringBoot源碼結構及各個子產品pom之間的關系後,那麼此篇開始就開始解開SpringBoot新特性之一--自動配置的神秘面紗了。因為SpringBoot自動配置原理是基于其大量的條件注解

ConditionalOnXXX

,是以,本節我們先來撸下Spring的條件注解的相關源碼。

2 SpringBoot的派生條件注解

我們都知道,SpringBoot自動配置是需要滿足相應的條件才會自動配置,是以SpringBoot的自動配置大量應用了條件注解

ConditionalOnXXX

。如下圖:

那麼上圖的條件注解如何使用呢?

舉個栗子,我們來看下如何使用

@ConditionalOnClass

@ConditionalOnProperty

這兩個注解,先看下圖代碼:

HelloWorldEnableAutoConfiguration

這個自動配置類應用了

@ConditionalOnClass

ConditionalOnProperty

兩個條件注解,那麼隻有在滿足:

classpath

中存在

HelloWorldComponent.class

和配置了

hello.world.name

hello.world.age

屬性這兩個條件的情況下才會建立

HelloWorldComponent

這個

bean

其實SpringBoot的

@ConditionalOnXXX

等條件注解都是派生注解,那麼什麼是派生注解呢?

就拿上面的栗子來說,以

@ConditionalOnClass(HelloWorldComponent.class)

為例,我們打開

ConditionalOnClass

注解源碼,如下:

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

    Class<?>[] value() default {};

    String[] name() default {};

}           

可以看到

@ConditionalOnClass

注解上面又标注了

@Conditional(OnClassCondition.class)

注解,是以

@ConditionalOnClass

@Conditional

的派生注解,

@Conditional(OnClassCondition.class)

@ConditionalOnClass

注解是等價的,即這兩個注解标注在某個配置類上的效果是等價的。

而SpringBoot的自動配置原理正是建立在這些大量的派生條件注解

@ConditionalOnXXX

之上,而這些條件注解的原理跟Spring的Condition接口有關。是以我們先來研究下Condition接口的相關源碼。

3 Condition接口

3.1 Condition接口源碼分析

分析Condition接口源碼前先看下如何自定義

ConditionalOnXXX

注解,舉個栗子,比如自定義一個

@ConditionalOnLinux

注解,該注解隻有在其屬性

environment

是"linux"才會建立相關的bean。定義了以下代碼:

/**
 * 實作spring 的Condition接口,并且重寫matches()方法,如果@ConditionalOnLinux的注解屬性environment是linux就傳回true
 *
 */
public class LinuxCondition implements Condition {

    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        // 獲得注解@ConditionalOnLinux的所有屬性
        List<AnnotationAttributes> allAnnotationAttributes = annotationAttributesFromMultiValueMap(
                metadata.getAllAnnotationAttributes(
                        ConditionalOnLinux.class.getName()));
        for (AnnotationAttributes annotationAttributes : allAnnotationAttributes) {
            // 獲得注解@ConditionalOnLinux的environment屬性
            String environment = annotationAttributes.getString("environment");
            // 若environment等于linux,則傳回true
            if ("linux".equals(environment)) {
                return true;
            }
        }
        return false;
    }
}           
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(LinuxCondition.class)
public @interface ConditionalOnLinux {
    // 标注是哪個環境
    String environment() default "";

}           
@Configuration
public class ConditionConfig {
        // 隻有`@ConditionalOnLinux`的注解屬性`environment`是"linux"時才會建立bean
    @Bean
    @ConditionalOnLinux(environment = "linux")
    public Environment linuxEnvironment() {
        return new LinuxEnvironment();
    }
}           

上面的代碼我們捋一下:

  1. LinuxCondition

    實作了

    Condition

    接口并實作了

    matches

    方法,而

    matches

    方法則判斷

    @ConditionalOnLinux

    的注解屬性

    environment

    是否"linux",是則傳回true,否則false。
  2. 然後我們再定義了一個注解

    @ConditionalOnLinux

    ,這個注解是

    @Conditional

    的派生注解,與

    @Conditional(LinuxCondition.class)

    等價,注意

    @ConditionalOnLinux

    注解定義了一個屬性

    environment

    。而我們最終可以利用

    LinuxCondition

    matches

    方法中的參數

    AnnotatedTypeMetadata

    來擷取

    @ConditionalOnLinux

    environment

    的值,進而用來判斷值是否為linux"。
  3. 最後我們又定義了一個配置類

    ConditionConfig

    ,在

    linuxEnvironment

    方法上标注了

    @ConditionalOnLinux(environment = "linux")

    。是以,這裡隻有

    LinuxCondition

    matches

    方法傳回true才會建立

    bean

學會了如何自定義

@ConditionalOnXXX

注解後,我們現在再來看下

Condition

接口的源碼:

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

Condition接口主要有一個

matches

方法,該方法決定了是否要注冊相應的

bean

對象。其中

matches

方法中有兩個參數,參數類型分别是

ConditionContext

AnnotatedTypeMetadata

,這兩個參數非常重要。它們分别用來擷取一些環境資訊和注解中繼資料進而用在

matches

方法中判斷是否符合條件。

ConditionContext

,顧名思義,主要是跟

Condition

的上下文有關,主要用來擷取

Registry

,

BeanFactory

Environment

ResourceLoader

ClassLoader

等。那麼擷取這些用來幹什麼呢?舉個栗子,比如

OnResourceCondition

需要靠

ConditionContext

ResourceLoader

來加載指定資源,

OnClassCondition

ConditionContext

ClassLoader

來加載指定類等,下面看下其源碼:
public interface ConditionContext {
    BeanDefinitionRegistry getRegistry();
    @Nullable
    ConfigurableListableBeanFactory getBeanFactory();
    Environment getEnvironment();
    ResourceLoader getResourceLoader();
    @Nullable
    ClassLoader getClassLoader();
}           

AnnotatedTypeMetadata

,這個跟注解中繼資料有關,利用

AnnotatedTypeMetadata

可以拿到某個注解的一些中繼資料,而這些中繼資料就包含了某個注解裡面的屬性,比如前面的栗子,利用

AnnotatedTypeMetadata

可以拿到

@ConditionalOnLinux

environment

的值。下面看下其源碼:
public interface AnnotatedTypeMetadata {
    boolean isAnnotated(String annotationName);
    Map<String, Object> getAnnotationAttributes(String annotationName);
    Map<String, Object> getAnnotationAttributes(String annotationName, boolean classValuesAsString);
    MultiValueMap<String, Object> getAllAnnotationAttributes(String annotationName);
    MultiValueMap<String, Object> getAllAnnotationAttributes(String annotationName, boolean classValuesAsString);
}           

回到剛才的栗子,我們知道

@ConditionalOnLinux

注解真正起作用的是

Condition

接口的具體實作類

LinuxCondition

matches

方法,那麼這個

matches

方法是在何時被調用的呢?

通過idea調試看調用的棧幀,如下圖:

發現是在

ConditionEvaluator

shouldSkip

方法中調用了

LinuxCondition

matches

方法,自然我們再去看看

ConditionEvaluator

shouldSkip

的方法執行了什麼邏輯。

// 這個方法主要是如果是解析階段則跳過,如果是注冊階段則不跳過
public boolean shouldSkip(@Nullable AnnotatedTypeMetadata metadata, @Nullable ConfigurationPhase phase) {
    // 若沒有被@Conditional或其派生注解所标注,則不會跳過
    if (metadata == null || !metadata.isAnnotated(Conditional.class.getName())) {
        return false;
    }
    // 沒有指定phase,注意phase可以分為PARSE_CONFIGURATION或REGISTER_BEAN類型
    if (phase == null) {
        // 若标有@Component,@Import,@Bean或@Configuration等注解的話,則說明是PARSE_CONFIGURATION類型
        if (metadata instanceof AnnotationMetadata &&
                ConfigurationClassUtils.isConfigurationCandidate((AnnotationMetadata) metadata)) {
            return shouldSkip(metadata, ConfigurationPhase.PARSE_CONFIGURATION);
        }
        // 否則是REGISTER_BEAN類型
        return shouldSkip(metadata, ConfigurationPhase.REGISTER_BEAN);
    }

    List<Condition> conditions = new ArrayList<>();
    // TODO 獲得所有标有@Conditional注解或其派生注解裡面的Condition接口實作類并執行個體化成對象。
    // 比如@Conditional(OnBeanCondition.class)則獲得OnBeanCondition.class,OnBeanCondition.class往往實作了Condition接口
    for (String[] conditionClasses : getConditionClasses(metadata)) {
        // 将類執行個體化成對象
        for (String conditionClass : conditionClasses) {
            Condition condition = getCondition(conditionClass, this.context.getClassLoader());
            conditions.add(condition);
        }
    }
    // 排序,即按照Condition的優先級進行排序
    AnnotationAwareOrderComparator.sort(conditions);

    for (Condition condition : conditions) {
        ConfigurationPhase requiredPhase = null;
        if (condition instanceof ConfigurationCondition) {
            // 從condition中獲得對bean是解析還是注冊
            requiredPhase = ((ConfigurationCondition) condition).getConfigurationPhase();
        }
        // 若requiredPhase為null或擷取的階段類型正是目前階段類型且不符合condition的matches條件,則跳過
        if ((requiredPhase == null || requiredPhase == phase) && !condition.matches(this.context, metadata)) {
            return true;
        }
    }

    return false;
}           

shouldSkip

這個方法執行的邏輯主要是如果是解析階段則跳過,如果是注冊階段則不跳過;如果是在注冊階段即

REGISTER_BEAN

階段的話,此時會得到所有的

Condition

接口的具體實作類并執行個體化這些實作類,然後再執行下面關鍵的代碼進行判斷是否需要跳過。

if ((requiredPhase == null || requiredPhase == phase) && !condition.matches(this.context, metadata)) {
    return true;
}           

上面代碼最重要的邏輯是調用了

Condition

接口的具體實作類的

matches

方法,若

matches

傳回

false

,則跳過,不進行注冊

bean

的操作;若

matches

true

,則不跳過,進行注冊

bean

的操作;

好了,

Condition

的源碼分析就到此為止,再往上翻調用方法的話應該就是Spring加載

bean

定義的相關源碼了,不屬于這裡的分析範圍。

3.2 Spring的内置Condition接口實作類

前面我們學會了如何自定義條件注解及

Condition

的源碼分析,那麼我們不禁好奇,Spring究竟内置了哪些

Condition

接口的實作類呢?

那麼看下Spring的

Condition

接口的具體實作類的類圖:

發現Spring内置的

Condition

接口的具體實作類雖然有多個,但隻有

ProfileCondition

不是測試相關的,是以可以說真正的内置的

Condition

接口的具體實作類隻有

ProfileCondition

一個,非常非常少,這跟SpringBoot的大量派生條件注解形成了鮮明的對比。

ProfileCondition

大家都知道,是跟環境有關,比如我們平時一般有

dev

test

prod

環境,而

ProfileCondition

就是判斷我們項目配置了哪個環境的。下面是

ProfileCondition

的源碼,很簡單,這裡就不分析了。

class ProfileCondition implements Condition {
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        MultiValueMap<String, Object> attrs = metadata.getAllAnnotationAttributes(Profile.class.getName());
        if (attrs != null) {
            for (Object value : attrs.get("value")) {
                if (context.getEnvironment().acceptsProfiles(Profiles.of((String[]) value))) {
                    return true;
                }
            }
            return false;
        }
        return true;
    }
}           

4 SpringBootCondition源碼解析

前面看到Spring對

Condition

的内置注解可以說隻有

ProfileCondition

一個,但是我們都知道,SpringBoot則内置了大量的條件注解

ConditionalOnXXX

。在分析前,我們先來看一下

SpringBootCondition

的整體類圖來個整體的了解,如下圖:

SpringBootCondition

作為SpringBoot條件注解的基類,處于整個類圖的中心,它實作了

Condition

接口,然後又有很多具體的子類

OnXXXCondition

,這些

OnXXXCondition

其實就是

@ConditionalOnXXX

的條件類。

我們先來看下

SpringBootCondition

這個父類是主要做了哪些事情,抽象了哪些共有的邏輯?

SpringBootConditon

Condition

接口,作為SpringBoot衆多條件注解

OnXXXCondtion

的父類,它的作用主要就是列印一些條件注解評估報告的日志,比如列印哪些配置類是符合條件注解的,哪些是不符合的。列印的日志形式如下圖:

因為

SpringBootConditon

Condition

接口,也實作了

matches

方法,是以該方法同樣也是被

ConditionEvaluator

shouldSkip

方法中調用,是以我們就以

SpringBootConditon

matches

方法為入口去進行分析。直接上代碼:

// SpringBootCondition.java

public final boolean matches(ConditionContext context,
            AnnotatedTypeMetadata metadata) {
        // 得到metadata的類名或方法名
        String classOrMethodName = getClassOrMethodName(metadata);
        try {
            // 判斷每個配置類的每個條件注解@ConditionalOnXXX是否滿足條件,然後記錄到ConditionOutcome結果中
            // 注意getMatchOutcome是一個抽象模闆方法,交給OnXXXCondition子類去實作
            ConditionOutcome outcome = getMatchOutcome(context, metadata);
            // 列印condition評估的日志,哪些條件注解@ConditionalOnXXX是滿足條件的,哪些是不滿足條件的,這些日志都列印出來
            logOutcome(classOrMethodName, outcome);
            // 除了列印日志外,這些是否比對的資訊還要記錄到ConditionEvaluationReport中
            recordEvaluation(context, classOrMethodName, outcome);
            // 最後傳回@ConditionalOnXXX是否滿足條件
            return outcome.isMatch();
        }
        catch (NoClassDefFoundError ex) {
            throw new IllegalStateException(
                    "Could not evaluate condition on " + classOrMethodName + " due to "
                            + ex.getMessage() + " not "
                            + "found. Make sure your own configuration does not rely on "
                            + "that class. This can also happen if you are "
                            + "@ComponentScanning a springframework package (e.g. if you "
                            + "put a @ComponentScan in the default package by mistake)",
                    ex);
        }
        catch (RuntimeException ex) {
            throw new IllegalStateException(
                    "Error processing condition on " + getName(metadata), ex);
        }
    }           

上面代碼的注釋已經非常詳細,我們知道了

SpringBootCondition

抽象了所有其具體實作類

OnXXXCondition

的共有邏輯--

condition

評估資訊列印,最重要的是封裝了一個模闆方法

getMatchOutcome(context, metadata)

,留給各個

OnXXXCondition

具體子類去覆寫實作屬于自己的判斷邏輯,然後再傳回相應的比對結果給

SpringBootCondition

用于日志列印。

是以我們知道了

SpringBootCondition

其實就是用來列印

condition

評估資訊的,對于其他枝節方法我們不必追究過深,免得丢了主線。我們現在的重點是放在交給

OnXXXCondition

子類實作的模闆方法上

getMatchOutcome(context, metadata);

,因為這個方法将會由很多

OnXXXCondition

覆寫重寫判斷邏輯,這裡是我們接下來分析的重點。

SpringBootCondition

有衆多具體實作類,下面隻挑

OnResourceCondition

OnBeanCondition

OnWebApplicationCondition

進行講解,而

AutoConfigurationImportFilter

跟自動配置有關,則留到自動配置源碼解析的時候再進行分析。

4.1 OnResourceCondition源碼分析

現在先來看下一個邏輯及其簡單的注解條件類

OnResourceCondition

OnResourceCondition

繼承了

SpringBootCondition

父類,覆寫了其

getMatchOutcome

方法,用于

@ConditionalOnResource

注解指定的資源存在與否。

OnResourceCondition

的判斷邏輯非常簡單,主要拿到

@ConditionalOnResource

注解指定的資源路徑後,然後用

ResourceLoader

根據指定路徑去加載看資源存不存在。下面直接看代碼:

先來看下

@ConditionalOnResource

的代碼,

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

    /**
     * The resources that must be present.
     * @return the resource paths that must be present.
     */
    String[] resources() default {};

}           

再來看

OnResourceCondition

的代碼:

@Order(Ordered.HIGHEST_PRECEDENCE + 20)
class OnResourceCondition extends SpringBootCondition {

    private final ResourceLoader defaultResourceLoader = new DefaultResourceLoader();

    @Override
    public ConditionOutcome getMatchOutcome(ConditionContext context,
            AnnotatedTypeMetadata metadata) {
        // 獲得@ConditionalOnResource注解的屬性中繼資料
        MultiValueMap<String, Object> attributes = metadata
                .getAllAnnotationAttributes(ConditionalOnResource.class.getName(), true);
        // 獲得資源加載器,若ConditionContext中有ResourceLoader則用ConditionContext中的,沒有則用預設的
        ResourceLoader loader = (context.getResourceLoader() != null)
                ? context.getResourceLoader() : this.defaultResourceLoader;
        List<String> locations = new ArrayList<>();
        // 将@ConditionalOnResource中定義的resources屬性值取出來裝進locations集合
        collectValues(locations, attributes.get("resources"));
        Assert.isTrue(!locations.isEmpty(),
                "@ConditionalOnResource annotations must specify at "
                        + "least one resource location");
        // missing集合是裝不存在指定資源的資源路徑的
        List<String> missing = new ArrayList<>();
        // 周遊所有的資源路徑,若指定的路徑的資源不存在則将其資源路徑存進missing集合中
        for (String location : locations) {
            // 這裡針對有些資源路徑是Placeholders的情況,即處理${}
            String resource = context.getEnvironment().resolvePlaceholders(location);
            if (!loader.getResource(resource).exists()) {
                missing.add(location);
            }
        }
        // 如果存在某個資源不存在,那麼則報錯
        if (!missing.isEmpty()) {
            return ConditionOutcome.noMatch(ConditionMessage
                    .forCondition(ConditionalOnResource.class)
                    .didNotFind("resource", "resources").items(Style.QUOTE, missing));
        }
        // 所有資源都存在,那麼則傳回能找到就提的資源
        return ConditionOutcome
                .match(ConditionMessage.forCondition(ConditionalOnResource.class)
                        .found("location", "locations").items(locations));
    }
    
    // 将@ConditionalOnResource中定義的resources屬性值取出來裝進locations集合
    private void collectValues(List<String> names, List<Object> values) {
        for (Object value : values) {
            for (Object item : (Object[]) value) {
                names.add((String) item);
            }
        }
    }
}
           

OnResourceCondition

getMatchOutcome

方法非常簡單,這裡不再詳述。

4.2 OnBeanCondition源碼分析

OnBeanCondition

同樣繼承了

FilteringSpringBootCondition

父類,覆寫了父類

FilteringSpringBootCondition

getOutcomes

方法。而

FilteringSpringBootCondition

又是

SpringBootCondition

的子類,

FilteringSpringBootCondition

跟自動配置類過濾有關,這裡先不分析。值得注意的是

OnBeanCondition

同樣重寫了

SpringBootCondition

getMatchOutcome

方法,用來判斷Spring容器中是否存在指定條件的

bean

。同時是

OnBeanCondition

@ConditionalOnBean

@ConditionalOnSingleCandidate

ConditionalOnMissingBean

同樣,先來看

OnBeanCondition

複寫父類

SpringBootCondition

getMatchOutcome

方法的代碼:

@Override
    public ConditionOutcome getMatchOutcome(ConditionContext context,
            AnnotatedTypeMetadata metadata) {
        ConditionMessage matchMessage = ConditionMessage.empty();
        // (1),配置類(metadata)标注@ConditionalOnBean注解的情況
        if (metadata.isAnnotated(ConditionalOnBean.class.getName())) {
            // 将@ConditionalOnBean注解屬性封裝進BeanSearchSpec對象中
            // 注意BeanSearchSpec是一個靜态内部類,用來存儲@ConditionalOnBean和@ConditionalOnMissingBean注解的屬性值
            BeanSearchSpec spec = new BeanSearchSpec(context, metadata,
                    ConditionalOnBean.class);
            // 調用getMatchingBeans得到符合條件的bean
            MatchResult matchResult = getMatchingBeans(context, spec);
            // 如果不比對
            if (!matchResult.isAllMatched()) {
                String reason = createOnBeanNoMatchReason(matchResult);
                return ConditionOutcome.noMatch(ConditionMessage
                        .forCondition(ConditionalOnBean.class, spec).because(reason));
            }
            // 如果比對
            matchMessage = matchMessage.andCondition(ConditionalOnBean.class, spec)
                    .found("bean", "beans")
                    .items(Style.QUOTE, matchResult.getNamesOfAllMatches());
        }
        // (2),配置類(metadata)标注@ConditionalOnSingleCandidate注解的情況
        if (metadata.isAnnotated(ConditionalOnSingleCandidate.class.getName())) {
            BeanSearchSpec spec = new SingleCandidateBeanSearchSpec(context, metadata,
                    ConditionalOnSingleCandidate.class);
            MatchResult matchResult = getMatchingBeans(context, spec);
            if (!matchResult.isAllMatched()) {
                return ConditionOutcome.noMatch(ConditionMessage
                        .forCondition(ConditionalOnSingleCandidate.class, spec)
                        .didNotFind("any beans").atAll());
            }
            else if (!hasSingleAutowireCandidate(context.getBeanFactory(),
                    matchResult.getNamesOfAllMatches(),
                    spec.getStrategy() == SearchStrategy.ALL)) {
                return ConditionOutcome.noMatch(ConditionMessage
                        .forCondition(ConditionalOnSingleCandidate.class, spec)
                        .didNotFind("a primary bean from beans")
                        .items(Style.QUOTE, matchResult.getNamesOfAllMatches()));
            }
            matchMessage = matchMessage
                    .andCondition(ConditionalOnSingleCandidate.class, spec)
                    .found("a primary bean from beans")
                    .items(Style.QUOTE, matchResult.getNamesOfAllMatches());
        }
        // (3),配置類(metadata)标注@ConditionalOnMissingBean注解的情況
        if (metadata.isAnnotated(ConditionalOnMissingBean.class.getName())) {
            BeanSearchSpec spec = new BeanSearchSpec(context, metadata,
                    ConditionalOnMissingBean.class);
            MatchResult matchResult = getMatchingBeans(context, spec);
            if (matchResult.isAnyMatched()) {
                String reason = createOnMissingBeanNoMatchReason(matchResult);
                return ConditionOutcome.noMatch(ConditionMessage
                        .forCondition(ConditionalOnMissingBean.class, spec)
                        .because(reason));
            }
            matchMessage = matchMessage.andCondition(ConditionalOnMissingBean.class, spec)
                    .didNotFind("any beans").atAll();
        }
        // 最終傳回matchMessage
        return ConditionOutcome.match(matchMessage);
    }           

我們可以看到

OnBeanCondition

類覆寫的

getMatchOutcome

方法分别處理了标注

@ConditionalOnBean

@ConditionalOnSingleCandidate

@ConditionalOnMissingBean

注解的情況,分别對應上面代碼注釋的

(1)

(2)

(3)

處。

現在我們隻看針對

@ConditionalOnBean

注解的處理邏輯,從上面代碼中可以看到若配置類(metadata)标注

@ConditionalOnBean

注解的話,主要做了以下事情:

  1. 将該注解屬性提取出來封裝進

    BeanSearchSpec

    對象中;
  2. 然後調用

    getMatchingBeans(context, spec)

    方法來擷取是否有比對的

    bean

  3. 最後傳回

    bean

    的比對情況;

可以看到最重要的邏輯是第2步,那麼我們再來看下

getMatchingBeans

方法,直接上代碼:

protected final MatchResult getMatchingBeans(ConditionContext context,
            BeanSearchSpec beans) {
        // 獲得Spring容器的beanFactory
        ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
        // 判斷bean的搜尋政策是否是SearchStrategy.ANCESTORS政策
        if (beans.getStrategy() == SearchStrategy.ANCESTORS) {
            BeanFactory parent = beanFactory.getParentBeanFactory();
            Assert.isInstanceOf(ConfigurableListableBeanFactory.class, parent,
                    "Unable to use SearchStrategy.PARENTS");
            beanFactory = (ConfigurableListableBeanFactory) parent;
        }
        // MatchResult用來存儲bean的比對結果
        MatchResult matchResult = new MatchResult();
        // 如果bean的搜尋政策不是SearchStrategy.CURRENT的話,則置considerHierarchy為true
        boolean considerHierarchy = beans.getStrategy() != SearchStrategy.CURRENT;
        // 擷取TypeExtractor,TypeExtractor是用來判斷bean的類型的
        TypeExtractor typeExtractor = beans.getTypeExtractor(context.getClassLoader());
        // 擷取是否有被忽略bean類型,若有的話将該bean類型的名稱裝進beansIgnoredByType集合
        // 這裡主要是針對@ConditionalOnMissingBean的ignored屬性
        List<String> beansIgnoredByType = getNamesOfBeansIgnoredByType(
                beans.getIgnoredTypes(), typeExtractor, beanFactory, context,
                considerHierarchy);
        // 周遊bean的所有類型
        for (String type : beans.getTypes()) {
            // 調用getBeanNamesForType方法根據bean類型得到所有符合條件的bean類型,并放到typeMatches集合
            Collection<String> typeMatches = getBeanNamesForType(beanFactory, type,
                    typeExtractor, context.getClassLoader(), considerHierarchy);
            // 移除掉Ignored的類型
            typeMatches.removeAll(beansIgnoredByType);
            // 若typeMatches為空,那麼則說明正在周遊的這個type類型不符合比對條件,此時用matchResult記錄一下這個不符合條件的類型
            if (typeMatches.isEmpty()) {
                matchResult.recordUnmatchedType(type);
            }
            // 若typeMatches不為空,那麼則說明正在周遊的這個type類型符合比對條件,此時用matchResult記錄一下這個符合條件的類型
            else {
                matchResult.recordMatchedType(type, typeMatches);
            }
        }
        // 這裡針對@ConditionalOnBean等注解的annotation屬性的處理
        for (String annotation : beans.getAnnotations()) {
            List<String> annotationMatches = Arrays
                    .asList(getBeanNamesForAnnotation(beanFactory, annotation,
                            context.getClassLoader(), considerHierarchy));
            annotationMatches.removeAll(beansIgnoredByType);
            if (annotationMatches.isEmpty()) {
                matchResult.recordUnmatchedAnnotation(annotation);
            }
            else {
                matchResult.recordMatchedAnnotation(annotation, annotationMatches);
            }
        }
        // 這裡針對@ConditionalOnBean等注解的name屬性的處理
        for (String beanName : beans.getNames()) {
            // beansIgnoredByType集合不包含beanName且beanFactory包含這個bean,則比對
            if (!beansIgnoredByType.contains(beanName)
                    && containsBean(beanFactory, beanName, considerHierarchy)) {
                matchResult.recordMatchedName(beanName);
            }
            // 否則,不比對
            else {
                matchResult.recordUnmatchedName(beanName);
            }
        }
        // 最後傳回比對結果
        return matchResult;
    }           

上面的邏輯主要是從spring容器中搜尋有無指定條件的

bean

,搜尋Spring容器搜尋bean的話有三種搜尋政策,分别是

CURRENT

ANCESTORS

ALL

,分表表示隻從目前的

context

中搜尋

bean

,隻從父

context

bean

和從整個

context

bean

;定義了搜尋政策後,然後再根據

BeanSearchSpec

對象封裝的注解屬性分别取指定的容器中查找有無符合條件的

bean

,然後再進行一些過濾。比如

@ConditionalOnMissingBean

注解有定義

ignored

屬性值,那麼從容器中搜尋到有符合條件的

bean

時,此時還要移除掉

ignored

指定的

bean

好了,上面就已經分析了

OnBeanCondition

這個條件類了,我們堅持主線優先的原則,具體的細節代碼不會深究。

4.3 OnWebApplicationCondition

OnWebApplicationCondition

FilteringSpringBootCondition

FilteringSpringBootCondition

getOutcomes

FilteringSpringBootCondition

SpringBootCondition

FilteringSpringBootCondition

OnWebApplicationCondition

SpringBootCondition

getMatchOutcome

方法,用來判斷目前應用是否web應用。同時是

OnWebApplicationCondition

@ConditionalOnWebApplication

OnWebApplicationCondition

重寫

SpringBootCondition

getMatchOutcome

方法:

public ConditionOutcome getMatchOutcome(ConditionContext context,
            AnnotatedTypeMetadata metadata) {
        // 配置類是否标注有@ConditionalOnWebApplication注解
        boolean required = metadata
                .isAnnotated(ConditionalOnWebApplication.class.getName());
        // 調用isWebApplication方法傳回比對結果
        ConditionOutcome outcome = isWebApplication(context, metadata, required);
        // 若有标注@ConditionalOnWebApplication但不符合條件,則傳回不比對
        if (required && !outcome.isMatch()) {
            return ConditionOutcome.noMatch(outcome.getConditionMessage());
        }
        // 若沒有标注@ConditionalOnWebApplication但符合條件,則傳回不比對
        if (!required && outcome.isMatch()) {
            return ConditionOutcome.noMatch(outcome.getConditionMessage());
        }
        // 這裡傳回比對的情況,TODO 不過有個疑問:如果沒有标注@ConditionalOnWebApplication注解,又不符合條件的話,也會執行到這裡,傳回比對?
        return ConditionOutcome.match(outcome.getConditionMessage());
    }
           

上面代碼的邏輯很簡單,主要是調用

isWebApplication

方法來判斷目前應用是否是web應用。是以,我們再來看下

isWebApplication

方法:

private ConditionOutcome isWebApplication(ConditionContext context,
            AnnotatedTypeMetadata metadata, boolean required) {
        // 調用deduceType方法判斷是哪種類型,其中有SERVLET,REACTIVE和ANY類型,其中ANY表示了SERVLET或REACTIVE類型
        switch (deduceType(metadata)) {
        // SERVLET類型
        case SERVLET:
            return isServletWebApplication(context);
        // REACTIVE類型
        case REACTIVE:
            return isReactiveWebApplication(context);
        default:
            return isAnyWebApplication(context, required);
        }
    }           

isWebApplication

方法中,首先從

@ConditionalOnWebApplication

注解中擷取其定義了什麼類型,然後根據不同的類型進入不同的判斷邏輯。這裡我們隻看下

SERVLET

的情況判斷處理,看代碼:

private ConditionOutcome isServletWebApplication(ConditionContext context) {
        ConditionMessage.Builder message = ConditionMessage.forCondition("");
        // 若classpath中不存在org.springframework.web.context.support.GenericWebApplicationContext.class,則傳回不比對
        if (!ClassNameFilter.isPresent(SERVLET_WEB_APPLICATION_CLASS,
                context.getClassLoader())) {
            return ConditionOutcome.noMatch(
                    message.didNotFind("servlet web application classes").atAll());
        }
        // 若classpath中存在org.springframework.web.context.support.GenericWebApplicationContext.class,那麼又分為以下幾種比對的情況
        // session
        if (context.getBeanFactory() != null) {
            String[] scopes = context.getBeanFactory().getRegisteredScopeNames();
            if (ObjectUtils.containsElement(scopes, "session")) {
                return ConditionOutcome.match(message.foundExactly("'session' scope"));
            }
        }
        // ConfigurableWebEnvironment
        if (context.getEnvironment() instanceof ConfigurableWebEnvironment) {
            return ConditionOutcome
                    .match(message.foundExactly("ConfigurableWebEnvironment"));
        }
        // WebApplicationContext
        if (context.getResourceLoader() instanceof WebApplicationContext) {
            return ConditionOutcome.match(message.foundExactly("WebApplicationContext"));
        }
        // 若以上三種都不比對的話,則說明不是一個servlet web application
        return ConditionOutcome.noMatch(message.because("not a servlet web application"));
    }           

對于是

SERVLET

的情況,首先根據

classpath

中是否存在

org.springframework.web.context.support.GenericWebApplicationContext.class

,如果不存在該類,則直接傳回不比對;若存在的話那麼又分為以下幾種比對的情況:

  • session
  • ConfigurableWebEnvironment
  • WebApplicationContext

若上面三種情況都不比對,則說明不是一個servlet web application。

4.4 其他

由于springboot的

OnXXXCondition

類實作太多,不可能每個條件類都分析一遍,是以上面隻分析了

OnResourceCondition

OnBeanCondition

onWebApplicationCondition

的源碼。我們分析源碼不可能把所有代碼都通讀一遍的,閱讀源碼的話,隻要了解了某個子產品的類之間的關系及挑幾個有代表性的類分析下就行,不可能一網打盡。

若有時間的話,推薦看下幾個我們常用的條件類的源碼:

OnPropertyCondition

OnClassCondition

OnExpressionCondition

等。

5 如何擴充SpringBootCondition

前文我們知道了如何擴充Spring的

Condition

接口,那麼我們該如何擴充SpringBoot的

SpringBootCondition

類呢?

推薦閱讀

springboot之使用SpringBootCondition

獲得答案

好了,本篇文章是SpringBoot自動配置源碼分析的前置文章,這裡分析了條件注解源碼,那麼下篇文章我們就來看看SpringBoot自動配置的源碼了。

下節預告:

SpringBoot新特性:SpringBoot是如何自動配置的?--SpringBoot源碼(四)

原創不易,幫忙點個贊呗!

參考:

1,

spring 自動配置(上) 配置檔案和插件解讀

2,

SpringBoot内置條件注解

3,

spring boot 系列之六:深入了解spring boot的自動配置