disconf問題引起對spring boot配置加載的探究

問題

今天小夥伴跑過來講,搭建框架的時候出現disconf配置好的信息不可以及時注入到實體類中的狀況。他經過實踐發現,spring 加載Configuration 的時候,經過@Autowired注入的RedisProperties 實體類裏面沒有值。等到容器加載完成後,在Controller 層注入的RedisProperties是有數據的,搞了接近一天。我在他控制檯看到了以下信息(簡化):html

**** DISCONF START FIRST SCAN **** //此處省略 **** DISCONF END FIRST SCAN **** //@configuration 註冊bean的信息(能夠本身添加日誌) **** DISCONF START SECOND SCAN **** //此處省略 **** DISCONF END SECOND SCAN ****java

經過信息能夠看出,關鍵問題出如今了第二次掃描在Bean註冊以後。第二次掃描負責將配置注入實體類中,詳細能夠參考disconf-client設計spring

那麼第二次掃描在何時進行的呢,打開DisconfMgrBeanSecond 類app

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)ide

調試方法

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

  1. 編寫測試工程,好比我要理解spring @Configuration的加載過程,先用spring boot 快速搭建一個能夠運行的工程
  2. 在本身須要瞭解的地方打斷點
  3. 觀察調用棧,找到關鍵方法

以下圖源碼分析

@Configuration加載調用棧

Debugger 菜單欄中咱們很容易找到調用棧的信息,觀察這些方法,咱們能夠看到這三個方法的方法名很像咱們想知道的加載過程post

尋找相對靠後的入口方法

在仔細點開源碼會發現 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找一下緣由。這裏給個小提示,是跟代理有關。

相關文章
相關標籤/搜索