Spring IoC之處理Bean創建循環依賴

什麼是循環依賴

循環依賴,其實就是循環引用,就是兩個或者兩個以上的 bean 互相引用對方,最終形成一個閉環,如 A 依賴 B,B 依賴 C,C 依賴 A。如下圖所示:

image.png

循環依賴,其實就是一個死循環的過程,在初始化 A 的時候發現引用了 B,這時就會去初始化 B,然後又發現 B 引用 C,跑去初始化 C,初始化 C 的時候發現引用了 A,則又會去初始化 A,依次循環永不退出,除非有終結條件。

Spring-Bean循環依賴

Spring 循環依賴的場景有兩種:

  1. 構造器的循環依賴
  2. 字段屬性的循環依賴

對於構造器的循環依賴,Spring 是無法解決的,只能拋出BeanCurrentlyInCreationException異常表示循環依賴,所以Spring只解決基於屬性的循環依賴,而且Spring只解決 scope 爲 singleton 的循環依賴。對於scope 爲 prototype 的 bean ,Spring無法解決,直接拋出 BeanCurrentlyInCreationException 異常。Spring爲什麼不處理 prototype bean 的循環依賴呢?其實如果理解 Spring 是如何解決 singleton bean 的循環依賴就明白了,所以我們先來關注 Spring 是如何解決 singleton bean 的循環依賴的。

Spring-解決循環依賴

我們先從加載 bean 最初始的方法AbstractBeanFactory#doGetBean方法開始。在 #doGetBean(...) 方法中,首先會根據 beanName 從單例 bean 緩存中獲取,如果不爲空則直接返回。從緩存中獲取單例bean調用的是DefaultSingletonBeanRegistry#getSingleton(String beanName, boolean allowEarlyReference) 方法,代碼如下:

// DefaultSingletonBeanRegistry.java

@Nullable
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
    // 從單例緩存中加載 bean
    Object singletonObject = this.singletonObjects.get(beanName);
    // 如果緩存中的 bean 爲空,且當前 bean 正在創建
    if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
        // 單例緩存加鎖
        synchronized (this.singletonObjects) {
            // 嘗試從 earlySingletonObjects 獲取
            singletonObject = this.earlySingletonObjects.get(beanName);
            // earlySingletonObjects 中沒有,且允許提前創建
            if (singletonObject == null && allowEarlyReference) {
                // 從 singletonFactories 中獲取對應的 ObjectFactory
                ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
                if (singletonFactory != null) {
                    // 獲得 bean
                    singletonObject = singletonFactory.getObject();
                    // 添加 bean 到 earlySingletonObjects 中
                    this.earlySingletonObjects.put(beanName, singletonObject);
                    // 從 singletonFactories 中移除對應的 ObjectFactory
                    this.singletonFactories.remove(beanName);
                }
            }
        }
    }
    return singletonObject;
}

這個方法主要是從三個緩存中獲取,分別是:singletonObjects、singletonFactories、earlySingletonObjects 。三者定義如下:

/**
 * 緩存單例bean: bean name --> bean instance.
 */
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

/**
 * 緩存單例bean的factory: bean name --> ObjectFactory.
 * 這個 Map 也是解決【循環依賴】的關鍵所在。
 */
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);

/**
 * 緩存[早期]單例bean: bean name --> bean instance.
 * [早期]單例bean可以理解爲還沒有還沒有初始化完整的bean。
 */
private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);

這三個緩存是 Spring 解決 singleton bean循環依賴的關鍵因素所在,稱他們爲三級緩存:

  1. 第一級爲 singletonObjects:單例對象的 Cache
  2. 第二級爲 earlySingletonObjects:單例對象工廠的 Cache
  3. 第三級爲 singletonFactories:提前曝光的單例對象的 Cache

這裏,我們已經通過 #getSingleton(String beanName, boolean allowEarlyReference) 方法,看到他們是如何配合的。詳細分析該方法之前,提下其中的 #isSingletonCurrentlyInCreation(String beanName) 方法和 allowEarlyReference 變量:

  • isSingletonCurrentlyInCreation(String beanName) 方法:判斷當前 singleton bean 是否處於創建中。bean 處於創建中,也就是說 bean 在初始化但是沒有完成初始化,這樣一個過程與Spring 解決 bean 循環依賴的理念相輔相成。因爲 Spring 解決 singleton bean 循環依賴的核心就是在bean已經開始初始化但還未初始化完成時就將其曝光。
  • allowEarlyReference 變量:從字面意思上面理解就是允許提前拿到引用。其實真正的意思是,是否允許從 singletonFactories 緩存中通過 #getObject() 方法,拿到已經開始初始化但未初始化完成的對象。爲什麼會有這樣一個字段呢?原因就在於 singletonFactories 是 Spring 解決 singleton bean 循環依賴的訣竅所在,這個我們後續分析。

#getSingleton(String beanName, boolean allowEarlyReference) 方法,整個過程如下:

  • 首先,從一級緩存 singletonObjects 獲取。
  • 如果,沒有且當前指定的 beanName 正在創建,就再從二級緩存 earlySingletonObjects 中獲取。
  • 如果,還是沒有獲取到且允許singletonFactories通過#getObject()獲取,則從三級緩存singletonFactories獲取。如果獲取到,則通過其#getObject()方法,獲取對象,並將其加入到二級緩存earlySingletonObjects中,並從三級緩存singletonFactories刪除。代碼如下:
// DefaultSingletonBeanRegistry.java
singletonObject = singletonFactory.getObject();
this.earlySingletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);

所以,二級緩存存在的意義,就是緩存三級緩存中的 ObjectFactory 的 #getObject() 方法的執行結果,提早曝光的單例 bean 對象。

SingletonFactory

上面是從緩存中獲取,但是緩存中的數據從哪裏添加進來的呢?一直往下跟會發現在AbstractAutowireCapableBeanFactory 的 #doCreateBean(final String beanName, final RootBeanDefinition mbd, final Object[] args) 方法中,有這麼一段代碼:

// AbstractAutowireCapableBeanFactory.java

// 提早緩存單例,以便即使在諸如BeanFactoryAware之類的生命週期接口觸發時也能夠解析循環引用。
boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
        isSingletonCurrentlyInCreation(beanName));
if (earlySingletonExposure) {
    if (logger.isTraceEnabled()) {
        logger.trace("Eagerly caching bean '" + beanName +
                "' to allow for resolving potential circular references");
    }
    addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
}

當一個 Bean 滿足三個條件時,則調用 #addSingletonFactory(...) 方法,將它添加到緩存中。三個條件如下:

  • 單例
  • 運行提前暴露 bean
  • 當前 bean 正在創建中

跟進addSingletonFactory()方法,從這段代碼可以看出,singletonFactories 這個三級緩存纔是解決 Spring Bean 循環依賴的訣竅所在。同時這段代碼發生在 #createBeanInstance(...) 方法之後,也就是說這個 bean 其實已經被創建出來了,但是它還不是很完美(沒有進行屬性填充和初始化),但是對於其他依賴它的對象而言已經足夠了(可以根據對象引用定位到堆中對象),能夠被認出來了。所以 Spring 在這個時候,選擇將該對象提前曝光出來。

// DefaultSingletonBeanRegistry.java

protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
    Assert.notNull(singletonFactory, "Singleton factory must not be null");
    synchronized (this.singletonObjects) {
        if (!this.singletonObjects.containsKey(beanName)) {
            // 放入三級緩存
            this.singletonFactories.put(beanName, singletonFactory);
            // 從二級緩存移除
            this.earlySingletonObjects.remove(beanName);
            this.registeredSingletons.add(beanName);
        }
    }
}

addSingleton

介紹到這裏我們已經知道三級緩存 singletonFactories 和二級緩存 earlySingletonObjects 值的出處,那一級緩存在哪裏設置的呢?按照常用的緩存做法(創建後放入緩存),可以猜測將單例放入緩存的位置應該是在創建單例的時,往下看,在類 AbstractBeanFactory 中,可以發現有這麼一段代碼:

// AbstractBeanFactory.java
// 創建單例bean
if (mbd.isSingleton()) {
    // <>跟進去看看<>
    sharedInstance = getSingleton(beanName, () -> {
        try {
            return createBean(beanName, mbd, args);
        } catch (BeansException ex) {
            // Explicitly remove instance from singleton cache: It might have been put there
            // eagerly by the creation process, to allow for circular reference resolution.
            // Also remove any beans that received a temporary reference to the bean.
            destroySingleton(beanName);
            throw ex;
        }
    });
    bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
}

<>跟進去看看<> 查看調用的方法getSingleton(),代碼如下:

// DefaultSingletonBeanRegistry.java

public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
    Assert.notNull(beanName, "Bean name must not be null");
    synchronized (this.singletonObjects) {
        Object singletonObject = this.singletonObjects.get(beanName);
        if (singletonObject == null) {
            //....
            try {
                singletonObject = singletonFactory.getObject();
                newSingleton = true;
            }
            //.....
            if (newSingleton) {
                // 調用addSingleton()
                addSingleton(beanName, singletonObject);
            }
        }
        return singletonObject;
    }
}

注意,此處的 #getSingleton(String beanName, ObjectFactory<?> singletonFactory) 方法與開始介紹的#getSingleton(String beanName, boolean allowEarlyReference)不同。

 

#addSingleton(String beanName, Object singletonObject) 方法,代碼如下:

// DefaultSingletonBeanRegistry.java

protected void addSingleton(String beanName, Object singletonObject) {
    synchronized (this.singletonObjects) {
        // 添加至一級緩存,同時從二級、三級緩存中刪除
        this.singletonObjects.put(beanName, singletonObject);
        this.singletonFactories.remove(beanName);
        this.earlySingletonObjects.remove(beanName);
        this.registeredSingletons.add(beanName);
    }
}

小結

至此,Spring 關於 singleton bean 循環依賴已經分析完畢了。所以我們基本上可以確定 Spring 解決循環依賴的方案了:

  • Spring 在創建 bean 的時候並不是等它完全完成,而是在創建過程中將創建中的 bean 的 ObjectFactory 提前曝光(即加入到 singletonFactories 緩存中)。
  • 這樣,一旦下一個 bean 創建的時候需要依賴 bean ,則直接使用 ObjectFactory 的 #getObject() 方法來獲取了。

到這裏,關於 Spring 解決 bean 循環依賴就已經分析完畢了。最後來描述下就上面那個循環依賴 Spring 解決的過程:

  • 首先 A 完成初始化第一步並將自己提前曝光出來(通過 ObjectFactory 將自己提前曝光),在初始化的時候,發現自己依賴對象 B,此時就會去嘗試 get(B),這個時候發現 B 還沒有被創建出來
  • 然後 B 就走創建流程,在 B 初始化的時候,同樣發現自己依賴 C,C 也沒有被創建出來
  • 這個時候 C 又開始初始化進程,但是在初始化的過程中發現自己依賴 A,於是嘗試 get(A),這個時候由於 A 已經添加至緩存中(一般都是添加至三級緩存 singletonFactories ),通過 ObjectFactory 提前曝光,所以可以通過 ObjectFactory#getObject() 方法來拿到 A 對象,C 拿到 A 對象後順利完成初始化,然後將自己添加到一級緩存中
  • 回到 B ,B 也可以拿到 C 對象,完成初始化,A 可以順利拿到 B 完成初始化。到這裏整個鏈路就已經完成了初始化過程了

 

Spring爲什麼不處理 prototype bean 呢?

最後回答一下開始遺留的問題,相信大家看了上面的描述之後,應該知道Spring解決循環依賴的關鍵是singletonFactories ,而其映射關係是beanName-->ObjectFactory,只有單例我們才能夠根據beanName獲取唯一的ObjectFactory,而作用域爲prototype是沒有做到的。[個人理解]

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