Spring解決循環依賴源碼分析

本文基于spring4.3.9

什么是循環依賴

在spring中一個bean依賴另外一個bean有兩種方式,一是通過構造函數,二是通過字段注入。
用代碼來解釋循環依賴,那么就是以下場景

public class A{
  @Autowired
  B b;  

}

public class B{
  @Autowired
  A a;  

}

或者

public class A{
   public A(B b){
   }
}

public class B{
   public B(A a){
   }
}

也可以是

public class A{
   public A(B b){
   }
}

public class B{
  @Autowired
  A a;  
}

如何解決

循環依賴只存在于singleton類型bean之間

對于構造函數循環依賴的情況,spring無能為力,在獲取bean前spring中會通過下面代碼拋出異常

    protected void beforeSingletonCreation(String beanName) {
        if (!this.inCreationCheckExclusions.contains(beanName) && !this.singletonsCurrentlyInCreation.add(beanName)) {
            throw new BeanCurrentlyInCreationException(beanName);
        }
    }

singletonsCurrentlyInCreation會保存當前正在構造中的beanName,錯誤產生邏輯大概如下:

  1. 我們向BeanFactory獲取A類型的bean
  2. A類型bean準備構造,把beanName保存到singletonsCurrentlyInCreation
  3. A類型通過構造函數實例化,依賴B類型的Bean,向BeanFactory請求B類型Bean
  4. B類型bean準備構造,把beanName保存到singletonsCurrentlyInCreation
  5. B類型通過構造函數實例化,依賴A類型的Bean,向BeanFactory請求A類型Bean
  6. 在DefaultSingletonBeanRegistry#getSingleton#beforeSingletonCreation方法的檢查singletonsCurrentlyInCreation是否已經包含當前請求的beanName,拋出異常

而對于字段注入類型或者字段注入和構造函數混合的循環依賴,spring通過緩存解決這個問題。因為其中一個對象是可實例化的!!

我們以字段注入類型的循環依賴為例

  1. 我們向BeanFactory獲取A類型的bean
  2. A類型bean實例化,把未初始化的自己放到緩存中
  3. A類型bean進行構造(populdateBean),觸發了依賴注入B
  4. 我們向BeanFactory獲取B類型的bean
  5. B類型bean實例化,把未初始化的自己放到緩存中
  6. B類型bean進行構造(populdateBean),觸發了依賴注入A
  7. 從二級緩存中獲取到未初始化的A
  8. B類型bean進行初始化,返回給第3步的A進行依賴注入
  9. A類型bean進行初始化
  10. 返回給調用getBean的方法

這個緩存我們稱它為三級緩存,它的代碼如下,會在doGetBean的開頭被調用

//DefaultSingletonBeanRegistry#getSingleton
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
    //singletonObjects 第一級緩存,BeanFactory的單例全存在singletonObjects中
    //保存的是已經初始化完全的單例
    Object singletonObject = this.singletonObjects.get(beanName);
            //isSingletonCurrentlyInCreation=true,代表beanName所代表的bean循環依賴了
    if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
        synchronized (this.singletonObjects) {
            //第二級緩存,保存的是未初始化完全的單例(只是實例化)
            singletonObject = this.earlySingletonObjects.get(beanName);
            //allowEarlyReference在當前場景下,默認為true
            if (singletonObject == null && allowEarlyReference) {
                //第三級緩存,不是真的緩存,緩存的是生成二級緩存的工廠方法
                ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
                if (singletonFactory != null) {
                    //通過三級緩存構造二級緩存
                    singletonObject = singletonFactory.getObject();
                    this.earlySingletonObjects.put(beanName, singletonObject);
                    this.singletonFactories.remove(beanName);
                }
            }
        }
    }
    return (singletonObject != NULL_OBJECT ? singletonObject : null);
}

在我們實例化bean之后,會把獲取當前bean的方式放入到三級緩存

//AbstractAutowireCapableBeanFactory#doCreateBean
boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
        isSingletonCurrentlyInCreation(beanName));
if (earlySingletonExposure) {
    if (logger.isDebugEnabled()) {
        logger.debug("Eagerly caching bean '" + beanName +
                "' to allow for resolving potential circular references");
    }
    //添加三級緩存
    addSingletonFactory(beanName, new ObjectFactory<Object>() {
        @Override
        public Object getObject() throws BeansException {
            return getEarlyBeanReference(beanName, mbd, bean);
        }
    });
}

具體三級緩存構造二級緩存的邏輯如下

protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
    Object exposedObject = bean;
    if (bean != null && !mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
        for (BeanPostProcessor bp : getBeanPostProcessors()) {
            if (bp instanceof SmartInstantiationAwareBeanPostProcessor) {
                SmartInstantiationAwareBeanPostProcessor ibp = (SmartInstantiationAwareBeanPostProcessor) bp;
                //這邊可能返回的exposedObject可能不是之前的bean了,生成代理時目前的應用場景
                exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName);
                if (exposedObject == null) {
                    return null;
                }
            }
        }
    }
    return exposedObject;
}

在返回bean前,可能會通過SmartInstantiationAwareBeanPostProcessor#getEarlyBeanReference處理一下bean,返回修改后的exposedObject。

這邊是重點?用于解答為啥不是二級緩存而是三級緩存。

因為SmartInstantiationAwareBeanPostProcessor#getEarlyBeanReference的實現為AbstractAutoProxyCreator

public Object getEarlyBeanReference(Object bean, String beanName) throws BeansException {
    Object cacheKey = getCacheKey(bean.getClass(), beanName);
    //冪等,防止重復生成代理
    if (!this.earlyProxyReferences.contains(cacheKey)) {
        this.earlyProxyReferences.add(cacheKey);
    }
    return wrapIfNecessary(bean, beanName, cacheKey);
}

上面的代碼用于提前對bean生成代理。

按照正常的依賴注入,注入的bean,如果被切面切了,會通過postProcessAfterInitialization轉換為代理對象。而對于循環依賴,會把未初始化完全的bean提前注入,但是可能bean可能是被切面切中的,所以使用第三級緩存中的getEarlyBeanReference發揮作用了,用于提前對未初始化的bean生成代理。

這邊有個問題,提前對未初始化的bean生成代理,會不會影響該bean的正常初始化?

不會。代理對象引用了我們目標bean,目標bean的引用也還是被doCreateBean方法持有的。所以目標bean的初始化還是照常進行。

那么目標bean在執行到postProcessAfterInitialization鉤子的時候,會不會重復生成代理?

不會,AbstractAutoProxyCreator#getEarlyBeanReference中使用earlyProxyReferences做了冪等。

在doGetBean中一直有段代碼看不懂它的意圖,現在也找到答案了

if (earlySingletonExposure) {
    Object earlySingletonReference = getSingleton(beanName, false);
    if (earlySingletonReference != null) {
        if (exposedObject == bean) {
            //用于將普通bean替換為它的代理對象
            exposedObject = earlySingletonReference;
        }
        //走到這里說明有其他鉤子把bean替換了,所以要檢查在此之前是否已經發生過該bean的依賴注入,如果發生,就導致一個bean的不同版本被注入,針對這種情況,會拋出異常
        else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)) {
            String[] dependentBeans = getDependentBeans(beanName);
            Set<String> actualDependentBeans = new LinkedHashSet<String>(dependentBeans.length);
            for (String dependentBean : dependentBeans) {
                if (!removeSingletonIfCreatedForTypeCheckOnly(dependentBean)) {
                    actualDependentBeans.add(dependentBean);
                }
            }
            if (!actualDependentBeans.isEmpty()) {
                throw new BeanCurrentlyInCreationException(beanName,
                        "Bean with name '" + beanName + "' has been injected into other beans [" +
                        StringUtils.collectionToCommaDelimitedString(actualDependentBeans) +
                        "] in its raw version as part of a circular reference, but has eventually been " +
                        "wrapped. This means that said other beans do not use the final version of the " +
                        "bean. This is often the result of over-eager type matching - consider using " +
                        "'getBeanNamesOfType' with the 'allowEagerInit' flag turned off, for example.");
            }
        }
    }
}

善始善終,當循環依賴的bean構造好之后,他們的本體bean應該被放到一級緩存中,用于被其他bean獲取。

//DefaultSingletonBeanRegistry#getSingleton#addSingleton
protected void addSingleton(String beanName, Object singletonObject) {
    synchronized (this.singletonObjects) {
        this.singletonObjects.put(beanName, (singletonObject != null ? singletonObject : NULL_OBJECT));
        this.singletonFactories.remove(beanName);
        this.earlySingletonObjects.remove(beanName);
        this.registeredSingletons.add(beanName);
    }
}
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,572評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,071評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,409評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,569評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,360評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,895評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,979評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,123評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,643評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,559評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,742評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,250評論 5 356
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,981評論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,363評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,622評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,354評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,707評論 2 370

推薦閱讀更多精彩內容