SpringBoot項目啓動過程源碼終於整體捋了一遍(二)

上一篇寫到run()方法分兩步,即:

1.new SpringApplication() 利用主源類primarySources初始化SpringApplication類

2.調用SpringApplication的run()方法啓動項目,並傳遞參數args。

也提到啓動的時候主源類primarySources可以有多個,那就有一個問題了,這個primarySources爲什麼還可以有多個呢,啓動的時候需要從主源類中獲取什麼信息,也就是primarySources有什麼用,這篇就帶着這個問題繼續往下擼源碼,先看上面提到的兩步中的第一步,即new SpringApplication()初始化SpringApplication類:

 SpringApplication類有兩個構造方法,區別就是是否指定資源加載器resourceLoader,如果不指定的話就將其賦值爲null,上一篇的例子中啓動類中直接使用SpringApplication.run(),所以默認調用的第一種構造器沒有指定resourceLoader,可見SpringBoot啓動還可以先自己初始化SpringApplication並指定resourceLoader,然後在啓動。

那這個resourceLoader又是什麼東西呢?看第二個構造器的第一行:

上來就給this.resourceLoader賦值,然後直接在該類中Crtl+F搜索這個this.resourceLoader,首先看到這個:

原來即使構造SpringApplication的時候不指定 resourceLoader也沒關係,還提供了setResourceLoader()方法設置,爲時不晚,繼續搜索,看到這裏:

上面那個方法就不說了,提供方法拿到當前的resourceLoader,重點是下面那個方法裏的this.resourceLoader.getClassLoader(),該類中其他地方用到這個resourceLoader也都是這樣,其實看名字也就明白了,原來這個resourceLoader就是爲了拿到類加載器。之所以可以自己設置啓動時的resourceLoader也是爲了可以應用自定義類加載器之類的吧,那看到這裏又有一個疑問了,我不傳resourceLoader就給它賦值爲null的啊,類加載器還能爲null?這不鬧呢嘛,這個問題先留着,源碼繼續往下擼會解釋resourceLoader爲null的時候怎麼辦。

繼續看構造方法的第二行:

這就不用說了,沒有primarySources會報錯,這更加讓我疑惑這個primarySources有什麼用,可以有多個,沒有就報錯。這裏的Assert類可以學習用一下,可以優雅地拋出異常。

繼續看第三行:

呦吼終於出現了,趕緊Crtl+F一下這個this.primarySources,看哪裏用到了,首先看到這個:

居然還帶半路添加primarySources的,繼續搜索,看到這裏:

沒有其他地方用到這個this.primarySources了,這個getAllsource()方法就是獲取所有的primarySources,看來關鍵在於哪裏調用了這個getAllSources(),一共也就兩個類中會調用這個方法,其中一個就是SpringApplication類本身,另一個暫時不管,SpringApplication類中prepareContext()方法中調用了getAllSources():

 調用getAllSources()無非是爲了拿到所有的primarySources,另一個調用這個方法只是爲了判斷prepareContext是否爲空,所以沒必要管,而這個prepareContext()需要看一看。

文章開頭說的兩步,第二步run()方法中會調用這個prepareContext()方法,這個方法是爲了初始化Spring上下文Applicationcontext,具體的暫時先不做介紹,因爲後續的文章遲早會擼到這一步,到時候再說,先直接看調用getAllSource()的這幾步,即:

源碼中還特意用註釋與前面隔開了,因爲和前面確實沒什麼關係 ,這幾步是爲了加載拿到的所有的primarySources,那就看load()方法:

 先看getBeanDefinitionRegistry(context),可知這個上下文context的作用是拿到一個Bean定義的註冊器,可以看看這個getBeanDefinitionRegistry()方法:

方法裏沒什麼,需要好好理理這幾個類之間的繼承關係,就不難理解了,這裏就不理了怕扯遠,就當這個context厲害得很,就是能拿到這個BeanDefinitionRegistry註冊器,然後重點應該是createBeanDefinitionLoader()方法,有了註冊器和需要被加載的資源,就可以先拿Bean定義的加載器了即BeanDefinitionLoader,方法裏是這樣的:

 就是用註冊器和source構造了一個BeanDefinitionLoader,那就看看這個構造方法:

 意思就是註冊器和被加載的資源source不能爲空,然後初始化了source,等着被加載吧。還初始化了幾種Bean定義的讀取器,annotateReader即註解形式的Bean定義讀取器,xmlReader即XML形式的Bean定義讀取器,還有一個Grooyy語言的定義讀取器。現在知道爲什麼代碼中既可以用註解@Bean、@Service這些去將對象交給Bean容器管理,又可以在XML中配置Bean,就是這兩個讀取器在幹活。還有一個Grooyy語言不常用就不說了。scanner是類路徑掃描器,可以指定類的路徑去掃描讀取,這也算是一種讀取器,最後是給這個類路徑掃描器添加了排除過濾器,還可以排除某些路徑不去那裏讀取,具體細節可以去看看ClassPathBeanDefinitionScanner這個類,這裏就不展開了。

然後往上翻倒數第四張圖,再回到這裏,爲了看圖方便那張圖再貼一遍:

已經拿到這個Bean定義的加載器BeanDefinitionLoader了,然後是一頓set,其中有一個setResourceLoader(),這不是set之前說的那個類加載器嘛,這裏告訴我們不要把這個Bean定義加載器和類加載混淆,它還是需要類加載器的。另外兩個set的東西也介紹一下,BeanNameGenerator主要功能是從一定的條件中計算出bean的name,bean容器的管理很大一部分是基於beanName的,瞭解這個可以讓我們自定義beanName的生成規則。而這個this.environment即ConfigurationEnvironment應用運行時環境,常見的用途就是用來根據鍵獲取配置文件的值,示例如下:

yml配置文件裏這樣就能獲取到值了:

ConfigurationEnvironment類還有很多其他方法,畢竟運行時環境中的信息還是挺多的,這些可以去了解一下。然後終於到loader.load()這一步了,看一下這個load()方法:

大佬們都喜歡重載方法,剛loader.load()調用的是第一個load()方法,裏面就是遍歷調用第二個load()方法,並且計數。第二個load()方法裏將被加載的資源分類型,一共是四種類型,又來四個load()。

第一種是加載Class,調用了這個load()方法:

一上來又是判斷是否是Groovy語言,這個不管,然後有一個isComponent()方法判斷資源是否爲組件,判斷成功了才用之前說的註解形式的Bean定義讀取器去註冊資源,那就看一下這個isComponent()方法:

如果打上了@Component註解直接返回true,如果沒有這個註解,依次爲如果是內部類、匿名類或者沒有構造函數的類,就返回false。這裏要注意一下,避免開發過程中有些對象不能被Bean容器管理。

如果這個isComponent()方法返回true,下一步就是用之前提到的註解形式Bean定義讀取器加載資源,即this.annotatedReader.register()。

第二種是加載Resource,調用了這個load()方法:

忽略Groovy的話就是用之前說的XML形式Bean定義讀取器去加載資源。

第三種是加載Package,調用了這個load()方法:

就是使用包掃描器加載資源。

第四種是加載CharSequence,調用的load()方法有點長就不貼了,這種形式實際上等同於source爲字符串,解析字符串後進行匹配上面三種方式,即Class、Resource和Package,哪個匹配上了就用哪種方式去加載。

看到這幾種方式就可以聯想到前面構造了BeanDefinitionLoader的時候,初始化的就是這幾個東西。

源碼擼到這裏已經離SpringBoot啓動流程這條主線有點遠了,其實一直都在講Bean加載的過程,也就是主源類primarySources的加載,Bean加載就Bean加載吧,繼續往下看,這四種加載方式除去第四種是匹配前三種加載方式,也就前三種加載Bean的方式我們最常用的應該是第一種和第三種,第一種更多,那就着中看看第一種加載Class的方式是怎麼加載Bean的吧,還是那張圖再貼一遍:

 得看看this.annotateReader.register()方法裏有什麼:

就這?又是一個遍歷註冊,看看registerBean()方法:

調用了doRegisterBean()方法,並且其他三個參數都爲null,看一下這個方法:

這個方法的第二個參數instanceSupplier是創建Bean實例的回調,可見SpringBoot也提供了自定義註冊Bean的方法,那就一行一行來分析吧。

首先上來就是利用bean的class構造了一個AnnotatedGenericBeanDefinition對象,暫且和源碼中一樣稱之爲abd,看一下這個對象的構造方法:

 AnnotatedGenericBeanDefinition實現了接口AnnotatedBeanDefinition,而AnnotatedBeanDefinition繼承了BeanDefinition。Spring中使用BeanDefinition描述了一個bean的實例,而這個AnnotatedBeanDefinition比BeanDefinition還多了兩個獲取類上註解的方法:

那這個abd豈不是厲害了,即能描述一個bean實例即獲取bean的各種信息比如beanName、scope什麼的,又能獲取類上的註解 ,留着有用。

繼續往下看:

 嗯哼?看着意思還有不註冊這個bean的情況?那就看一看shouldSkip()方法,截取一部分:

原來@Conditional註解作用在這裏,@Conditional 是Spring 4框架的新特性。此註解使得只有在特定條件滿足時才啓用一些配置,也就是註冊這個bean,如果沒有這個註解直接返回false,也就是繼續註冊,有這個註解後面一長串是否滿足註解中的條件。

繼續往下看:

 setInstanceSupplier()是設置bean註冊的回調,就是給這個abd設置的,其實這個註冊bean的過程很大一部分時間都在給abd各種set屬性,看來這個abd很有用。然後又是拿到註解scope的值set給abd,如果沒有@scope註解則默認爲單例"singleton"。scope有哪幾種值接下來就會說到。

繼續往下看:

原來beanName是這時候出來的,beanNameGenerator前面也提到了,generateBeanName()方法裏面有一定的規則去生成beanName。規則大致介紹下,圖就不貼了,大概就是獲得所有註解,看是不是有@Component、@ManageBean或者是@Name註解,有任何一個就將他們的value值返回作爲beanName,如果都沒有配置value,就會返回默認的baenName,這就和我們平時的使用對上了,而且最後還會判斷value的值不能多於一個,舉個例子就是不能在一個類上打了@Component又打@Service而且都定義了value,不然會報錯,詳細的可以點進方法看。

繼續往下看:

這是個什麼鬼?看方法名意思是處理通用註解?點進去看看:

 

 害,確實是處理了5個註解,不過說到底又是給abd的一頓set,這五個註解分別是:

@lazy註解:用於指定該Bean是否懶加載,如果該註解的value爲true的話,則這個bean在spring容器初始化之後,第一次使用時才初始化。AbstractBeanDefinition中定義該值的默認值是false。
@primary註解:自動裝配時當出現多個Bean候選者時,被註解爲@Primary的Bean將作爲首選者,否則將拋出異常。
@DependsOn註解:定義Bean初始化順序。如果這個bean是AbstractBeanDefinition的子類的話,還會處理以下兩個註解:
@Role註解:用於用戶自定義的bean,value值是int類型,表明該bean在應用中的角色,默認值是0。
@Description註解:用於描述bean,提高代碼可讀性。

繼續往下看:

上面剛除了完了5個註解,這又來處理@Qualifier註解給abd一頓set,@Autowired是根據類型進行自動裝配的。如果當Spring上下文中存在不止一個該類型的bean時,就會拋出BeanCreationException異常。@Qualifier可以配置自動依賴注入裝配的限定條件,@Qualifier 可以直接指定注入 Bean 的名稱,簡單來說, @Autowired和@Qualifier 結合使用時,自動注入的策略就從 byType 轉變成 byName 了。

繼續往下看:

這段代碼是spring5.0以後新加入的,Spring 5允許使用lambda 表達式來自定義註冊一個 bean。

繼續往下看:

這個abd都被set這麼肥了,終於用到它了,可以把這個BeanDefinitionHolder理解爲abd的一個持有者,先拿着吧,以後想用abd就直接找這個definitionHolder就行了。重點關注一下applyScopedProxyMode()方法:

這段代碼首先需要獲得ScopedProxyMode值,這個值實在@scope註解中使用proxyMode屬性設置的,默認爲NO,就是沒有代理。它還可被設置爲ScopedProxyMode. INTERFACES或者是ScopedProxyMode. TARGET_CLASS。這個值具體有什麼用呢,我們知道@scope註解的value可以設置爲以下幾種:
單例(singleton):在整個應用中,只創建bean的一個實例。
原型(prototype):每次注入或者通過Spring應用上下文獲取的時候,都會創建一個新的實例。
會話(session):在Web應用中,爲每個會話創建一個bean實例。
請求(request):在Web應用中,爲每個請求創建一個bean實例。
當一個singleton作用域的bean中需要注入一個session作用域的bean的時候,會報錯,應爲此時應用沒有人訪問,session作用域bean沒有創建,所以出現了問題。spring提供給我們的解決方案就是通過設置proxyMode屬性的值來解決,當他設置爲ScopedProxyMode. INTERFACES或者是ScopedProxyMode. TARGET_CLASS時,spring會爲session作用域bean創建代理對象,而真正調用的bean則在運行時懶加載,兩者的區別是一個使用JDK提供的動態代理實現,一個使用CGLIB實現。
這段代碼就是通過判斷proxyMode的值爲註冊的Bean創建相應模式的代理對象。默認不創建。

終於到最後一行了,就是這個registerBeanDefinition()方法,墨跡了這麼久終於開始註冊bean了?,看一下這個方法:

拿到beanName又掉了一個registerBeanDefinition()方法,後面aliases別名就不管了,這些方法名一樣的方法只要理清類直接的集成關係,就很容易找到到底調用的哪裏的方法,看一下這個registerBeanDefinition()方法:

@Override
	public void registerBeanDefinition(String beanName, BeanDefinition beanDefinition)
			throws BeanDefinitionStoreException {

		Assert.hasText(beanName, "Bean name must not be empty");
		Assert.notNull(beanDefinition, "BeanDefinition must not be null");

		if (beanDefinition instanceof AbstractBeanDefinition) {
			try {
				((AbstractBeanDefinition) beanDefinition).validate();
			}
			catch (BeanDefinitionValidationException ex) {
				throw new BeanDefinitionStoreException(beanDefinition.getResourceDescription(), beanName,
						"Validation of bean definition failed", ex);
			}
		}

		BeanDefinition existingDefinition = this.beanDefinitionMap.get(beanName);
		if (existingDefinition != null) {
			if (!isAllowBeanDefinitionOverriding()) {
				throw new BeanDefinitionOverrideException(beanName, beanDefinition, existingDefinition);
			}
			else if (existingDefinition.getRole() < beanDefinition.getRole()) {
				// e.g. was ROLE_APPLICATION, now overriding with ROLE_SUPPORT or ROLE_INFRASTRUCTURE
				if (logger.isInfoEnabled()) {
					logger.info("Overriding user-defined bean definition for bean '" + beanName +
							"' with a framework-generated bean definition: replacing [" +
							existingDefinition + "] with [" + beanDefinition + "]");
				}
			}
			else if (!beanDefinition.equals(existingDefinition)) {
				if (logger.isDebugEnabled()) {
					logger.debug("Overriding bean definition for bean '" + beanName +
							"' with a different definition: replacing [" + existingDefinition +
							"] with [" + beanDefinition + "]");
				}
			}
			else {
				if (logger.isTraceEnabled()) {
					logger.trace("Overriding bean definition for bean '" + beanName +
							"' with an equivalent definition: replacing [" + existingDefinition +
							"] with [" + beanDefinition + "]");
				}
			}
			this.beanDefinitionMap.put(beanName, beanDefinition);
		}
		else {
			if (hasBeanCreationStarted()) {
				// Cannot modify startup-time collection elements anymore (for stable iteration)
				synchronized (this.beanDefinitionMap) {
					this.beanDefinitionMap.put(beanName, beanDefinition);
					List<String> updatedDefinitions = new ArrayList<>(this.beanDefinitionNames.size() + 1);
					updatedDefinitions.addAll(this.beanDefinitionNames);
					updatedDefinitions.add(beanName);
					this.beanDefinitionNames = updatedDefinitions;
					removeManualSingletonName(beanName);
				}
			}
			else {
				// Still in startup registration phase
				this.beanDefinitionMap.put(beanName, beanDefinition);
				this.beanDefinitionNames.add(beanName);
				removeManualSingletonName(beanName);
			}
			this.frozenBeanDefinitionNames = null;
		}

		if (existingDefinition != null || containsSingleton(beanName)) {
			resetBeanDefinition(beanName);
		}
	}

方法全貼了,但是我實在不想仔細看了,不過一眼就看到了這句:

行吧行吧,註冊好了原來就是以beanName爲key放在了一個Map裏面,value就是之前一直在set的abd。

其實這還遠不是bean加載的過程,只能理解爲beanDefinition的加載過程,因爲最終產物是能描述bean信息的beanDefinition,至於這個beanDefinition的作用前面也提到了,bean加載還有後續很多步驟,這裏甚至連bean的對象都沒有構造出來,更別說屬性注入、創建代理對象這些了。

這是我寫過的最長的一篇文章了,可是對最初的主線Spring Boot啓動流程而言僅僅是擼了一行代碼,就爲了看那個主源類primarySources有什麼用,就這麼朝bean加載發散出去了,現在知道primarySources的beanDefinition會被放進一個map,然後就可以根據SpringApplication的getAllSources()方法獲取所有的primarySources,然後再從這個map裏中獲取primarySources的實例信息,註解信息什麼的,應該這樣吧,也是猜的。

依然總結一下:這篇文章一直是在分析SpringApplication類的構造方法,對於Spring Boot啓動流程就走了三行代碼,但是摸了一下Bean加載的過程,也就是如何加載主源類primarySources。而且還留了一個問題,就是初始化SpringApplication的時候resourceLoader爲null的時候怎麼拿類加載器。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章