1. 程式人生 > >老師,Spring 是怎麼解決迴圈依賴的?

老師,Spring 是怎麼解決迴圈依賴的?

前言

你可能會有如下問題:

1、想看Spring原始碼,但是不知道應當如何入手去看,對整個Bean的流程沒有概念,碰到相關問題也沒有頭緒如何下手

2、看過幾遍原始碼,沒辦法徹底理解,沒什麼感覺,沒過一陣子又忘了

本文將結合實際問題,由問題引出原始碼,並在解釋時會盡量以圖表的形式讓你一步一步徹底理解Spring Bean的IOC、DI、生命週期、作用域等。

先看一個迴圈依賴問題

現象

迴圈依賴其實就是迴圈引用,也就是兩個或則兩個以上的bean互相持有對方,最終形成閉環。比如A依賴於B,B依賴於C,C又依賴於A。如下圖:



如何理解“依賴”呢,在Spring中有:

  • 構造器迴圈依賴

  • field屬性注入迴圈依賴


直接上程式碼:

構造器迴圈依賴


結果:專案啟動失敗,發現了一個cycle


2.field屬性注入迴圈依賴


結果:專案啟動成功



3.field屬性注入迴圈依賴(prototype)


結果:專案啟動失敗,發現了一個cycle。



現象總結:同樣對於迴圈依賴的場景,構造器注入和prototype型別的屬性注入都會初始化Bean失敗。因為@Service預設是單例的,所以單例的屬性注入是可以成功的。

分析原因

分析原因也就是在發現SpringIOC的過程,如果對原始碼不感興趣可以關注每段原始碼分析之後的總結和迴圈依賴問題的分析

即可。

SpringBean的載入流程(原始碼分析)

簡單一段程式碼作為入口


ClassPathXmlApplicationContext是一個載入XML配置檔案的類,與之相對的還有AnnotationConfigWebApplicationContext,這兩個類大差不差的,只是ClassPathXmlApplicationContext的Resource是XML檔案而AnnotationConfigWebApplicationContext是Scan註解獲得的。

看到第二行就已經可以直接獲取bean的例項了,所以第一行構造方法時,就已經完成了對所有bean的載入。

ClassPathXmlApplicationContext舉例,他裡面儲存的東西如下:


BeanDefinition在IOC容器中的註冊

接下來簡要分析一下loadBeanDefinitions。

對於這個BeanDefinition,我是這麼理解的: 它是SpringIOC過程中間的一個產物,可以看成是對Bean定義的抽象,裡面封裝的資料都是與Bean定義相關的,封裝了一些基本的bean的Property、initi-method、destroy-method等。

這裡的主要方法是loadBeanDefinitions,這裡不詳細展開說,它主要做了幾件事:

1、初始化了BeanDefinitionReader

2、通過BeanDefinitionReader獲取Resource,也就是xml配置檔案的位置,並且把檔案轉換成一個叫Document的物件

3、接下來需要將Document物件轉化成容器內部的資料結構(也就是BeanDefinition),也即是將Bean定義的List、Map、Set等各種元素進行解析,轉換成Managed類(Spring對BeanDefinition資料的封裝)放在BeanDefinition中;這個方法是RegisterBeanDefinition(),也就是解析的過程。

4、解析完成後,會把解析的結果放到BeanDefinition物件中並設定到一個Map中

以上這個過程就是BeanDefinition在IOC容器中的註冊。

再回到Refresh方法,總結每一步如下圖:

總結:這一部分步驟主要是Spring如何載入Xml檔案或者註解,並把它解析成BeanDefinition。

Spring建立Bean的過程

先回到之前的refresh方法(也就是在構造ApplicationContext時的方法),我們跳過不重要的部分:



我們直接看finishBeanFactoryInitialization裡面的preInstantiateSingletons方法,顧名思義初始化所有的單例bean,擷取部分如下:



現在來看核心的getBean方法,對於所有獲取Bean物件是例項,都是用這個getBean方法,這個方法最終呼叫的是doGetBean方法,這個方法就是所謂的DI(依賴注入)發生的地方。

程式=資料+演算法,之前的BeanDefinition就是“資料”,依賴注入也就是在BeanDefinition準備好情況下進行進行的,這個過程不簡單,因為Spring提供了很多引數配置,每一個引數都代表了IOC容器的特性,這些特性的實現需要在Bean的生命週期中完成。

程式碼比較多,就不貼了,大家可以自行檢視AbstractBeanFactory裡面的doGetBean方法,這裡直接上圖,這個圖就是依賴注入的整個過程:



總結:Spring建立好了BeanDefinition之後呢,會開始例項化Bean,並且對Bean的依賴屬性進行填充。例項化時底層使用了CGLIB或Java反射技術。上圖中instantiateBean核PupulateBean方法很重要!

迴圈依賴問題分析

我們先總結一下之前的結論:

1、構造器注入和prototype型別的field注入發生迴圈依賴時都無法初始化

2、field注入單例的bean時,儘管有迴圈依賴,但bean仍然可以被成功初始化

針對這幾個結論,提出問題

  1. 單例的設值注入bean是如何解決迴圈依賴問題呢?如果A中注入了B,那麼他們初始化的順序是什麼樣子的?

  2. 為什麼prototype型別的和構造器型別的Spring無法解決迴圈依賴呢?

之前在DefaultListableBeanFactory類中,列出了一個表格;現在我把關鍵的精華屬性列出來:


前面三個Map,我們稱為單例初始化的三級快取,理解這個問題,我們目前只需關注“三級”,也就是singletonFactories

分析:

對於問題1,單例的設值注入,如果A中注入了B,B應該是A中的一個屬性,那麼猜想應該是A已經被instantiate(例項化)之後,在populateBean(填充A中的屬性)時,對B進行初始化。

對於問題2,instantiate(例項化)其實就是理解成new一個物件的過程,而new的時候肯定要執行構造方法,所以猜想對於應該是A在instantiate(例項化)時,進行B的初始化。

有了分析和猜想之後呢,圍繞關鍵的屬性,根據從上圖的doGetBean方法開始到populateBean所有的程式碼,我整理了如下圖:



上圖是整個過程中關鍵的程式碼路徑,感興趣的可以自己debug幾回,最關鍵的解決迴圈依賴的是如上的兩個標紅的方法,第一個方法getSingleton會從singletonFactories裡面拿Singleton,而addSingletonFactory會把Singleton放入singletonFactories。

對於問題1:單例的設值注入bean是如何解決迴圈依賴問題呢?如果A中注入了B,那麼他們初始化的順序是什麼樣子的?

假設迴圈注入是A-B-A:A依賴B(A中autowire了B),B又依賴A(B中又autowire了A):



本質就是三級快取發揮作用,解決了迴圈。

對於當時問題2,instantiate(例項化)其實就是理解成new一個物件的過程,而new的時候肯定要執行構造方法,所以猜想對於應該是A在instantiate(例項化)時,進行B的初始化。

答案也很簡單,因為A中構造器注入了B,那麼A在關鍵的方法addSingletonFactory()之前就去初始化了B,導致三級快取中根本沒有A,所以會發生死迴圈,Spring發現之後就丟擲異常了。至於Spring是如何發現異常的呢,本質上是根據Bean的狀態給Bean進行mark,如果遞迴呼叫時發現bean當時正在建立中,那麼久丟擲迴圈依賴的異常即可。

那麼prototype的Bean是如何初始化的呢?

prototypeBean有一個關鍵的屬性:


儲存著正在建立的prototype的beanName,在流程上並沒有暴露任何factory之類的快取。並且在beforePrototypeCreation(String beanName)方法時,把每個正在建立的prototype的BeanName放入一個set中:


並且會迴圈依賴時檢查beanName是否處於建立狀態,如果是就丟擲異常:


從流程上就可以檢視,無論是構造注入還是設值注入,第二次進入同一個Bean的getBean方法是,一定會在校驗部分丟擲異常,因此不能完成注入,也就不能實現迴圈引用。

總結:Spring在InstantiateBean時執行構造器方法,構造出例項,如果是單例的話,會將它放入一個singletonBeanFactory的快取中,再進行populateBean方法,設定屬性。通過一個singletonBeanFactory的快取解決了迴圈依賴的問題。

再解決一個問題

現在大家已經對Spring整個流程有點感覺了,我們再來解決一個簡單的常見的問題:

考慮一下如下的singleton程式碼:


一個Singleton的Bean中Autowired了一個prototype的Bean,那麼問題來了,每次呼叫SingletonBean.doSomething()時列印的物件是不是同一個呢?

有了之前的知識儲備,我們簡單分析一下:因為Singleton是單例的,所以在專案啟動時就會初始化,prototypeBean本質上只是它的一個Property,那麼ApplicationContex中只存在一個SingletonBean和一個初始化SingletonBean時建立的一個prototype型別的PrototypeBean。

那麼每次呼叫SingletonBean.doSomething()時,Spring會從ApplicationContex中獲取SingletonBean,每次獲取的SingletonBean是同一個,所以即便PrototypeBean是prototype的,但PrototypeBean仍然是同一個。每次打印出來的記憶體地址肯定是同一個。

那這個問題如何解決呢?

解決辦法也很簡單,這種情況我們不能通過注入的方式注入一個prototypeBean,只能在程式執行時手動呼叫getBean("prototypeBean")方法,我寫了一個簡單的工具類:


對於這個ApplicationContextAware介面:

在某些特殊的情況下,Bean需要實現某個功能,但該功能必須藉助於Spring容器才能實現,此時就必須讓該Bean先獲取Spring容器,然後藉助於Spring容器實現該功能。為了讓Bean獲取它所在的Spring容器,可以讓該Bean實現ApplicationContextAware介面。

感興趣的讀者自己可以試試。

總結:

回到迴圈依賴的問題,有的人可能會問singletonBeanFactory只是一個三級快取,那麼一級快取和二級快取有什麼用呢?

其實大家只要理解整個流程就可以切入了,Spring在初始化Singleton的時候大致可以分幾步,初始化——設值——銷燬,迴圈依賴的場景下只有A——B——A這樣的順序,但在併發的場景下,每一步在執行時,都有可能呼叫getBean方法,而單例的Bean需要保證只有一個instance,那麼Spring就是通過這些個快取外加物件鎖去解決這類問題,同時也可以省去不必要的重複操作。Spring的鎖的粒度選取也是很吊的,這裡暫時不深入研究了。

解決此類問題的關鍵是要對SpringIOC和DI的整個流程做到心中有數,看原始碼一般情況下不要求每一行程式碼都瞭解透徹,但是對於整個的流程和每個流程中在做什麼事需要了然,這樣實際遇到問題時才可以很快的切入進行分析解決。

希望這篇文章可以幫助你對Spring的IOC和DI的流程有一個更深刻的認識!