文章目錄
1 簡介
本文,我們來看一下 Spring 是如何解決循環依賴問題的。在本篇文章中,我會首先向大家介紹一下什麼是循環依賴。然後,進入源碼分析階段。爲了更好的說明 Spring 解決循環依賴的辦法,我將會從獲取 bean 的方法getBean(String)
開始,把整個調用過程梳理一遍。梳理完後,再來詳細分析源碼。通過這幾步的講解,希望讓大家能夠弄懂什麼是循環依賴,以及如何解循環依賴。
循環依賴相關的源碼本身不是很複雜,不過這裏要先介紹大量的前置知識。不然這些源碼看起來很簡單,但讀起來可能卻也不知所云。那下面我們先來了解一下什麼是循環依賴。
2 背景知識
2.1 什麼是循環依賴
所謂的循環依賴是指,A 依賴 B,B 又依賴 A,它們之間形成了循環依賴。或者是 A 依賴 B,B 依賴 C,C 又依賴 A。它們之間的依賴關係如下:
這裏以兩個類直接相互依賴爲例,他們的實現代碼可能如下:
public class BeanB {
private BeanA beanA;
// 省略 getter/setter
}
public class BeanA {
private BeanB beanB;
}
配置信息如下:
<bean id="beanA" class="xyz.coolblog.BeanA">
<property name="beanB" ref="beanB"/>
</bean>
<bean id="beanB" class="xyz.coolblog.BeanB">
<property name="beanA" ref="beanA"/>
</bean>
IOC 容器在讀到上面的配置時,會按照順序,先去實例化 beanA。然後發現 beanA 依賴於 beanB,接在又去實例化 beanB。實例化 beanB 時,發現 beanB 又依賴於 beanA。如果容器不處理循環依賴的話,容器會無限執行上面的流程,直到內存溢出,程序崩潰。當然,Spring 是不會讓這種情況發生的。在容器再次發現 beanB 依賴於 beanA 時,容器會獲取 beanA 對象的一個早期的引用(early reference),並把這個早期引用注入到 beanB 中,讓 beanB 先完成實例化。beanB 完成實例化,beanA 就可以獲取到 beanB 的引用,beanA 隨之完成實例化。這裏大家可能不知道“早期引用”是什麼意思,這裏先彆着急,我會在下一章進行說明。
好了,本章先到這裏,我們繼續往下看。
2.2 一些緩存的介紹
在進行源碼分析前,我們先來看一組緩存的定義。如下:
/** Cache of singleton objects: bean name --> bean instance */
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<String, Object>(256);
/** Cache of singleton factories: bean name --> ObjectFactory */
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<String, ObjectFactory<?>>(16);
/** Cache of early singleton objects: bean name --> bean instance */
private final Map<String, Object> earlySingletonObjects = new HashMap<String, Object>(16);
根據緩存變量上面的註釋,大家應該能大致瞭解他們的用途。我這裏簡單說明一下吧:
緩存 | 用途 |
---|---|
singletonObjects |
用於存放完全初始化好的 bean,從該緩存中取出的 bean 可以直接使用 |
earlySingletonObjects |
存放原始的 bean 對象(尚未填充屬性),用於解決循環依賴 |
singletonFactories |
存放 bean 工廠對象,用於解決循環依賴 |
上一章提到了”早期引用“,所謂的”早期引用“是指向原始對象的引用。所謂的原始對象是指剛創建好的對象,但還未填充屬性。這樣講大家不知道大家聽明白了沒,不過沒聽明白也不要緊。簡單做個實驗就知道了,這裏我們先定義一個對象Room
:
/** Room 包含了一些電器 */
public class Room {
private String television;
private String airConditioner;
private String refrigerator;
private String washer;
// 省略 getter/setter
}
配置如下:
<bean id="room" class="xyz.coolblog.demo.Room">
<property name="television" value="Xiaomi"/>
<property name="airConditioner" value="Gree"/>
<property name="refrigerator" value="Haier"/>
<property name="washer" value="Siemens"/>
</bean>
我們先看一下完全實例化好後的 bean 長什麼樣的。如下:
從調試信息中可以看得出,Room
的每個成員變量都被賦上值了。然後我們再來看一下“原始的 bean 對象”長的是什麼樣的,如下:
結果比較明顯了,所有字段都是null
。這裏的 bean 和上面的 bean 指向的是同一個對象Room@1567
,但現在這個對象所有字段都是null
,我們把這種對象成爲原始的對象。形象點說,上面的 bean 對象是一個裝修好的房子,可以拎包入住了。而這裏的 bean 對象還是個毛坯房,還要裝修一下(填充屬性)纔行。
2.3 回顧獲取 bean 的過程
本節,我們來了解從 Spring IOC 容器中獲取 bean 實例的流程(簡化版),這對我們後續的源碼分析會有比較大的幫助。先看圖:
先來簡單介紹一下這張圖,這張圖是一個簡化後的流程圖。開始流程圖中只有一條執行路徑,在條件sharedInstance != null
這裏出現了岔路,形成了綠色和紅色兩條路徑。在上圖中,讀取/添加緩存的方法我用藍色的框和 ☆ 標註了出來。至於虛線的箭頭,和虛線框裏的路徑,這個下面會說到。
我來按照上面的圖,分析一下整個流程的執行順序。這個流程從getBean
方法開始,getBean
是個空殼方法,所有邏輯都在doGetBean
方法中。doGetBean
首先會調用getSingleton(beanName)
方法獲取sharedInstance
,sharedInstance
可能是完全實例化好的 bean,也可能是一個原始的 bean,當然也有可能是null
。如果不爲null
,則走綠色的那條路徑。再經getObjectForBeanInstance
這一步處理後,綠色的這條執行路徑就結束了。
我們再來看一下紅色的那條執行路徑,也就是sharedInstance = null
的情況。在第一次獲取某個 bean 的時候,緩存中是沒有記錄的,所以這個時候要走創建邏輯。上圖中的getSingleton(beanName, new ObjectFactory<Object>() {...})
方法會創建一個 bean 實例,上圖虛線路徑指的是getSingleton
方法內部調用的兩個方法,其邏輯如下:
public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
// 省略部分代碼
singletonObject = singletonFactory.getObject();
// ...
addSingleton(beanName, singletonObject);
}
如上所示,getSingleton
會在內部先調用getObject
方法創建singletonObject
,然後再調用addSingleton
將singletonObject
放入緩存中。getObject
在內部調用了createBean
方法,createBean
方法基本上也屬於空殼方法,更多的邏輯是寫在doCreateBean
方法中的。doCreateBean
方法中的邏輯很多,其首先調用了createBeanInstance
方法創建了一個原始的 bean 對象,隨後調用addSingletonFactory
方法向緩存中添加單例 bean 工廠,從該工廠可以獲取原始對象的引用,也就是所謂的“早期引用”。再之後,繼續調用populateBean
方法向原始 bean 對象中填充屬性,並解析依賴。getObject
執行完成後,會返回完全實例化好的 bean。緊接着再調用addSingleton
把完全實例化好的 bean 對象放入緩存中。到這裏,紅色執行路徑差不多也就要結束的。
我這裏沒有把getObject
、addSingleton
方法和getSingleton(String, ObjectFactory)
並列畫在紅色的路徑裏,目的是想簡化一下方法的調用棧(都畫進來有點複雜)。我們可以進一步簡化上面的調用流程,比如下面:
這個流程看起來是不是簡單多了,命中緩存走綠色路徑,未命中走紅色的創建路徑。好了,本節先到這。
3 源碼分析
好了,經過前面的鋪墊,現在我們終於可以深入源碼一探究竟了,想必大家已等不及了。那我不賣關子了,下面我們按照方法的調用順序,依次來看一下循環依賴相關的代碼。如下:
protected <T> T doGetBean(
final String name, final Class<T> requiredType, final Object[] args, boolean typeCheckOnly)
throws BeansException {
// ......
// 從緩存中獲取 bean 實例
Object sharedInstance = getSingleton(beanName);
// ......
}
public Object getSingleton(String beanName) {
return getSingleton(beanName, true);
}
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
// 從 singletonObjects 獲取實例,singletonObjects 中的實例都是準備好的 bean 實例,可以直接使用
Object singletonObject = this.singletonObjects.get(beanName);
// 判斷 beanName 對應的 bean 是否正在創建中
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
synchronized (this.singletonObjects) {
// 從 earlySingletonObjects 中獲取提前曝光的 bean
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null && allowEarlyReference) {
// 獲取相應的 bean 工廠
ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
if (singletonFactory != null) {
// 提前曝光 bean 實例(raw bean),用於解決循環依賴
singletonObject = singletonFactory.getObject();
// 將 singletonObject 放入緩存中,並將 singletonFactory 從緩存中移除
this.earlySingletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);
}
}
}
}
return (singletonObject != NULL_OBJECT ? singletonObject : null);
}
上面的源碼中,doGetBean
所調用的方法getSingleton(String)
是一個空殼方法,其主要邏輯在getSingleton(String, boolean)
中。該方法邏輯比較簡單,首先從singletonObjects
緩存中獲取 bean 實例。若未命中,再去earlySingletonObjects
緩存中獲取原始 bean 實例。如果仍未命中,則從singletonFactory
緩存中獲取ObjectFactory
對象,然後再調用getObject
方法獲取原始 bean 實例的應用,也就是早期引用。獲取成功後,將該實例放入earlySingletonObjects
緩存中,並將ObjectFactory
對象從singletonFactories
移除。看完這個方法,我們再來看看getSingleton(String, ObjectFactory)
方法,這個方法也是在doGetBean
中被調用的。這次我會把doGetBean
的代碼多貼一點出來,如下:
protected <T> T doGetBean(
final String name, final Class<T> requiredType, final Object[] args, boolean typeCheckOnly)
throws BeansException {
// ......
Object bean;
// 從緩存中獲取 bean 實例
Object sharedInstance = getSingleton(beanName);
// 這裏先忽略 args == null 這個條件
if (sharedInstance != null && args == null) {
// 進行後續的處理
bean = getObjectForBeanInstance(sharedInstance, name, beanName, null);
} else {
// ......
// mbd.isSingleton() 用於判斷 bean 是否是單例模式
if (mbd.isSingleton()) {
// 再次獲取 bean 實例
sharedInstance = getSingleton(beanName, new ObjectFactory<Object>() {
@Override
public Object getObject() throws BeansException {
try {
// 創建 bean 實例,createBean 返回的 bean 是完全實例化好的
return createBean(beanName, mbd, args);
} catch (BeansException ex) {
destroySingleton(beanName);
throw ex;
}
}
});
// 進行後續的處理
bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
}
// ......
}
// ......
// 返回 bean
return (T) bean;
}
這裏的代碼邏輯和我在「2.3 回顧獲取 bean 的過程」一節的最後貼的主流程圖已經很接近了,對照那張圖和代碼中的註釋,大家應該可以理解doGetBean
方法了。繼續往下看:
public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
synchronized (this.singletonObjects) {
// ......
// 調用 getObject 方法創建 bean 實例
singletonObject = singletonFactory.getObject();
newSingleton = true;
if (newSingleton) {
// 添加 bean 到 singletonObjects 緩存中,並從其他集合中將 bean 相關記錄移除
addSingleton(beanName, singletonObject);
}
// ......
// 返回 singletonObject
return (singletonObject != NULL_OBJECT ? singletonObject : null);
}
}
protected void addSingleton(String beanName, Object singletonObject) {
synchronized (this.singletonObjects) {
// 將 <beanName, singletonObject> 映射存入 singletonObjects 中
this.singletonObjects.put(beanName, (singletonObject != null ? singletonObject : NULL_OBJECT));
// 從其他緩存中移除 beanName 相關映射
this.singletonFactories.remove(beanName);
this.earlySingletonObjects.remove(beanName);
this.registeredSingletons.add(beanName);
}
}
上面的代碼中包含兩步操作,第一步操作是調用getObject
創建 bean 實例,第二步是調用addSingleton
方法將創建好的 bean 放入緩存中。代碼邏輯並不複雜,相信大家都能看懂。那麼接下來我們繼續往下看,這次分析的是doCreateBean
中的一些邏輯。如下:
protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final Object[] args)
throws BeanCreationException {
BeanWrapper instanceWrapper = null;
// ......
// ☆ 創建 bean 對象,並將 bean 對象包裹在 BeanWrapper 對象中返回
instanceWrapper = createBeanInstance(beanName, mbd, args);
// 從 BeanWrapper 對象中獲取 bean 對象,這裏的 bean 指向的是一個原始的對象
final Object bean = (instanceWrapper != null ? instanceWrapper.getWrappedInstance() : null);
/*
* earlySingletonExposure 用於表示是否”提前暴露“原始對象的引用,用於解決循環依賴。
* 對於單例 bean,該變量一般爲 true。更詳細的解釋可以參考我之前的文章
*/
boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
isSingletonCurrentlyInCreation(beanName));
if (earlySingletonExposure) {
// ☆ 添加 bean 工廠對象到 singletonFactories 緩存中
addSingletonFactory(beanName, new ObjectFactory<Object>() {
@Override
public Object getObject() throws BeansException {
/*
* 獲取原始對象的早期引用,在 getEarlyBeanReference 方法中,會執行 AOP
* 相關邏輯。若 bean 未被 AOP 攔截,getEarlyBeanReference 原樣返回
* bean,所以大家可以把
* return getEarlyBeanReference(beanName, mbd, bean)
* 等價於:
* return bean;
*/
return getEarlyBeanReference(beanName, mbd, bean);
}
});
}
Object exposedObject = bean;
// ......
// ☆ 填充屬性,解析依賴
populateBean(beanName, mbd, instanceWrapper);
// ......
// 返回 bean 實例
return exposedObject;
}
protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
synchronized (this.singletonObjects) {
if (!this.singletonObjects.containsKey(beanName)) {
// 將 singletonFactory 添加到 singletonFactories 緩存中
this.singletonFactories.put(beanName, singletonFactory);
// 從其他緩存中移除相關記錄,即使沒有
this.earlySingletonObjects.remove(beanName);
this.registeredSingletons.add(beanName);
}
}
}
上面的代碼簡化了不少,不過看起來仍有點複雜。好在,上面代碼的主線邏輯比較簡單,由三個方法組成。如下:
- 創建原始 bean 實例
createBeanInstance(beanName, mbd, args)
- 添加原始對象工廠對象到
singletonFactories
緩存中addSingletonFactory(beanName, new ObjectFactory<Object>{...})
- 填充屬性,解析依賴
populateBean(beanName, mbd, instanceWrapper)
到這裏,本節涉及到的源碼就分析完了。可是看完源碼後,我們似乎仍然不知道這些源碼是如何解決循環依賴問題的。難道本篇文章就到這裏了嗎?答案是否。下面我來解答這個問題,這裏我還是以 BeanA 和 BeanB 兩個類相互依賴爲例。在上面的方法調用中,有幾個關鍵的地方,下面一一列舉出來:
3.1 創建原始 bean 對象
instanceWrapper = createBeanInstance(beanName, mbd, args);
final Object bean = (instanceWrapper != null ? instanceWrapper.getWrappedInstance() : null);
假設 beanA 先被創建,創建後的原始對象爲BeanA@1234
,上面代碼中的 bean 變量指向就是這個對象。
3.2 暴露早期引用
addSingletonFactory(beanName, new ObjectFactory<Object>() {
@Override
public Object getObject() throws BeansException {
return getEarlyBeanReference(beanName, mbd, bean);
}
});
beanA 指向的原始對象創建好後,就開始把指向原始對象的引用通過ObjectFactory
暴露出去。getEarlyBeanReference
方法的第三個參數 bean 指向的正是createBeanInstance
方法創建出原始 bean 對象BeanA@1234
。
3.3 解析依賴
populateBean(beanName, mbd, instanceWrapper);
populateBean
用於向 beanA 這個原始對象中填充屬性,當它檢測到 beanA 依賴於 beanB 時,會首先去實例化 beanB。beanB 在此方法處也會解析自己的依賴,當它檢測到 beanA 這個依賴,於是調用BeanFactry.getBean("beanA")
這個方法,從容器中獲取 beanA。
3.4 獲取早期引用
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
synchronized (this.singletonObjects) {
// ☆ 從緩存中獲取早期引用
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null && allowEarlyReference) {
ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
if (singletonFactory != null) {
// ☆ 從 SingletonFactory 中獲取早期引用
singletonObject = singletonFactory.getObject();
this.earlySingletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);
}
}
}
}
return (singletonObject != NULL_OBJECT ? singletonObject : null);
}
接着上面的步驟講,populateBean
調用BeanFactry.getBean("beanA")
以獲取 beanB 的依賴。getBean("beanA")
會先調用getSingleton("beanA")
,嘗試從緩存中獲取 beanA。此時由於 beanA 還沒完全實例化好,於是this.singletonObjects.get("beanA")
返回null
。接着 this.earlySingletonObjects.get("beanA")
也返回空,因爲 beanA 早期引用還沒放入到這個緩存中。最後調用singletonFactory.getObject()
返回singletonObject
,此時singletonObject != null
。singletonObject
指向BeanA@1234
,也就是createBeanInstance
創建的原始對象。此時 beanB 獲取到了這個原始對象的引用,beanB 就能順利完成實例化。beanB 完成實例化後,beanA 就能獲取到 beanB 所指向的實例,beanA 隨之也完成了實例化工作。由於beanB.beanA
和 beanA 指向的是同一個對象BeanA@1234
,所以 beanB 中的 beanA 此時也處於可用狀態了。
以上的過程對應下面的流程圖:
4 總結
到這裏,本篇文章差不多就快寫完了,不知道大家看懂了沒。這篇文章在前面做了大量的鋪墊,然後再進行源碼分析。相比於我之前寫的幾篇文章,本篇文章所對應的源碼難度上比之前簡單一些。但說實話也不好寫,我本來只想簡單介紹一下背景知識,然後直接進行源碼分析。但是又怕有的朋友看不懂,所以還是用了大篇幅介紹的背景知識。這樣寫,可能有的朋友覺得比較囉嗦。但是考慮到大家的水平不一,爲了保證讓大家能夠更好的理解,所以還是儘量寫的詳細一點。本篇文章總的來說寫的還是有點累的,花了一些心思思考怎麼安排章節順序,怎麼簡化代碼和畫圖。如果大家看完這篇文章,覺得還不錯的話,不妨給個贊吧,也算是對我的鼓勵吧。
由於個人的技術能力有限,若文章有錯誤不妥之處,歡迎大家指出來。好了,本篇文章到此結束,謝謝大家的閱讀。