1. 程式人生 > 其它 >spring中的迴圈依賴(單例)

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 的單例物件的初始化主要分為三步:

  1. createBeanInstance:例項化,其實也就是呼叫物件的構造方法例項化物件
  2. populateBean:填充屬性,這一步主要是多 bean 的依賴屬性進行填充
  3. 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 的例項物件”這種迴圈依賴的情況,原理步驟如下:

  1. A 首先完成了初始化的第一步,並且將自己提前曝光到 singletonFactories 三級快取中
  2. 此時進行初始化的第二步,發現自己依賴物件 B,此時就嘗試去 get(B),發現 B 還沒有被建立,所以走建立 B 的流程
  3. B 在初始化第一步的時候發現自己依賴了物件 A,於是嘗試 get(A),嘗試從一級快取 singletonObjects 中獲取(肯定沒有,因為 A 還沒初始化完全),再嘗試從二級快取 earlySingletonObjects 中獲取(也沒有),再次嘗試從三級快取 singletonFactories 中獲取,由於 A 通過方法 addSingletonFactory() 將自己提前曝光了,所以 B 能夠通過 ObjectFactory.getObject 拿到 A 物件(雖然 A 還沒有初始化完成,沒有填充屬性,但是總比沒有好呀)
  4. B 拿到 A 物件後順利完成了初始化階段 1、2、3,完成初始化之後將自己放入到一級快取 singletonObjects 中
  5. 此時返回 A 中,A 此時能拿到 B 的物件順利完成自己的初始化階段 2、3,最終 A 也完成了初始化,新增到一級快取 singletonObjects 中

4.spring 為什麼不能解決構造器的迴圈依賴

spring 將依賴注入主要分為兩種

  • 構造器的注入
  • setter 方法的注入

對於構造器的迴圈依賴來說,在 bean 呼叫構造器例項化之前,一二三級快取並沒有 bean 的任何相關資訊,在例項化之後才放入三級快取中,因此當 getBean 的時候快取並沒有命中,這樣就丟擲了迴圈依賴的異常了
例如:“A 的構造方法中依賴了 B 的例項物件,同時 B 的構造方法中依賴了 A 的例項物件”
當 A 首先去例項化的時候,需要通過 getBean() 方法來獲取 B 的例項物件,而 getBean() 方法沒有命中任何一級的快取,所以會丟擲異常

參考:https://www.cnblogs.com/csnjava/p/13511034.html