從源碼分析Spring是如何解決循環(huán)依賴的

循環(huán)依賴問題

什么是循環(huán)依賴

首先看一下下面的Spring配置文件

<!-- beanA依賴于beanB -->
<bean id="beanA" class="top.okay3r.ClassA">
    <property name="beanB" ref="beanB"/>
</bean>
<!-- beanB依賴于beanA -->
<bean id="beanB" class="top.okay3r.ClassB">
    <property name="beanA" ref="beanA"/>
</bean>

當IOC容器讀取上面的配置時蛙吏,就會先對beanA進行加載;在對beanA進行屬性填充時鞋吉,會發(fā)現(xiàn)beanA依賴于beanB鸦做,然后就會對beanB進行加載;當對beanB進行屬性填充時谓着,又會發(fā)現(xiàn)beanB依賴于beanA泼诱,于是就加載beanA...
可以想到,如果Spring的容器對于這種循環(huán)依賴問題不作出響應的處理赊锚,那么就會無限執(zhí)行上面的過程治筒。最終的結果就可能造成OOM從而導致程序崩潰


WX20200213-173232@2x.png

Spring中bean注入的方式

我們知道在Spring中屉栓,注入bean的方式有【構造器注入】和【setter注入】兩種方式。但在我們使用Spring管理bean時耸袜,可能會遇到一種特殊的情況系瓢,那么就是上面所說的循環(huán)依賴問題
我們再看一下Spring創(chuàng)建bean的過程

Spring創(chuàng)建bean的過程

如果閱讀過IOC相關的源碼就會知道,創(chuàng)建bean的過程大體可以分為初始化bean句灌,對bean的屬性進行填充夷陋,對bean進行初始化三個步驟

  • 初始化bean:即new一個bean實例,是通過反射調(diào)用構造器實現(xiàn)的
  • 對bean的屬性進行填充:可以理解為對<property>標簽相應的屬性進行賦值
  • 對bean進行初始化:即調(diào)用事先配置好的init-method方法胰锌,所以可以將一些初始化的行為寫到這個方法中

然后就來分析一下兩種注入方式

構造器注入

在普通的java程序中骗绕,如果已經(jīng)new出了一個對象,我們就知道這個對象已經(jīng)是可用的了资昧,不論它的屬性是否完整酬土。
但在Spring中,創(chuàng)建出來的bean必須要完成三個步驟才能被認為是可用的格带,才會將這個“完整”的bean放入到IOC容器中撤缴。
因為構造器注入是在實例化對象時反射調(diào)用構造器去注入?yún)?shù),所以既然beanA叽唱、beanB的都拿不到完整的依賴屈呕,就會進行無限的循環(huán)調(diào)用,從而無法解決【循環(huán)依賴問題】棺亭。解決辦法就只有是修改依賴關系了

setter注入

再看一下setter注入方式
setter注入方式就是new出一個對象后虎眨,調(diào)用該對象的set方法對屬性進行賦值。此時對象已經(jīng)被new出來了镶摘,只不過是不完整而已嗽桩。
如果出現(xiàn)了循環(huán)依賴的問題,這就要比構造器注入的方式好的多
所以Spring對于循環(huán)依賴問題的解決就是針對于setter方法的

接下來就開始分析Spring是如何解決循環(huán)依賴問題的

Spring對于循環(huán)依賴的解決

先提前知道一下問題大概是怎樣解決的

首先我們要知道凄敢,Spring對于循環(huán)依賴的問題是采用【緩存】的方式解決的
看一下Spring源碼中的DefaultSingletonBeanRegistry類(注:SingletonBeanRegistry接口提供了關于訪問單例bean的功能碌冶,DefaultSingletonBeanRegistry就是該接口的默認實現(xiàn))

    /** Cache of singleton objects: bean name to bean instance. */
    // 用于存儲完整的bean,接下來稱之為【一級緩存】
    private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

    /** Cache of early singleton objects: bean name to bean instance. */
    // 用于存儲不完整的bean涝缝,即只是new出來扑庞,并沒有屬性值的bean,接下來稱之為【二級緩存】
    private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);

    /** Cache of singleton factories: bean name to ObjectFactory. */
    //用于存儲bean工廠對象俊卤,接下來稱之為【三級緩存】
    private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);

大概捋一遍bean的獲取嫩挤、創(chuàng)建過程

因為循環(huán)依賴都是產(chǎn)生在獲取bean時害幅,所以我們直接從AbstractBeanFactory的getBean()方法開始

  1. AbstractBeanFactory#getBean()沒什么自身的實現(xiàn)消恍,只調(diào)用了doGetBean()
  2. AbstractBeanFactory#doGetBean(),在這個方法中調(diào)用了getSingleton(beanName)獲取實例:Object sharedInstance = getSingleton(beanName);
  3. 判斷sharedInstance是否為null,如果不為null則調(diào)用getObjectForBeanInstance處理以现,然后返回狠怨。也就是IOC容器獲取bean成功约啊,可以拿去使用了发框。如果sharedInstance為null拙徽,則調(diào)用getSingleton(beanName,Object{...})方法
  4. DefaultSingletonBeanRegistry#getSingleton中,首先會從【一級緩存】中get一下bean谦去,如果獲取不到憎蛤,則會進入創(chuàng)建bean的流程
  5. 創(chuàng)建bean的主要邏輯就是走AbstractAutowireCapableBeanFactory#doCreateBean外傅,先是使用createBeanInstance方法創(chuàng)建bean的實例,然后對bean進行初始化俩檬,再進行屬性填充....然后返回bean
  6. 獲取到bean萎胰,完成

上面并沒有涉及到循環(huán)依賴和二級、三級緩存的問題棚辽,因為對于循環(huán)依賴的處理技竟,都表現(xiàn)在代碼中的細節(jié)之處

對應上面的過程,從源碼上開始分析

首先看doGetBean方法

protected <T> T doGetBean(final String name, @Nullable final Class<T> requiredType,
            @Nullable final Object[] args, boolean typeCheckOnly) throws BeansException{
        // 從緩存中獲取單例bean
        Object sharedInstance = getSingleton(beanName);
        if (sharedInstance != null && args == null) { //如果獲取到單例bean屈藐,則走下面代碼
            //......
            bean = getObjectForBeanInstance(sharedInstance, name, beanName, null);
        }else {//如果沒有獲取到單例bean榔组,則走下面代碼   
                //......        
                // 如果是單例的Bean,請下面的代碼
                if (mbd.isSingleton()) {
                    sharedInstance = getSingleton(beanName, () -> {
                        try {
                            // 創(chuàng)建單例Bean的主要方法联逻,返回的bean是完整的
                            return createBean(beanName, mbd, args);
                        }
                    });
                    bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
                }
                //......
        }
        return (T) bean;
    }
}

上面的代碼中搓扯,sharedInstance是通過getSingleton()方法獲得的,實際上getSingleton(beanName)方法沒什么邏輯包归,內(nèi)部調(diào)用了getSingleton(beanName, boolean)這個方法擅编,所以接下來就進入到這個方法中

getSingleton(beanName, boolean)的實現(xiàn)

protected Object getSingleton(String beanName, boolean allowEarlyReference) {
        // 從一級緩存中獲取單例對象
        Object singletonObject = this.singletonObjects.get(beanName);
        // isSingletonCurrentlyInCreation : 判斷當前單例bean是否正在創(chuàng)建中,也就是沒有初始化完成箫踩。比如beanA的構造器依賴了beanB對象所以得先去創(chuàng)建B對象爱态,或者在A的populateBean過程中依賴了B對象,得先去創(chuàng)建B對象境钟,這時的beanA就是處于創(chuàng)建中的狀態(tài)
        if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
            synchronized (this.singletonObjects) {
                // 從二級緩存中獲取單例bean
                singletonObject = this.earlySingletonObjects.get(beanName);
                // allowEarlyReference :是否允許從singletonFactories中通過getObject拿到對象
                if (singletonObject == null && allowEarlyReference) {
                    // 從三級緩存中獲取單例bean
                    ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
                    if (singletonFactory != null) {
                        // 通過單例工廠獲取單例bean
                        singletonObject = singletonFactory.getObject();
                        // 從三級緩存移動到了二級緩存锦担,并移除singletonFactory
                        this.earlySingletonObjects.put(beanName, singletonObject);
                        this.singletonFactories.remove(beanName);
                    }
                }
            }
        }
        return singletonObject;
    }

從上面的代碼中可以總結出以下幾點:

  1. 先從【一級緩存】中查找,有則直接返回
  2. 如果在【一級緩存】中獲取不到慨削,并且對象正在創(chuàng)建中(beanName包含在singletonsCurrentlyInCreation)洞渔,那么就再從【二級緩存】中查找,有則直接返回
  3. 如果還是獲取不到缚态,且允許singletonFactories(allowEarlyReference=true)通過getObject()獲取磁椒,就從【三級緩存】中獲取(singletonFactory.getObject())玫芦。通過ObjectFactory獲取到的對象浆熔,是進行代理后的對象(假設有AOP)。將從【三級緩存】中獲取到的對象放到【二級緩存】中桥帆,同時刪除此beanName對應的【三級緩存數(shù)據(jù)】

再看一下doGetBean()方法中剛剛沒有講到的“if-else”部分

如果getSingleton()方法獲取到了bean医增,即sharedInstance不為null慎皱,則對其進行處理然后返回
如果sharedInstance為null,就要走else中的代碼了
首先判斷一下是否為單例茫多,(mbd是通過讀取配置文件中bean標簽生成的bean的定義信息,具體獲得的方法這里不詳細說了)忽刽。因為多例的bean是不需要放入到IOC容器中的天揖,所以這里只處理單例bean
如果為單例,則調(diào)用getSingleton(String beanName, ObjectFactory<?> singletonFactory)方法

getSingleton(String beanName, ObjectFactory<?> singletonFactory)方法

public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
    synchronized (this.singletonObjects) {
        // ......
        // 創(chuàng)建 bean 實例
        singletonObject = singletonFactory.getObject();
        newSingleton = true;
        if (newSingleton) {
            // 添加新創(chuàng)建的bean添加到【一級緩存】中跪帝,并刪除其他緩存中對應的bean
            addSingleton(beanName, singletonObject);
        }
        // ......
        // 返回 singletonObject
        return (singletonObject != NULL_OBJECT ? singletonObject : null);
    }
}

protected void addSingleton(String beanName, Object singletonObject) {
    synchronized (this.singletonObjects) {
        // 將新創(chuàng)建的bean添加到【一級緩存】中
        this.singletonObjects.put(beanName, (singletonObject != null ? singletonObject : NULL_OBJECT));

        // 從其他緩存中移除相關的bean
        this.singletonFactories.remove(beanName);
        this.earlySingletonObjects.remove(beanName);
        this.registeredSingletons.add(beanName);
    }
}

上面的代碼主要包含了兩個功能

  1. 獲取完整的bean實例
  2. 將新的bean添加到【一級緩存】中宝剖,以后getBean的時候就可以直接獲取了

可以看到bean實例是由singletonFactory.getObject()拿到的,也就是通過doGetBean()方法中判斷是否單例后的匿名內(nèi)部類獲取到的歉甚,從而知道獲取到的bean是由createBean()方法創(chuàng)建的

creatBean()方法調(diào)用了doCreatBean()方法万细,所以實際的創(chuàng)建邏輯就再doCreatBean()中

doCreatBean()

protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args)
            throws BeanCreationException {

        // 默認調(diào)用無參構造實例化Bean
        // 構造方法的依賴注入,就是發(fā)生在這一步
        if (instanceWrapper == null) {
            instanceWrapper = createBeanInstance(beanName, mbd, args);
        }
        // 實例化后的Bean對象纸泄,這里獲取到的是一個原始對象赖钞,即沒有進行屬性填充的對象
        final Object bean = instanceWrapper.getWrappedInstance();
        Class<?> beanType = instanceWrapper.getWrappedClass();
        
        //......

        // 解決循環(huán)依賴的關鍵步驟
        // earlySingletonExposure:是否”提前暴露“原始對象的引用
        // 因為不論這個bean是否完整,他前后的引用都是一樣的聘裁,所以提前暴露的引用到后來也指向完整的bean
        boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
                isSingletonCurrentlyInCreation(beanName));
        // 如果需要提前暴露單例bean雪营,則將該bean工廠放入【三級緩存】中
        if (earlySingletonExposure) {
            // 將剛創(chuàng)建的bean工廠放入三級緩存中singleFactories(key是beanName,value是FactoryBean)
            // 同樣也會移除【二級緩存】中對應的bean衡便,即便沒有
            addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
        }

        // Initialize the bean instance.
        Object exposedObject = bean;
        try {
            //填充屬性(依賴注入)
            populateBean(beanName, mbd, instanceWrapper);
            //調(diào)用初始化方法献起,完成bean的初始化操作
            exposedObject = initializeBean(beanName, exposedObject, mbd);
        }
        //......

        return exposedObject;
    }

ok,看到這里镣陕,整個在有循環(huán)依賴問題下創(chuàng)建谴餐、獲取bean的流程就結束了
舉個例子,從頭串一下流程呆抑。假設beanA->beanB, beanB->beanA岂嗓,即A、B相互依賴

  1. 調(diào)用doGetBean()方法鹊碍,想要獲取beanA厌殉,于是調(diào)用getSingleton()方法從緩存中查找beanA
  2. 在getSingleton()方法中,從一級緩存中查找侈咕,沒有公罕,返回null
  3. doGetBean()方法中獲取到的beanA為null,于是走對應的處理邏輯耀销,調(diào)用getSingleton()的重載方法(參數(shù)為ObjectFactory的)
    (ps:現(xiàn)在是2020.2.14 凌晨1點07分楼眷,情人節(jié)了,因為疫情不能和小楊一起,在我的第一篇博客中紀念一下這個節(jié)日??摩桶,祝所有人情人節(jié)快樂)
  4. 在getSingleton()方法中桥状,先將beanA_name添加到一個集合中帽揪,用于標記該bean正在創(chuàng)建中硝清。然后回調(diào)匿名內(nèi)部類的creatBean方法
  5. 進入AbstractAutowireCapableBeanFactory#doCreateBean,先反射調(diào)用構造器創(chuàng)建出beanA的實例转晰,然后判斷:是否為單例芦拿、是否允許提前暴露引用(對于單例一般為true)、是否正在創(chuàng)建中(即是否在第四步的集合中)查邢。判斷為true蔗崎,則將beanA添加到【三級緩存】中
  6. 對beanA進行屬性填充,此時檢測到beanA依賴于beanB扰藕,于是開始查找beanB
  7. 調(diào)用doGetBean()方法缓苛,和上面beanA的過程一樣,到緩存中查找beanB邓深,沒有則創(chuàng)建未桥,然后給beanB填充屬性
  8. 此時beanB依賴于beanA,調(diào)用getSingleton()獲取beanA芥备,依次從一級冬耿、二級、三級緩存中找萌壳,此時從三級緩存中獲取到beanA的創(chuàng)建工廠,通過創(chuàng)建工廠獲取到singletonObject袱瓮,此時這個singletonObject指向的就是上面在doCreateBean()方法中實例化的beanA
  9. 這樣beanB就獲取到了beanA的依賴,于是beanB順利完成實例化尺借,并將beanA從三級緩存移動到二級緩存中
  10. 隨后beanA繼續(xù)他的屬性填充工作,此時也獲取到了beanB褐望,beanA也隨之完成了創(chuàng)建,回到getSingleton()方法中繼續(xù)向下執(zhí)行瘫里,將beanA從二級緩存移動到一級緩存中

最后

整個過程大概就是這樣了,由于spring的源碼比較多谨读,就只挑選了重點部分進行注釋
其實主要思想就是利用二級、三級緩存對未初始化完成的bean進行提前的引用暴露,也就是將其設置為可引用的铐尚,這樣當依賴于他的bean在進行屬性填充時就可以直接拿到引用,解決了死循環(huán)的問題
============================================================
還有幾個比較重要的點宣增,在這里指出位置玫膀,可以根據(jù)這些去查找看

  • 這三個級別的緩存,在同一時間爹脾,同一beanName對應的bean只會存在于一個緩存中
  • 如果沒有循環(huán)依賴的問題帖旨,二級、三級緩存是沒有用處的灵妨,體現(xiàn)在AbstractAutowireCapableBeanFactory#doCreateBean的判斷earlySingletonExposure這個地方
  • 判斷循環(huán)依賴解阅,是用一個Set集合實現(xiàn)的,正在創(chuàng)建中的beanName會加到這個集合中
  • 三級緩存其實還有創(chuàng)建AOP代理的功能泌霍,在AbstractAutowireCapableBeanFactory#createBean調(diào)用resolveBeforeInstantiation的位置货抄。而如果沒有循環(huán)依賴問題,那么代理就是在調(diào)用init-method過程中創(chuàng)建的
  • bean實例化之后朱转,屬性填充之前蟹地,如果有循環(huán)依賴,就將這個bean封裝到一個ObjectFactory然后放到三級緩存中(為了提前暴露引用)
  • 三級緩存中的ObjectFactory第一次拿出被他保存bean后肋拔,這個bean就會進入二級緩存
  • bean被創(chuàng)建完整后锈津,進入一級緩存

》》》》》》》》》》》》》》》》》》》》》

有些東西不知道怎么轉述成語言表達出來,還有如果有不好的或者說錯的地方希望看過的大佬能幫忙指正凉蜂,謝謝~~

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末琼梆,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子窿吩,更是在濱河造成了極大的恐慌茎杂,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,406評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件纫雁,死亡現(xiàn)場離奇詭異煌往,居然都是意外死亡,警方通過查閱死者的電腦和手機轧邪,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,732評論 3 393
  • 文/潘曉璐 我一進店門刽脖,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人忌愚,你說我怎么就攤上這事曲管。” “怎么了硕糊?”我有些...
    開封第一講書人閱讀 163,711評論 0 353
  • 文/不壞的土叔 我叫張陵院水,是天一觀的道長腊徙。 經(jīng)常有香客問我,道長檬某,這世上最難降的妖魔是什么撬腾? 我笑而不...
    開封第一講書人閱讀 58,380評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮恢恼,結果婚禮上厅瞎,老公的妹妹穿的比我還像新娘和簸。我一直安慰自己碟刺,他們只是感情好半沽,可當我...
    茶點故事閱讀 67,432評論 6 392
  • 文/花漫 我一把揭開白布浩村。 她就那樣靜靜地躺著占哟,像睡著了一般榨乎。 火紅的嫁衣襯著肌膚如雪蜜暑。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,301評論 1 301
  • 那天隐绵,我揣著相機與錄音依许,去河邊找鬼恬偷。 笑死,一個胖子當著我的面吹牛坦康,可吹牛的內(nèi)容都是我干的滞欠。 我是一名探鬼主播,決...
    沈念sama閱讀 40,145評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼逸绎,長吁一口氣:“原來是場噩夢啊……” “哼棺牧!你這毒婦竟也來了颊乘?” 一聲冷哼從身側響起醉锄,我...
    開封第一講書人閱讀 39,008評論 0 276
  • 序言:老撾萬榮一對情侶失蹤檩小,失蹤者是張志新(化名)和其女友劉穎烟勋,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體颓哮,經(jīng)...
    沈念sama閱讀 45,443評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,649評論 3 334
  • 正文 我和宋清朗相戀三年姨伤,在試婚紗的時候發(fā)現(xiàn)自己被綠了庸疾。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片届慈。...
    茶點故事閱讀 39,795評論 1 347
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖鲤桥,靈堂內(nèi)的尸體忽然破棺而出渠概,到底是詐尸還是另有隱情播揪,我是刑警寧澤猪狈,帶...
    沈念sama閱讀 35,501評論 5 345
  • 正文 年R本政府宣布罪裹,位于F島的核電站状共,受9級特大地震影響谁帕,放射性物質發(fā)生泄漏。R本人自食惡果不足惜碾牌,卻給世界環(huán)境...
    茶點故事閱讀 41,119評論 3 328
  • 文/蒙蒙 一舶吗、第九天 我趴在偏房一處隱蔽的房頂上張望择膝。 院中可真熱鬧肴捉,春花似錦、人聲如沸傲隶。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,731評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至洛二,卻和暖如春晾嘶,著一層夾襖步出監(jiān)牢的瞬間娶吞,已是汗流浹背妒蛇。 一陣腳步聲響...
    開封第一講書人閱讀 32,865評論 1 269
  • 我被黑心中介騙來泰國打工吏奸, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留陶耍,地道東北人烈钞。 一個月前我還...
    沈念sama閱讀 47,899評論 2 370
  • 正文 我出身青樓馒过,卻偏偏與公主長得像酗钞,于是被迫代替她去往敵國和親算吩。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,724評論 2 354

推薦閱讀更多精彩內(nèi)容