SpringBoot是如何實現自動配置的? SpringBoot源碼(四)

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

1 前言

本篇接 助力SpringBoot自動配置的條件註解ConditionalOnXXX分析--SpringBoot源碼(三)

溫故而知新,我們來簡單回顧一下上篇的內容,上一篇我們分析了SpringBoot的條件註解@ConditionalOnXxx的相關源碼,現挑重點總結如下:

  1. SpringBoot的所有@ConditionalOnXxx的條件類OnXxxCondition都是繼承於SpringBootCondition基類,而SpringBootCondition又實現了Condition接口。
  2. SpringBootCondition基類主要用來打印一些條件註解評估報告的日誌,這些條件評估信息全部來源於其子類註解條件類OnXxxCondition,因此其也抽象了一個模板方法getMatchOutcome留給子類去實現來評估其條件註解是否符合條件。
  3. 前一篇我們也還有一個重要的知識點還沒分析,那就是跟過濾自動配置類邏輯有關的AutoConfigurationImportFilter接口,這篇文章我們來填一下這個坑。

前面我們分析了跟SpringBoot的自動配置息息相關內置條件註解@ConditionalOnXxx後,現在我們就開始來擼SpringBoot自動配置的相關源碼了。

2 @SpringBootApplication註解

在開始前,我們先想一下,SpringBoot為何一個標註有@SpringBootApplication註解的啟動類通過執行一個簡單的run方法就能實現SpringBoot大量Starter的自動配置呢? 其實SpringBoot的自動配置就跟@SpringBootApplication這個註解有關,我們先來看下其這個註解的源碼:

<code>@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = {
\t\t@Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
\t\t@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
// ...省略非關鍵代碼
}
/<code>

@SpringBootApplication標註了很多註解,我們可以看到其中跟SpringBoot自動配置有關的註解就有一個即@EnableAutoConfiguration,因此,可以肯定的是SpringBoot的自動配置肯定跟@EnableAutoConfiguration息息相關(其中@ComponentScan註解的excludeFilters屬性也有一個類AutoConfigurationExcludeFilter,這個類跟自動配置也有點關係,但不是我們關注的重點)。 現在我們來打開@EnableAutoConfiguration註解的源碼:

<code>@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
\tString ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
\tClass>[] exclude() default {};
\tString[] excludeName() default {};
}
/<code>

看到@EnableAutoConfiguration註解又標有@AutoConfigurationPackage和@Import(AutoConfigurationImportSelector.class)兩個註解,顧名思義,@AutoConfigurationPackage註解肯定跟自動配置的包有關,而AutoConfigurationImportSelector則是跟SpringBoot的自動配置選擇導入有關(Spring中的ImportSelector是用來導入配置類的,通常是基於某些條件註解@ConditionalOnXxxx來決定是否導入某個配置類)。

因此,可以看出AutoConfigurationImportSelector類是我們本篇的重點,因為SpringBoot的自動配置肯定有一個配置類,而這個配置類的導入則需要靠AutoConfigurationImportSelector這個哥們來實現。

接下來我們重點來看AutoConfigurationImportSelector這個類,完了我們再簡單分析下@AutoConfigurationPackage這個註解的邏輯。

3 如何去找SpringBoot自動配置實現邏輯的入口方法?

可以肯定的是SpringBoot的自動配置的邏輯肯定與AutoConfigurationImportSelector這個類有關,那麼我們該如何去找到SpringBoot自動配置實現邏輯的入口方法呢?

在找SpringBoot自動配置實現邏輯的入口方法前,我們先來看下AutoConfigurationImportSelector的相關類圖,好有個整體的理解。看下圖:


SpringBoot是如何實現自動配置的? SpringBoot源碼(四)


圖1

可以看到AutoConfigurationImportSelector重點是實現了DeferredImportSelector接口和各種Aware接口,然後DeferredImportSelector接口又繼承了ImportSelector接口。

自然而然的,我們會去關注AutoConfigurationImportSelector複寫DeferredImportSelector接口的實現方法selectImports方法,因為selectImports方法跟導入自動配置類有關,而這個方法往往是程序執行的入口方法。經過調試發現selectImports方法很具有迷惑性,selectImports方法跟自動配置相關的邏輯有點關係,但實質關係不大。

此時劇情的發展好像不太符合常理,此時我們又該如何來找到自動配置邏輯有關的入口方法呢?

最簡單的方法就是在AutoConfigurationImportSelector類的每個方法都打上斷點,然後調試看先執行到哪個方法。但是我們可以不這麼做,我們回想下,自定義一個Starter的時候我們是不是要在spring.factories配置文件中配置

<code>EnableAutoConfiguration=XxxAutoConfiguration
/<code>

因此可以推斷,SpringBoot的自動配置原理肯定跟從spring.factories配置文件中加載自動配置類有關,於是結合AutoConfigurationImportSelector的方法註釋,我們找到了getAutoConfigurationEntry方法。於是我們在這個方法裡面打上一個斷點,此時通過調用棧幀來看下更上層的入口方法在哪裡,然後我們再從跟自動配置相關的更上層的入口方法開始分析。


SpringBoot是如何實現自動配置的? SpringBoot源碼(四)


圖2

通過圖1我們可以看到,跟自動配置邏輯相關的入口方法在DeferredImportSelectorGrouping類的getImports方法處,因此我們就從DeferredImportSelectorGrouping類的getImports方法來開始分析SpringBoot的自動配置源碼好了。

4 分析SpringBoot自動配置原理

既然找到ConfigurationClassParser.getImports()方法是自動配置相關的入口方法,那麼下面我們就來真正分析SpringBoot自動配置的源碼了。

先看一下getImports方法代碼:

<code>// ConfigurationClassParser.java

public Iterable<group.entry> getImports() {
// 遍歷DeferredImportSelectorHolder對象集合deferredImports,deferredImports集合裝了各種ImportSelector,當然這裡裝的是AutoConfigurationImportSelector
for (DeferredImportSelectorHolder deferredImport : this.deferredImports) {
\t// 【1】,利用AutoConfigurationGroup的process方法來處理自動配置的相關邏輯,決定導入哪些配置類(這個是我們分析的重點,自動配置的邏輯全在這了)
\tthis.group.process(deferredImport.getConfigurationClass().getMetadata(),
\t\t\tdeferredImport.getImportSelector());
}
// 【2】,經過上面的處理後,然後再進行選擇導入哪些配置類
return this.group.selectImports();
}
/<group.entry>/<code>

標【1】處的的代碼是我們分析的重中之重,自動配置的相關的絕大部分邏輯全在這裡了,將在4.1 分析自動配置的主要邏輯深入分析。那麼this.group.process(deferredImport.getConfigurationClass().getMetadata(), deferredImport.getImportSelector());主要做的事情就是在this.group即AutoConfigurationGroup對象的process方法中,傳入的AutoConfigurationImportSelector對象來選擇一些符合條件的自動配置類,過濾掉一些不符合條件的自動配置類,就是這麼個事情,無他。

注:

  1. AutoConfigurationGroup:是AutoConfigurationImportSelector的內部類,主要用來處理自動配置相關的邏輯,擁有process和selectImports方法,然後擁有entries和autoConfigurationEntries集合屬性,這兩個集合分別存儲被處理後的符合條件的自動配置類,我們知道這些就足夠了;
  2. AutoConfigurationImportSelector:承擔自動配置的絕大部分邏輯,負責選擇一些符合條件的自動配置類;
  3. metadata:標註在SpringBoot啟動類上的@SpringBootApplication註解元數據

標【2】的this.group.selectImports的方法主要是針對前面的process方法處理後的自動配置類再進一步有選擇的選擇導入,將在4.2 有選擇的導入自動配置類這小節深入分析。

4.1 分析自動配置的主要邏輯

這裡繼續深究前面 4 分析SpringBoot自動配置原理這節標【1】處的 this.group.process方法是如何處理自動配置相關邏輯的。

<code>// AutoConfigurationImportSelector$AutoConfigurationGroup.java

// 這裡用來處理自動配置類,比如過濾掉不符合匹配條件的自動配置類
public void process(AnnotationMetadata annotationMetadata,
\t\tDeferredImportSelector deferredImportSelector) {
\tAssert.state(
\t\t\tdeferredImportSelector instanceof AutoConfigurationImportSelector,
\t\t\t() -> String.format("Only %s implementations are supported, got %s",
\t\t\t\t\tAutoConfigurationImportSelector.class.getSimpleName(),
\t\t\t\t\tdeferredImportSelector.getClass().getName()));
\t// 【1】,調用getAutoConfigurationEntry方法得到自動配置類放入autoConfigurationEntry對象中
\tAutoConfigurationEntry autoConfigurationEntry = ((AutoConfigurationImportSelector) deferredImportSelector)
\t\t\t.getAutoConfigurationEntry(getAutoConfigurationMetadata(),
\t\t\t\t\tannotationMetadata);
\t// 【2】,又將封裝了自動配置類的autoConfigurationEntry對象裝進autoConfigurationEntries集合
\tthis.autoConfigurationEntries.add(autoConfigurationEntry);
\t// 【3】,遍歷剛獲取的自動配置類
\tfor (String importClassName : autoConfigurationEntry.getConfigurations()) {
\t\t// 這裡符合條件的自動配置類作為key,annotationMetadata作為值放進entries集合
\t\tthis.entries.putIfAbsent(importClassName, annotationMetadata);
\t}
}
/<code>

上面代碼中我們再來看標【1】的方法getAutoConfigurationEntry,這個方法主要是用來獲取自動配置類有關,承擔了自動配置的主要邏輯。直接上代碼:

<code>// AutoConfigurationImportSelector.java

// 獲取符合條件的自動配置類,避免加載不必要的自動配置類從而造成內存浪費
protected AutoConfigurationEntry getAutoConfigurationEntry(
\t\tAutoConfigurationMetadata autoConfigurationMetadata,

\t\tAnnotationMetadata annotationMetadata) {
\t// 獲取是否有配置spring.boot.enableautoconfiguration屬性,默認返回true
\tif (!isEnabled(annotationMetadata)) {
\t\treturn EMPTY_ENTRY;
\t}
\t// 獲得@Congiguration標註的Configuration類即被審視introspectedClass的註解數據,
\t// 比如:@SpringBootApplication(exclude = FreeMarkerAutoConfiguration.class)
\t// 將會獲取到exclude = FreeMarkerAutoConfiguration.class和excludeName=""的註解數據
\tAnnotationAttributes attributes = getAttributes(annotationMetadata);
\t// 【1】得到spring.factories文件配置的所有自動配置類
\tList<string> configurations = getCandidateConfigurations(annotationMetadata,
\t\t\tattributes);
\t// 利用LinkedHashSet移除重複的配置類
\tconfigurations = removeDuplicates(configurations);
\t// 得到要排除的自動配置類,比如註解屬性exclude的配置類
\t// 比如:@SpringBootApplication(exclude = FreeMarkerAutoConfiguration.class)
\t// 將會獲取到exclude = FreeMarkerAutoConfiguration.class的註解數據
\tSet<string> exclusions = getExclusions(annotationMetadata, attributes);
\t// 檢查要被排除的配置類,因為有些不是自動配置類,故要拋出異常
\tcheckExcludedClasses(configurations, exclusions);
\t// 【2】將要排除的配置類移除
\tconfigurations.removeAll(exclusions);
\t// 【3】因為從spring.factories文件獲取的自動配置類太多,如果有些不必要的自動配置類都加載進內存,會造成內存浪費,因此這裡需要進行過濾
\t// 注意這裡會調用AutoConfigurationImportFilter的match方法來判斷是否符合@ConditionalOnBean,@ConditionalOnClass或@ConditionalOnWebApplication,後面會重點分析一下
\tconfigurations = filter(configurations, autoConfigurationMetadata);
\t// 【4】獲取了符合條件的自動配置類後,此時觸發AutoConfigurationImportEvent事件,

\t// 目的是告訴ConditionEvaluationReport條件評估報告器對象來記錄符合條件的自動配置類
\t// 該事件什麼時候會被觸發?--> 在刷新容器時調用invokeBeanFactoryPostProcessors後置處理器時觸發
\tfireAutoConfigurationImportEvents(configurations, exclusions);
\t// 【5】將符合條件和要排除的自動配置類封裝進AutoConfigurationEntry對象,並返回
\treturn new AutoConfigurationEntry(configurations, exclusions);
}
/<string>/<string>/<code>

AutoConfigurationEntry方法主要做的事情就是獲取符合條件的自動配置類,避免加載不必要的自動配置類從而造成內存浪費。我們下面總結下AutoConfigurationEntry方法主要做的事情:

【1】從spring.factories配置文件中加載EnableAutoConfiguration自動配置類,獲取的自動配置類如圖3所示。這裡我們知道該方法做了什麼事情就行了,後面還會有一篇文章詳述spring.factories的原理;

【2】若@EnableAutoConfiguration等註解標有要exclude的自動配置類,那麼再將這個自動配置類排除掉;

【3】排除掉要exclude的自動配置類後,然後再調用filter方法進行進一步的過濾,再次排除一些不符合條件的自動配置類;這個在稍後會詳細分析。

【4】經過重重過濾後,此時再觸發AutoConfigurationImportEvent事件,告訴ConditionEvaluationReport條件評估報告器對象來記錄符合條件的自動配置類;(這個在6 AutoConfigurationImportListener這小節詳細分析。)

【5】 最後再將符合條件的自動配置類返回。


SpringBoot是如何實現自動配置的? SpringBoot源碼(四)


圖3

總結了AutoConfigurationEntry方法主要的邏輯後,我們再來細看一下AutoConfigurationImportSelector的filter方法:

<code>// AutoConfigurationImportSelector.java

private List<string> filter(List<string> configurations,

\t\t\tAutoConfigurationMetadata autoConfigurationMetadata) {
\tlong startTime = System.nanoTime();
\t// 將從spring.factories中獲取的自動配置類轉出字符串數組
\tString[] candidates = StringUtils.toStringArray(configurations);
\t// 定義skip數組,是否需要跳過。注意skip數組與candidates數組順序一一對應
\tboolean[] skip = new boolean[candidates.length];
\tboolean skipped = false;
\t// getAutoConfigurationImportFilters方法:拿到OnBeanCondition,OnClassCondition和OnWebApplicationCondition
\t// 然後遍歷這三個條件類去過濾從spring.factories加載的大量配置類
\tfor (AutoConfigurationImportFilter filter : getAutoConfigurationImportFilters()) {
\t\t// 調用各種aware方法,將beanClassLoader,beanFactory等注入到filter對象中,
\t\t// 這裡的filter對象即OnBeanCondition,OnClassCondition或OnWebApplicationCondition
\t\tinvokeAwareMethods(filter);
\t\t// 判斷各種filter來判斷每個candidate(這裡實質要通過candidate(自動配置類)拿到其標註的
\t\t// @ConditionalOnClass,@ConditionalOnBean和@ConditionalOnWebApplication裡面的註解值)是否匹配,
\t\t// 注意candidates數組與match數組一一對應
\t\t/**********************【主線,重點關注】********************************/
\t\tboolean[] match = filter.match(candidates, autoConfigurationMetadata);
\t\t// 遍歷match數組,注意match順序跟candidates的自動配置類一一對應
\t\tfor (int i = 0; i < match.length; i++) {
\t\t\t// 若有不匹配的話
\t\t\tif (!match[i]) {
\t\t\t\t// 不匹配的將記錄在skip數組,標誌skip[i]為true,也與candidates數組一一對應
\t\t\t\tskip[i] = true;
\t\t\t\t// 因為不匹配,將相應的自動配置類置空
\t\t\t\tcandidates[i] = null;
\t\t\t\t// 標註skipped為true
\t\t\t\tskipped = true;

\t\t\t}
\t\t}
\t}
\t// 這裡表示若所有自動配置類經過OnBeanCondition,OnClassCondition和OnWebApplicationCondition過濾後,全部都匹配的話,則全部原樣返回
\tif (!skipped) {
\t\treturn configurations;
\t}
\t// 建立result集合來裝匹配的自動配置類
\tList<string> result = new ArrayList<>(candidates.length);
\tfor (int i = 0; i < candidates.length; i++) {
\t\t// 若skip[i]為false,則說明是符合條件的自動配置類,此時添加到result集合中
\t\tif (!skip[i]) {
\t\t\tresult.add(candidates[i]);
\t\t}
\t}
\t// 打印日誌
\tif (logger.isTraceEnabled()) {
\t\tint numberFiltered = configurations.size() - result.size();
\t\tlogger.trace("Filtered " + numberFiltered + " auto configuration class in "
\t\t\t\t+ TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime)
\t\t\t\t+ " ms");
\t}
\t// 最後返回符合條件的自動配置類
\treturn new ArrayList<>(result);
}
/<string>/<string>/<string>/<code>

AutoConfigurationImportSelector的filter方法主要做的事情就是調用AutoConfigurationImportFilter接口的match方法來判斷每一個自動配置類上的條件註解(若有的話)@ConditionalOnClass,@ConditionalOnBean或@ConditionalOnWebApplication是否滿足條件,若滿足,則返回true,說明匹配,若不滿足,則返回false說明不匹配。

我們現在知道AutoConfigurationImportSelector的filter方法主要做了什麼事情就行了,現在先不用研究的過深,至於AutoConfigurationImportFilter接口的match方法將在5 AutoConfigurationImportFilter這小節再詳細分析,填補一下我們前一篇條件註解源碼分析中留下的坑。

注意:我們堅持主線優先的原則,其他枝節代碼這裡不深究,以免丟了主線哈。

4.2 有選擇的導入自動配置類

這裡繼續深究前面 4 分析SpringBoot自動配置原理這節標【2】處的 this.group.selectImports方法是如何進一步有選擇的導入自動配置類的。直接看代碼:

<code>// AutoConfigurationImportSelector$AutoConfigurationGroup.java

public Iterable<entry> selectImports() {
\tif (this.autoConfigurationEntries.isEmpty()) {
\t\treturn Collections.emptyList();
\t}
\t// 這裡得到所有要排除的自動配置類的set集合
\tSet<string> allExclusions = this.autoConfigurationEntries.stream()
\t\t\t.map(AutoConfigurationEntry::getExclusions)
\t\t\t.flatMap(Collection::stream).collect(Collectors.toSet());
\t// 這裡得到經過濾後所有符合條件的自動配置類的set集合
\tSet<string> processedConfigurations = this.autoConfigurationEntries.stream()
\t\t\t.map(AutoConfigurationEntry::getConfigurations)
\t\t\t.flatMap(Collection::stream)
\t\t\t.collect(Collectors.toCollection(LinkedHashSet::new));
\t// 移除掉要排除的自動配置類
\tprocessedConfigurations.removeAll(allExclusions);
\t// 對標註有@Order註解的自動配置類進行排序,
\treturn sortAutoConfigurations(processedConfigurations,
\t\t\tgetAutoConfigurationMetadata())
\t\t\t\t\t.stream()
\t\t\t\t\t.map((importClassName) -> new Entry(
\t\t\t\t\t\t\tthis.entries.get(importClassName), importClassName))
\t\t\t\t\t.collect(Collectors.toList());
}
/<string>/<string>/<entry>/<code>

可以看到,selectImports方法主要是針對經過排除掉exclude的和被AutoConfigurationImportFilter接口過濾後的滿足條件的自動配置類再進一步排除exclude的自動配置類,然後再排序。邏輯很簡單,不再詳述。

不過有個疑問,前面已經exclude過一次了,為何這裡還要再exclude一次?

5 AutoConfigurationImportFilter

這裡繼續深究前面 4.1節的 AutoConfigurationImportSelector.filter方法的過濾自動配置類的boolean[] match = filter.match(candidates, autoConfigurationMetadata);這句代碼。

因此我們繼續分析AutoConfigurationImportFilter接口,分析其match方法,同時也是對前一篇@ConditionalOnXxx的源碼分析文章中留下的坑進行填補。

AutoConfigurationImportFilter接口只有一個match方法用來過濾不符合條件的自動配置類。

<code>@FunctionalInterface
public interface AutoConfigurationImportFilter {
boolean[] match(String[] autoConfigurationClasses,
\t\tAutoConfigurationMetadata autoConfigurationMetadata);
}
/<code>

同樣,在分析AutoConfigurationImportFilter接口的match方法前,我們先來看下其類關係圖:


SpringBoot是如何實現自動配置的? SpringBoot源碼(四)


圖4

可以看到,AutoConfigurationImportFilter接口有一個具體的實現類FilteringSpringBootCondition,FilteringSpringBootCondition又有三個具體的子類:OnClassCondition,OnBeanCondtition和OnWebApplicationCondition。

那麼這幾個類之間的關係是怎樣的呢?

FilteringSpringBootCondition實現了AutoConfigurationImportFilter接口的match方法,然後在FilteringSpringBootCondition的match方法調用getOutcomes這個抽象模板方法返回自動配置類的匹配與否的信息。同時,最重要的是FilteringSpringBootCondition的三個子類OnClassCondition,OnBeanCondtition和OnWebApplicationCondition將會複寫這個模板方法實現自己的匹配判斷邏輯。

好了,AutoConfigurationImportFilter接口的整體關係已經清楚了,現在我們再進入其具體實現類FilteringSpringBootCondition的match方法看看是其如何根據條件過濾自動配置類的。

<code>// FilteringSpringBootCondition.java

@Override
public boolean[] match(String[] autoConfigurationClasses,
\t\tAutoConfigurationMetadata autoConfigurationMetadata) {
\t// 創建評估報告
\tConditionEvaluationReport report = ConditionEvaluationReport
\t\t\t.find(this.beanFactory);
\t// 注意getOutcomes是模板方法,將spring.factories文件種加載的所有自動配置類傳入
\t// 子類(這裡指的是OnClassCondition,OnBeanCondition和OnWebApplicationCondition類)去過濾
\t// 注意outcomes數組存儲的是不匹配的結果,跟autoConfigurationClasses數組一一對應

\t/*****************************【主線,重點關注】*********************************************/
\tConditionOutcome[] outcomes = getOutcomes(autoConfigurationClasses,
\t\t\tautoConfigurationMetadata);
\tboolean[] match = new boolean[outcomes.length];
\t// 遍歷outcomes,這裡outcomes為null則表示匹配,不為null則表示不匹配
\tfor (int i = 0; i < outcomes.length; i++) {
\t\tConditionOutcome outcome = outcomes[i];
\t\tmatch[i] = (outcome == null || outcome.isMatch());
\t\tif (!match[i] && outcomes[i] != null) {
\t\t\t// 這裡若有某個類不匹配的話,此時調用父類SpringBootCondition的logOutcome方法打印日誌
\t\t\tlogOutcome(autoConfigurationClasses[i], outcomes[i]);
\t\t\t// 並將不匹配情況記錄到report
\t\t\tif (report != null) {
\t\t\t\treport.recordConditionEvaluation(autoConfigurationClasses[i], this,
\t\t\t\t\t\toutcomes[i]);
\t\t\t}
\t\t}
\t}
\treturn match;
}
/<code>

FilteringSpringBootCondition的match方法主要做的事情還是調用抽象模板方法getOutcomes來根據條件來過濾自動配置類,而複寫getOutcomes模板方法的有三個子類,這裡不再一一分析,只挑選OnClassCondition複寫的getOutcomes方法進行分析。

5.1 OnClassCondition

先直接上OnClassCondition複寫的getOutcomes方法的代碼:

<code>// OnClassCondition.java

protected final ConditionOutcome[] getOutcomes(String[] autoConfigurationClasses,
\t\tAutoConfigurationMetadata autoConfigurationMetadata) {
\t// Split the work and perform half in a background thread. Using a single
\t// additional thread seems to offer the best performance. More threads make

\t// things worse
\t// 這裡經過測試用兩個線程去跑的話性能是最好的,大於兩個線程性能反而變差
\tint split = autoConfigurationClasses.length / 2;
\t// 【1】開啟一個新線程去掃描判斷已經加載的一半自動配置類
\tOutcomesResolver firstHalfResolver = createOutcomesResolver(
\t\t\tautoConfigurationClasses, 0, split, autoConfigurationMetadata);
\t// 【2】這裡用主線程去掃描判斷已經加載的一半自動配置類
\tOutcomesResolver secondHalfResolver = new StandardOutcomesResolver(
\t\t\tautoConfigurationClasses, split, autoConfigurationClasses.length,
\t\t\tautoConfigurationMetadata, getBeanClassLoader());
\t// 【3】先讓主線程去執行解析一半自動配置類是否匹配條件
\tConditionOutcome[] secondHalf = secondHalfResolver.resolveOutcomes();
\t// 【4】這裡用新開啟的線程取解析另一半自動配置類是否匹配
\t// 注意為了防止主線程執行過快結束,resolveOutcomes方法裡面調用了thread.join()來
\t// 讓主線程等待新線程執行結束,因為後面要合併兩個線程的解析結果
\tConditionOutcome[] firstHalf = firstHalfResolver.resolveOutcomes();
\t// 新建一個ConditionOutcome數組來存儲自動配置類的篩選結果
\tConditionOutcome[] outcomes = new ConditionOutcome[autoConfigurationClasses.length];
\t// 將前面兩個線程的篩選結果分別拷貝進outcomes數組
\tSystem.arraycopy(firstHalf, 0, outcomes, 0, firstHalf.length);
\tSystem.arraycopy(secondHalf, 0, outcomes, split, secondHalf.length);
\t// 返回自動配置類的篩選結果
\treturn outcomes;
}
/<code>

可以看到,OnClassCondition的getOutcomes方法主要解析自動配置類是否符合匹配條件,當然這個匹配條件指自動配置類上的註解@ConditionalOnClass指定的類存不存在於classpath中,存在則返回匹配,不存在則返回不匹配。

由於解析自動配置類是否匹配比較耗時,因此從上面代碼中我們可以看到分別創建了firstHalfResolver和secondHalfResolver兩個解析對象,這兩個解析對象個分別對應一個線程去解析加載的自動配置類是否符合條件,最終將兩個線程的解析自動配置類的匹配結果合併後返回。

那麼自動配置類是否符合條件的解析判斷過程又是怎樣的呢?現在我們分別來看一下上面代碼註釋標註的【1】,【2】,【3】和【4】處。

5.1.1 createOutcomesResolver

這裡對應前面5.1節的代碼註釋標註【1】處的OutcomesResolver firstHalfResolver = createOutcomesResolver(...);的方法:

<code>// OnClassCondition.java

private OutcomesResolver createOutcomesResolver(String[] autoConfigurationClasses,
\t\tint start, int end, AutoConfigurationMetadata autoConfigurationMetadata) {
\t// 新建一個StandardOutcomesResolver對象
\tOutcomesResolver outcomesResolver = new StandardOutcomesResolver(
\t\t\tautoConfigurationClasses, start, end, autoConfigurationMetadata,
\t\t\tgetBeanClassLoader());
\ttry {
\t\t// new一個ThreadedOutcomesResolver對象,並將StandardOutcomesResolver類型的outcomesResolver對象作為構造器參數傳入
\t\treturn new ThreadedOutcomesResolver(outcomesResolver);
\t}
\t// 若上面開啟的線程拋出AccessControlException異常,則返回StandardOutcomesResolver對象
\tcatch (AccessControlException ex) {
\t\treturn outcomesResolver;
\t}
}
/<code>

可以看到createOutcomesResolver方法創建了一個封裝了StandardOutcomesResolver類的ThreadedOutcomesResolver解析對象。 我們再來看下ThreadedOutcomesResolver這個線程解析類封裝StandardOutcomesResolver這個對象的目的是什麼?我們繼續跟進代碼:

<code>// OnClassCondtion.java

private ThreadedOutcomesResolver(OutcomesResolver outcomesResolver) {
\t// 這裡開啟一個新的線程,這個線程其實還是利用StandardOutcomesResolver的resolveOutcomes方法
\t// 對自動配置類進行解析判斷是否匹配
\tthis.thread = new Thread(
\t\t\t() -> this.outcomes = outcomesResolver.resolveOutcomes());
\t// 開啟線程
\tthis.thread.start();
}
/<code>

可以看到在構造ThreadedOutcomesResolver對象時候,原來是開啟了一個線程,然後這個線程其實還是調用了剛傳進來的StandardOutcomesResolver對象的resolveOutcomes方法去解析自動配置類。具體如何解析呢?稍後我們在分析【3】處代碼secondHalfResolver.resolveOutcomes();的時候再深究。

5.1.2 new StandardOutcomesResolver

這裡對應前面5.1節的【2】處的代碼OutcomesResolver secondHalfResolver = new StandardOutcomesResolver(...);,邏輯很簡單,就是創建了一個StandardOutcomesResolver對象,用於後面解析自動配置類是否匹配,同時,新建的一個線程也是利用它來完成自動配置類的解析的。

5.1.3 StandardOutcomesResolver.resolveOutcomes方法

這裡對應前面5.1節標註的【3】的代碼ConditionOutcome[] secondHalf = secondHalfResolver.resolveOutcomes();。

這裡StandardOutcomesResolver.resolveOutcomes方法承擔瞭解析自動配置類匹配與否的全部邏輯,是我們要重點分析的方法,resolveOutcomes方法最終把解析的自動配置類的結果賦給secondHalf數組。那麼它是如何解析自動配置類是否匹配條件的呢?

<code>// OnClassCondition$StandardOutcomesResolver.java

public ConditionOutcome[] resolveOutcomes() {
\t// 再調用getOutcomes方法來解析
\treturn getOutcomes(this.autoConfigurationClasses, this.start, this.end,
\t\t\tthis.autoConfigurationMetadata);
}

private ConditionOutcome[] getOutcomes(String[] autoConfigurationClasses,
\t\tint start, int end, AutoConfigurationMetadata autoConfigurationMetadata) { // 只要autoConfigurationMetadata沒有存儲相關自動配置類,那麼outcome默認為null,則說明匹配
\tConditionOutcome[] outcomes = new ConditionOutcome[end - start];
\t// 遍歷每一個自動配置類
\tfor (int i = start; i < end; i++) {
\t\tString autoConfigurationClass = autoConfigurationClasses[i];
\t\t// TODO 對於autoConfigurationMetadata有個疑問:為何有些自動配置類的條件註解能被加載到autoConfigurationMetadata,而有些又不能,比如自己定義的一個自動配置類HelloWorldEnableAutoConfiguration就沒有被存到autoConfigurationMetadata中
\t\tif (autoConfigurationClass != null) {
\t\t\t// 這裡取出註解在AutoConfiguration自動配置類類的@ConditionalOnClass註解的指定類的全限定名,
\t\t\t// 舉個栗子,看下面的KafkaStreamsAnnotationDrivenConfiguration這個自動配置類
\t\t\t/**
\t\t\t * @ConditionalOnClass(StreamsBuilder.class)
\t\t\t * class KafkaStreamsAnnotationDrivenConfiguration {
\t\t\t * // 省略無關代碼
\t\t\t * }
\t\t\t */
\t\t\t// 那麼取出的就是StreamsBuilder類的全限定名即candidates = org.apache.kafka.streams.StreamsBuilder

\t\t\tString candidates = autoConfigurationMetadata
\t\t\t\t\t.get(autoConfigurationClass, "ConditionalOnClass"); // 因為這裡是處理某個類是否存在於classpath中,所以傳入的key是ConditionalOnClass
\t\t\t// 若自動配置類標有ConditionalOnClass註解且有值,此時調用getOutcome判斷是否存在於類路徑中
\t\t\tif (candidates != null) {
\t\t\t\t// 拿到自動配置類註解@ConditionalOnClass的值後,再調用getOutcome方法去判斷匹配結果,若該類存在於類路徑,則getOutcome返回null,否則非null
\t\t\t\t/*******************【主線,重點關注】******************/
\t\t\t\toutcomes[i - start] = getOutcome(candidates);
\t\t\t}
\t\t}
\t}
\treturn outcomes;
}
/<code>

可以看到StandardOutcomesResolver.resolveOutcomes的方法中再次調用getOutcomes方法,主要是從autoConfigurationMetadata對象中獲取到自動配置類上的註解@ConditionalOnClass指定的類的全限定名,然後作為參數傳入getOutcome方法用於去類路徑加載該類,若能加載到則說明註解@ConditionalOnClass滿足條件,此時說明自動配置類匹配成功。

但是別忘了,這裡只是過了@ConditionalOnClass註解這一關,若自動配置類還有其他註解比如@ConditionalOnBean,若該@ConditionalOnBean註解不滿足條件的話,同樣最終結果是不匹配的。這裡扯的有點遠,我們回到OnClassCondtion的判斷邏輯,繼續進入getOutcome方法看它是如何去判斷@ConditionalOnClass註解滿不滿足條件的。

<code>// OnClassCondition$StandardOutcomesResolver.java

// 返回的outcome記錄的是不匹配的情況,不為null,則說明不匹配;為null,則說明匹配

private ConditionOutcome getOutcome(String candidates) {
\t// candidates的形式為“org.springframework.boot.autoconfigure.aop.AopAutoConfiguration.ConditionalOnClass=org.aspectj.lang.annotation.Aspect,org.aspectj.lang.reflect.Advice,org.aspectj.weaver.AnnotatedElement”
\ttry {// 自動配置類上@ConditionalOnClass的值只有一個的話,直接調用getOutcome方法判斷是否匹配
\t\tif (!candidates.contains(",")) {
\t\t\t// 看到因為傳入的參數是 ClassNameFilter.MISSING,因此可以猜測這裡應該是得到不匹配的結果
\t\t\t/******************【主線,重點關注】********************/
\t\t\treturn getOutcome(candidates, ClassNameFilter.MISSING,
\t\t\t\t\tthis.beanClassLoader);
\t\t}
\t\t// 自動配置類上@ConditionalOnClass的值有多個的話,則遍歷每個值(其值以逗號,分隔)
\t\tfor (String candidate : StringUtils
\t\t\t\t.commaDelimitedListToStringArray(candidates)) {
\t\t\tConditionOutcome outcome = getOutcome(candidate,
\t\t\t\t\tClassNameFilter.MISSING, this.beanClassLoader);
\t\t\t// 可以看到,這裡只要有一個不匹配的話,則返回不匹配結果
\t\t\tif (outcome != null) {
\t\t\t\treturn outcome;
\t\t\t}
\t\t}
\t}
\tcatch (Exception ex) {
\t\t// We'll get another chance later
\t}
\treturn null;
}
/<code>

可以看到,getOutcome方法再次調用重載方法getOutcome進一步去判斷註解@ConditionalOnClass指定的類存不存在類路徑中,跟著主線繼續跟進去:

<code>// OnClassCondition$StandardOutcomesResolver.java

private ConditionOutcome getOutcome(String className,
\t\tClassNameFilter classNameFilter, ClassLoader classLoader) {

\t// 調用classNameFilter的matches方法來判斷`@ConditionalOnClass`指定的類存不存在類路徑中
\t/******************【主線,重點關注】********************/
\tif (classNameFilter.matches(className, classLoader)) { // 這裡調用classNameFilter去判斷className是否存在於類路徑中,其中ClassNameFilter又分為PRESENT和MISSING兩種;目前只看到ClassNameFilter為MISSING的調用情況,所以默認為true的話記錄不匹配信息;若傳入ClassNameFilter為PRESENT的話,估計還要再寫一個else分支
\t\treturn ConditionOutcome.noMatch(ConditionMessage
\t\t\t\t.forCondition(ConditionalOnClass.class)
\t\t\t\t.didNotFind("required class").items(Style.QUOTE, className));
\t}
\treturn null;
}
/<code>

我們一層一層的剝,最終剝到了最底層了,這個真的需要足夠耐心,沒辦法,源碼只能一點一點的啃,嘿嘿。可以看到最終是調用ClassNameFilter的matches方法來判斷@ConditionalOnClass指定的類存不存在類路徑中,若不存在的話,則返回不匹配。

我們繼續跟進ClassNameFilter的源碼:

<code>// FilteringSpringBootCondition.java

protected enum ClassNameFilter {
\t// 這裡表示指定的類存在於類路徑中,則返回true
\tPRESENT {

\t\t@Override
\t\tpublic boolean matches(String className, ClassLoader classLoader) {
\t\t\treturn isPresent(className, classLoader);
\t\t}

\t},
\t// 這裡表示指定的類不存在於類路徑中,則返回true

\tMISSING {

\t\t@Override
\t\tpublic boolean matches(String className, ClassLoader classLoader) {
\t\t\treturn !isPresent(className, classLoader); // 若classpath不存在className這個類,則返回true
\t\t}

\t};
\t// 這又是一個抽象方法,分別被PRESENT和MISSING枚舉類實現
\tpublic abstract boolean matches(String className, ClassLoader classLoader);
\t// 檢查指定的類是否存在於類路徑中\t
\tpublic static boolean isPresent(String className, ClassLoader classLoader) {
\t\tif (classLoader == null) {
\t\t\tclassLoader = ClassUtils.getDefaultClassLoader();
\t\t}
\t\t// 利用類加載器去加載相應類,若沒有拋出異常則說明類路徑中存在該類,此時返回true
\t\ttry {
\t\t\tforName(className, classLoader);
\t\t\treturn true;
\t\t}// 若不存在於類路徑中,此時拋出的異常將catch住,返回false。
\t\tcatch (Throwable ex) {
\t\t\treturn false;
\t\t}
\t}
\t// 利用類加載器去加載指定的類
\tprivate static Class> forName(String className, ClassLoader classLoader)
\t\t\tthrows ClassNotFoundException {
\t\tif (classLoader != null) {
\t\t\treturn classLoader.loadClass(className);
\t\t}
\t\treturn Class.forName(className);
\t}

}
/<code>

可以看到ClassNameFilter原來是FilteringSpringBootCondition的一個內部枚舉類,其實現了判斷指定類是否存在於classpath中的邏輯,這個類很簡單,這裡不再詳述。

5.1.4 ThreadedOutcomesResolver.resolveOutcomes方法

這裡對應前面5.1節的標註的【4】的代碼ConditionOutcome[] firstHalf = firstHalfResolver.resolveOutcomes()。

前面分析5.1.3 StandardOutcomesResolver.resolveOutcomes方法已經刨根追底,陷入細節比較深,現在我們需要跳出來繼續看前面標註的【4】的代碼ConditionOutcome[] firstHalf = firstHalfResolver.resolveOutcomes()的方法哈。

這裡是用新開啟的線程去調用StandardOutcomesResolver.resolveOutcomes方法解析另一半自動配置類是否匹配,因為是新線程,這裡很可能會出現這麼一種情況:主線程解析完屬於自己解析的一半自動配置類後,那麼久繼續往下跑了,此時不會等待新開啟的子線程的。

因此,為了讓主線程解析完後,我們需要讓主線程繼續等待正在解析的子線程,直到子線程結束。那麼我們繼續跟進代碼區看下ThreadedOutcomesResolver.resolveOutcomes方法是怎樣實現讓主線程等待子線程的:

<code>// OnClassCondition$ThreadedOutcomesResolver.java

public ConditionOutcome[] resolveOutcomes() {
\ttry {
\t\t// 調用子線程的Join方法,讓主線程等待
\t\tthis.thread.join();
\t}
\tcatch (InterruptedException ex) {
\t\tThread.currentThread().interrupt();
\t}
\t// 若子線程結束後,此時返回子線程的解析結果
\treturn this.outcomes;

}
/<code>

可以看到用了Thread.join()方法來讓主線程等待正在解析自動配置類的子線程,這裡應該也可以用CountDownLatch來讓主線程等待子線程結束。最終將子線程解析後的結果賦給firstHalf數組。

5.2 OnBeanCondition和OnWebApplicationCondition

前面5.1 OnClassCondition節深入分析了OnClassCondition是如何過濾自動配置類的,那麼自動配置類除了要經過OnClassCondition的過濾,還要經過OnBeanCondition和OnWebApplicationCondition這兩個條件類的過濾,這裡不再詳述,有興趣的小夥伴可自行分析。

6 AutoConfigurationImportListener

這裡繼續深究前面 4.1節的 AutoConfigurationImportSelector.getAutoConfigurationEntry方法的觸發自動配置類過濾完畢的事件fireAutoConfigurationImportEvents(configurations, exclusions);這句代碼。

我們直接點進fireAutoConfigurationImportEvents方法看看其是如何觸發事件的:

<code>// AutoConfigurationImportSelector.java

private void fireAutoConfigurationImportEvents(List<string> configurations,
\t\tSet<string> exclusions) {
\t// 從spring.factories總獲取到AutoConfigurationImportListener即ConditionEvaluationReportAutoConfigurationImportListener
\tList<autoconfigurationimportlistener> listeners = getAutoConfigurationImportListeners();
\tif (!listeners.isEmpty()) {
\t\t// 新建一個AutoConfigurationImportEvent事件
\t\tAutoConfigurationImportEvent event = new AutoConfigurationImportEvent(this,
\t\t\t\tconfigurations, exclusions);
\t\t// 遍歷剛獲取到的AutoConfigurationImportListener
\t\tfor (AutoConfigurationImportListener listener : listeners) {
\t\t\t// 這裡調用各種Aware方法用於觸發事件前賦值,比如設置factory,environment等

\t\t\tinvokeAwareMethods(listener);
\t\t\t// 真正觸發AutoConfigurationImportEvent事件即回調listener的onXXXEveent方法。這裡用於記錄自動配置類的評估信息
\t\t\tlistener.onAutoConfigurationImportEvent(event);
\t\t}
\t}
}
/<autoconfigurationimportlistener>/<string>/<string>/<code>

如上,fireAutoConfigurationImportEvents方法做了以下兩件事情:

  1. 調用getAutoConfigurationImportListeners方法從spring.factoris配置文件獲取實現AutoConfigurationImportListener接口的事件監聽器;如下圖,可以看到獲取的是ConditionEvaluationReportAutoConfigurationImportListener:


  1. 遍歷獲取的各個事件監聽器,然後調用監聽器各種Aware方法給監聽器賦值,最後再依次回調事件監聽器的onAutoConfigurationImportEvent方法,執行監聽事件的邏輯。

此時我們再來看下ConditionEvaluationReportAutoConfigurationImportListener監聽器監聽到事件後,它的onAutoConfigurationImportEvent方法究竟做了哪些事情:

<code>// ConditionEvaluationReportAutoConfigurationImportListener.java

public void onAutoConfigurationImportEvent(AutoConfigurationImportEvent event) {
\tif (this.beanFactory != null) {
\t\t// 獲取到條件評估報告器對象
\t\tConditionEvaluationReport report = ConditionEvaluationReport
\t\t\t\t.get(this.beanFactory);
\t\t// 將符合條件的自動配置類記錄到unconditionalClasses集合中

\t\treport.recordEvaluationCandidates(event.getCandidateConfigurations());
\t\t// 將要exclude的自動配置類記錄到exclusions集合中
\t\treport.recordExclusions(event.getExclusions());
\t}
}
/<code>

可以看到,ConditionEvaluationReportAutoConfigurationImportListener監聽器監聽到事件後,做的事情很簡單,只是分別記錄下符合條件和被exclude的自動配置類。

7 AutoConfigurationPackages

前面已經詳述了SpringBoot的自動配置原理了,最後的最後,跟SpringBoot自動配置有關的註解@AutoConfigurationPackage還沒分析,我們來看下這個註解的源碼:

<code>@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import(AutoConfigurationPackages.Registrar.class)
public @interface AutoConfigurationPackage {
}
/<code>

可以看到@AutoConfigurationPackage註解是跟SpringBoot自動配置所在的包相關的,即將 添加該註解的類所在的package 作為 自動配置package 進行管理。

接下來我們再看看AutoConfigurationPackages.Registrar類是幹嘛的,直接看源碼:

<code>//AutoConfigurationPackages.Registrar.java

static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports {
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata,
\t\tBeanDefinitionRegistry registry) {
\tregister(registry, new PackageImport(metadata).getPackageName());

}

@Override
public Set<object> determineImports(AnnotationMetadata metadata) {
\treturn Collections.singleton(new PackageImport(metadata));
}
}
/<object>/<code>

可以看到Registrar類是AutoConfigurationPackages的靜態內部類,實現了ImportBeanDefinitionRegistrar和DeterminableImports兩個接口。現在我們主要來關注下Registrar實現的registerBeanDefinitions方法,顧名思義,這個方法是註冊bean定義的方法。看到它又調用了AutoConfigurationPackages的register方法,繼續跟進源碼:

<code>// AutoConfigurationPackages.java

public static void register(BeanDefinitionRegistry registry, String... packageNames) {
\tif (registry.containsBeanDefinition(BEAN)) {
\t\tBeanDefinition beanDefinition = registry.getBeanDefinition(BEAN);
\t\tConstructorArgumentValues constructorArguments = beanDefinition
\t\t\t\t.getConstructorArgumentValues();
\t\tconstructorArguments.addIndexedArgumentValue(0,
\t\t\t\taddBasePackages(constructorArguments, packageNames));
\t}
\telse {
\t\tGenericBeanDefinition beanDefinition = new GenericBeanDefinition();
\t\tbeanDefinition.setBeanClass(BasePackages.class);
\t\tbeanDefinition.getConstructorArgumentValues().addIndexedArgumentValue(0,
\t\t\t\tpackageNames);
\t\tbeanDefinition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
\t\tregistry.registerBeanDefinition(BEAN, beanDefinition);
\t}
}
/<code>

如上,可以看到register方法註冊了一個packageNames即自動配置類註解@EnableAutoConfiguration所在的所在的包名相關的bean。那麼註冊這個bean的目的是為了什麼呢? 結合官網註釋知道,註冊這個自動配置包名相關的bean是為了被其他地方引用,比如JPA entity scanner,具體拿來幹什麼久不知道了,這裡不再深究了。

8 小結

好了,SpringBoot的自動配置的源碼分析就到這裡了,比較長,有些地方也很深入細節,讀完需要一定的耐心。

最後,我們再總結下SpringBoot自動配置的原理,主要做了以下事情:

  1. 從spring.factories配置文件中加載自動配置類;
  2. 加載的自動配置類中排除掉@EnableAutoConfiguration註解的exclude屬性指定的自動配置類;
  3. 然後再用AutoConfigurationImportFilter接口去過濾自動配置類是否符合其標註註解(若有標註的話)@ConditionalOnClass,@ConditionalOnBean和@ConditionalOnWebApplication的條件,若都符合的話則返回匹配結果;
  4. 然後觸發AutoConfigurationImportEvent事件,告訴ConditionEvaluationReport條件評估報告器對象來分別記錄符合條件和exclude的自動配置類。
  5. 最後spring再將最後篩選後的自動配置類導入IOC容器中

最後留個自己的疑問,還望知道答案的大佬解答,這裡表示感謝

為了避免加載不必要的自動配置類造成內存浪費,FilteringSpringBootCondition用於過濾spring.factories文件的自動配置類,而FilteringSpringBootCondition為啥只有OnOnBeanCondition,OnClassCondition和onWebApplicationCondition這三個條件類用於過濾,為啥沒有onPropertyCondtion,onResourceCondition等條件類來過濾自動配置類呢?

下節預告: SpringBoot的啟動流程是怎樣的?--SpringBoot源碼(五)

由於筆者水平有限,若文中有錯誤還請指出,謝謝。

參考:

1,@AutoConfigurationPackage註解



分享到:


相關文章: