disconf問題引發對spring boot配置載入的探究

zeody發表於2019-05-07

問題

今天小夥伴跑過來說,搭建框架的時候出現disconf配置好的資訊不能夠及時注入到實體類中的情況。他通過實踐發現,spring 載入Configuration 的時候,通過@Autowired注入的RedisProperties 實體類裡面沒有值。等到容器載入完成後,在Controller 層注入的RedisProperties是有資料的,搞了接近一天。我在他控制檯看到了如下資訊(簡化):

**** DISCONF START FIRST SCAN **** //此處省略 **** DISCONF END FIRST SCAN **** //@configuration 註冊bean的資訊(可以自己新增日誌) **** DISCONF START SECOND SCAN **** //此處省略 **** DISCONF END SECOND SCAN ****

通過資訊可以看出,關鍵問題出現在了第二次掃描在Bean註冊之後。第二次掃描負責將配置注入實體類中,詳細可以參考disconf-client設計

那麼第二次掃描在什麼時候進行的呢,開啟DisconfMgrBeanSecond 類

public class DisconfMgrBeanSecond{
    public void init(){
        DisconfMgr.getInstance().secondScan(); //此處進行第二次掃描
    }
    public void destroy(){
        DisconfMgr.getInstance().close();
    }
}
複製程式碼

現在的問題一下明瞭了,我們需要做的也就是將 DisconfMgrBeanSecond 的Bean註冊提前,提前至@Configuration之前。我這裡用的是@DependsOn註解,將其放在Properties實體類上。表明當前Bean依賴於另外一個Bean,可以用來控制順序。

思考

上面的方法只是使用技巧解決了實際問題,我們不禁要思考了,spring載入的順序到底是怎麼樣的?為什麼有的專案沒有載入順序問題,有的就會出bug。接下來我們就來深入擼一下spring的原始碼。(本文基於的原始碼為 spring boot 2.0.0.RELEASE)

除錯方法

很多人不太會除錯原始碼,一上手就從入口函式開始,點幾下就自己犯暈了。還有些人習慣看類圖,從全域性去看,也會很累。這裡不是說類圖方式不好,而是分情況而定。比如你讀 Java 集合框架,類圖就是一個不錯的選擇,一來集合類功能相對獨立,二來集合本身很符合物件導向的思想。面對spring這種名字很相似,程式碼龐大的大型框架時,建議還是以點入面,有目的的去看。這裡介紹一下我自己使用的方法:

  1. 編寫測試工程,比如我要理解spring @Configuration的載入過程,先用spring boot 快速搭建一個可以執行的工程
  2. 在自己需要了解的地方打斷點
  3. 觀察呼叫棧,找到關鍵方法

如下圖

@Configuration載入呼叫棧

Debugger 選單欄中我們很容易找到呼叫棧的資訊,觀察這些方法,我們可以看到這三個方法的方法名很像我們想知道的載入過程

尋找相對靠後的入口方法

在仔細點開原始碼會發現 refresh()方法下的如下程式碼

                this.postProcessBeanFactory(beanFactory); //上下文子類對beanFactory進行後置處理
                this.invokeBeanFactoryPostProcessors(beanFactory);//呼叫工廠處理器,對bean進行註冊
                this.registerBeanPostProcessors(beanFactory); // 註冊bean的攔截處理器
                this.initMessageSource(); //初始化訊息源
                this.initApplicationEventMulticaster(); //初始化上下文事件多播器
                this.onRefresh(); //初始化其他子類上下文的特殊beans
                this.registerListeners(); //檢查監聽類的bean,並註冊他們
                this.finishBeanFactoryInitialization(beanFactory); //例項化剩餘非懶載入的bean單利
                this.finishRefresh(); //完成後重新整理,釋出相應的事件
複製程式碼

如果你通過idea把原始碼下載下來的話,可以看到游標停在 this.finishBeanFactoryInitialization(beanFactory)處,表明此時具體進入的方法。好了,除錯方法暫時就說到這裡,還是來看原始碼吧。

原始碼分析

上面提了一下@Configuration註解的bean 入口在finishBeanFactoryInitialization(beanFactory)方法中,接著往下走到preInstantiateSingletons()方法中

關鍵屬性beanDefinitionNames

我們發現這個方法裡有一個特別顯眼的屬性,beanDefinitionNames,這個就是容器的註冊順序。

beanDefinitionNames順序

我們端點是打在了Test類初始化的地方,但通過debugger 可以發現入口方法載入的反而是TestController類,並且中間方法的呼叫並沒有出現HelloServiceimpl類和TestServiceImpl類的載入。可見真實bean初始化的順序並不是這樣的。

回頭去找 beanDefinitionNames在哪裡初始化的,可以發現在registerBeanDefinition(String beanName, BeanDefinition beanDefinition)方法中,迴圈新增的,接下來再去找registerBeanDefinition 在什麼地方呼叫。

再次打斷點定位到 ClassPathBeanDefinitionScanner.doscan() 方法上

protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
		Assert.notEmpty(basePackages, "At least one base package must be specified");
		Set<BeanDefinitionHolder> beanDefinitions = new LinkedHashSet<>();
		for (String basePackage : basePackages) {
			//掃描package,尋找候選元件
			Set<BeanDefinition> candidates = findCandidateComponents(basePackage);
			//候選元件進行處理,處理其他註解
			for (BeanDefinition candidate : candidates) {
				ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(candidate);
				candidate.setScope(scopeMetadata.getScopeName());
				String beanName = this.beanNameGenerator.generateBeanName(candidate, this.registry);
				if (candidate instanceof AbstractBeanDefinition) {
					postProcessBeanDefinition((AbstractBeanDefinition) candidate, beanName);
				}
				if (candidate instanceof AnnotatedBeanDefinition) {
					AnnotationConfigUtils.processCommonDefinitionAnnotations((AnnotatedBeanDefinition) candidate);
				}
				if (checkCandidate(beanName, candidate)) {
					BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(candidate, beanName);
					definitionHolder =
							AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);
					beanDefinitions.add(definitionHolder);
					registerBeanDefinition(definitionHolder, this.registry);
				}
			}
		}
		return beanDefinitions;
	}
複製程式碼

首先通過掃描找出候選元件,掃描的範圍包含basePackages目錄下的所有class檔案,如果符合條件,將其放在LinkedHashSet中,使其保證唯一有序。判斷條件在ClassPathScanningCandidateComponentProvider.isCandidateComponent()方法中。這個類有兩個屬性,excludeFilters和includeFilters,分別控制著候選類的排除鏈和包含鏈。我debugger不進行設定的話,預設選取下面三種介面子類作為候選載入類,org.springframework.stereotype.Component,javax.annotation.ManagedBean,javax.inject.Named,而@Configuration,@Controller,@Service,@Repository,都是基於Component的註解。

真實bean的載入

上面只是說明白了類檔案的註冊順序,他是通過掃描包名,類名這樣排下來的,只是一個初步順序。

先來看一下之前除錯的初步順序 testConfig-->helloController-->testController-->helloServiceImpl-->testServiceImpl-->test

整體看下來,他是按照包名和型別排序的,只不過有一點需要注意 test 所在的包實際上是在Impl 前面的,且Test類上沒有任何註解,這表明他們的註冊順序其實是:先掃描Component,在掃描@Bean註解。

當bean真正載入的時候是這樣載入的,每載入一個類,看他有沒有依賴,有的話同時載入依賴bean。這也就解釋了為什麼testController為什麼跳過impl 直接載入test。

如何控制載入順序

其實有很多方法控制順序,依賴注入提前,@DepensOn 和 @Order註解,實現Ordered介面等等。像面對disconf這種第三方框架類的bean,最好是使用@DepensOn 來控制載入順序

總結

bean的載入還有很多其他的細節,這裡就不一一展開了。本文主要專注載入順序,順便聊一下初學如何去看原始碼。總結起來就是一句話,小目標,不擴充。

寫到最後才發現上面的問題,載入順序並不是主要原因!!(°ロ°٥) 好吧,下次一定搞清楚了再動筆,這裡也買一個關子,感興趣的童鞋可以自己Debugger找一下原因。這裡給個小提示,是跟代理有關。

相關文章