spring中的迴圈依賴(單例)
技術標籤:spring
1.什麼是 spring 的迴圈依賴
迴圈依賴就是在多個 bean 中,相互依賴對方,導致在建立的時候無法載入。如:beanA 依賴了 beanB,beanB 又依賴了 beanC,beanC 最後又依賴了 beanA,成了一個閉環。迴圈依賴是 bean 與 bean 之間才會發生的,而方法之間的相互呼叫的情況,叫做迴圈呼叫,此招無解最終會因為方法之間呼叫過多導致記憶體溢位
在程式碼中表現大概就是如下的情況:
public class BeanA {
private BeanB beanB;
public BeanB getBeanB() { return beanB;}
public void setBeanB(BeanB beanB) {this.beanB = beanB;}
}
public class BeanB {
private BeanC beanC;
public BeanC getBeanC() {return beanC;}
public void setBeanC(BeanC beanC) {this.beanC = beanC;}
}
public class BeanC {
private BeanA beanA;
public BeanA getBeanA () {return beanA;}
public void setBeanA(BeanA beanA) {this.beanA = beanA;}
}
2.spring 如何解決迴圈依賴
spring 的迴圈依賴的理論依據其實是基於 Java 的引用傳遞,當我們獲取到物件的引用時,物件的欄位或屬性是可以延後設定的(但是構造器必須是在獲取引用之前)
spring 的單例物件的初始化主要分為三步:
- createBeanInstance:例項化,其實也就是呼叫物件的構造方法例項化物件
- populateBean:填充屬性,這一步主要是多 bean 的依賴屬性進行填充
- initializeBean:呼叫 spring.xml 中的 init 方法
從上面講述的單例 bean 初始化步驟我們可以知道,迴圈依賴主要發生在第一、第二步。那麼我們要解決迴圈引用也應該從初始化過程著手,對於單例來說,在 spring 容器整個生命週期內,有且只有一個物件,所以很容易想到這個物件應該存在快取中,spring 為了解決單例的迴圈依賴問題,使用了三級快取
首先我們看原始碼,三級快取主要指:
/** 單例物件的快取池 */
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<String, Object>(256);
/** 單例物件工廠的快取池 */
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<String, ObjectFactory<?>>(16);
/** 提前曝光的單例物件的快取池 */
private final Map<String, Object> earlySingletonObjects = new HashMap<String, Object>(16);
我們在建立 bean 的時候,首先想到的是從快取中獲取這個單例的 bean,這個快取就是 singletonObjects。主要呼叫方法就是:
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
// 先從一級快取中獲取已經例項化,屬性填充完成的 bean
Object singletonObject = this.singletonObjects.get(beanName);
// 判斷當前單例bean是否正在建立中,也就是沒有初始化完成(比如A的構造器依賴了B物件所以得先去建立B物件,
// 或則在A的populateBean過程中依賴了B物件,得先去建立B物件,這時的A就是處於建立中的狀態
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
synchronized (this.singletonObjects) {
// 從二級快取中查詢,獲取 bean 的早期引用,例項化完成但屬性填充未完成的 bean
singletonObject = this.earlySingletonObjects.get(beanName);
// 是否允許從singletonFactories中通過getObject拿到物件
if (singletonObject == null && allowEarlyReference) {
// 從三級快取中查詢,例項化完成,屬性未填充的 bean
ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
if (singletonFactory != null) {
singletonObject = singletonFactory.getObject();
// 放入 earlySingletonObjects 二級快取中
this.earlySingletonObjects.put(beanName, singletonObject);
// 從 singletonFactories 三級快取中移除
this.singletonFactories.remove(beanName);
}
}
}
}
return (singletonObject != NULL_OBJECT ? singletonObject : null);
}
分析 getSingleton() 的整個過程,spring 首先從一級快取 singletonObjects 中獲取。如果獲取不到,並且物件正在建立中,就再從二級快取 earlySingletonObjects 中獲取。如果還是獲取不到且允許 singletonFactories 通過 getObject() 獲取,就從三級快取singletonFactory.getObject() 中獲取,如果獲取到了則:
// 放入 earlySingletonObjects 二級快取中
this.earlySingletonObjects.put(beanName, singletonObject);
// 從 singletonFactories 三級快取中移除
this.singletonFactories.remove(beanName);
從 singletonFactories 中移除,並放入 earlySingletonObjects 中。其實也就是從三級快取移動到了二級快取
從上面三級快取的分析,我們可以知道,spring 解決迴圈依賴的訣竅就在於 singletonFactories 這個三級快取。這個快取的型別是 ObjectFactory,定義如下:
public interface ObjectFactory<T> {
T getObject() throws BeansException;
}
這個介面在下面被引用
// 該方法發生在 createBeanInstance 之後
protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
Assert.notNull(singletonFactory, "Singleton factory must not be null");
synchronized (this.singletonObjects) {
if (!this.singletonObjects.containsKey(beanName)) {
// put 進 singletonFactories 三級快取中
this.singletonFactories.put(beanName, singletonFactory);
this.earlySingletonObjects.remove(beanName);
this.registeredSingletons.add(beanName);
}
}
}
這裡就是解決迴圈依賴的關鍵,這段程式碼發生在 createBeanInstance 之後,也就是說單例物件此時已經被創建出來(呼叫了構造器)。這個物件已經被生產出來了,雖然還不完美(還沒有進行初始化的第二步和第三步),但是已經能被人認出來了(根據物件引用能定位到堆中的物件),所以 spring 此時將這個物件提前曝光出來讓大家認識,讓大家使用
3.spring 解決迴圈依賴的原理
這樣做有什麼好處呢?讓我們來分析一下“A 的某個屬性或者 setter 依賴了 B 的例項物件,同時 B 的某個屬性或者 setter 依賴了 A 的例項物件”這種迴圈依賴的情況,原理步驟如下:
- A 首先完成了初始化的第一步,並且將自己提前曝光到 singletonFactories 三級快取中
- 此時進行初始化的第二步,發現自己依賴物件 B,此時就嘗試去 get(B),發現 B 還沒有被建立,所以走建立 B 的流程
- B 在初始化第一步的時候發現自己依賴了物件 A,於是嘗試 get(A),嘗試從一級快取 singletonObjects 中獲取(肯定沒有,因為 A 還沒初始化完全),再嘗試從二級快取 earlySingletonObjects 中獲取(也沒有),再次嘗試從三級快取 singletonFactories 中獲取,由於 A 通過方法 addSingletonFactory() 將自己提前曝光了,所以 B 能夠通過 ObjectFactory.getObject 拿到 A 物件(雖然 A 還沒有初始化完成,沒有填充屬性,但是總比沒有好呀)
- B 拿到 A 物件後順利完成了初始化階段 1、2、3,完成初始化之後將自己放入到一級快取 singletonObjects 中
- 此時返回 A 中,A 此時能拿到 B 的物件順利完成自己的初始化階段 2、3,最終 A 也完成了初始化,新增到一級快取 singletonObjects 中
4.spring 為什麼不能解決構造器的迴圈依賴
spring 將依賴注入主要分為兩種
- 構造器的注入
- setter 方法的注入
對於構造器的迴圈依賴來說,在 bean 呼叫構造器例項化之前,一二三級快取並沒有 bean 的任何相關資訊,在例項化之後才放入三級快取中,因此當 getBean 的時候快取並沒有命中,這樣就丟擲了迴圈依賴的異常了
例如:“A 的構造方法中依賴了 B 的例項物件,同時 B 的構造方法中依賴了 A 的例項物件”
當 A 首先去例項化的時候,需要通過 getBean() 方法來獲取 B 的例項物件,而 getBean() 方法沒有命中任何一級的快取,所以會丟擲異常