一步一步帶你理解 Spring 循環(huán)依賴

寫(xiě)在開(kāi)頭

學(xué)習(xí) Spring 的過(guò)程當(dāng)中,對(duì) Spring 的循環(huán)依賴大致明白了,可是自己再仔細(xì)跟蹤源碼,卻又總差點(diǎn)意思,似懂非懂就很煩躁,然后就埋頭苦干,一定要自己弄清楚,就肝下了這篇文章,也希望看了這篇文章的朋友能有所收獲。

大家也可以關(guān)注我的公眾號(hào): 漿果捕鼠草,文章也會(huì)同步更新,當(dāng)然,公眾號(hào)還會(huì)有一些資源可以分享給大家~

由于排版的原因,?????? 關(guān)注我的公眾號(hào):漿果捕鼠草,發(fā)送關(guān)鍵字: 循環(huán)依賴時(shí)序圖
即可獲得超高清時(shí)序圖!

? Spring 循環(huán)依賴

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

舉個(gè)栗子??

/**
 * A 類,引入 B 類的屬性 b
 */
public class A {
  private B b;
}
/**
 * B 類,引入 A 類的屬性 a
 */
public class B {
  private A a;
}

再看個(gè)簡(jiǎn)單的圖:

在這里插入圖片描述

像這樣,創(chuàng)建 a 的時(shí)候需要依賴 b,那就創(chuàng)建 b,結(jié)果創(chuàng)建 b 的時(shí)候又需要依賴 a,那就創(chuàng)建 a,創(chuàng)建 a 的時(shí)候需要依賴 b,那就創(chuàng)建 b,結(jié)果創(chuàng)建 b 的時(shí)候又需要依賴 a ......

??互相依賴何時(shí)了,死循環(huán)了吧?
??諾,這就是循環(huán)依賴!


循環(huán)依賴其實(shí)不算個(gè)問(wèn)題或者錯(cuò)誤,我們實(shí)際在開(kāi)發(fā)的時(shí)候,也可能會(huì)用到。
再拿最開(kāi)始的 A 和 B 來(lái)說(shuō),我們手動(dòng)使用的時(shí)候會(huì)用以下方式:

  A a = new A();
  B b = new B();
  b.setA(a);
  a.setB(b);

其實(shí)這樣就解決了循環(huán)依賴,功能上是沒(méi)有問(wèn)題的,但是為什么 Spring 要解決循環(huán)依賴?

?? 為什么 Spring 要解決循環(huán)依賴?

首先簡(jiǎn)單了解下,我們用 Spring 框架,它幫我們做了什么事情?總結(jié)性來(lái)說(shuō),六字真言:IoC 和 AOP

由于 Spring 解決循環(huán)依賴是考慮到 IoC 和 AOP 相關(guān)知識(shí)了,所以這里我先提一下。

由于本文主要的核心是 Spring 的循環(huán)依賴處理,所以不會(huì)對(duì) IoC 和 AOP 做詳細(xì)的說(shuō)明,想了解以后有機(jī)會(huì)再說(shuō) ??

IoC,主要是將對(duì)象的創(chuàng)建、管理都交給了 Spring 來(lái)管理,能夠解決對(duì)象之間的耦合問(wèn)題,對(duì)開(kāi)發(fā)人員來(lái)說(shuō)也是省時(shí)省力的。

AOP,主要是在不改變?cè)袠I(yè)務(wù)邏輯情況下,增強(qiáng)橫切邏輯代碼,也是解耦合,避免橫切邏輯代碼重復(fù);也是對(duì) OOP 的延續(xù)、補(bǔ)充。

既然類的實(shí)例化都交給了 Spring 來(lái)管理了,那么循環(huán)依賴 Spring 肯定也要考慮到怎么去處理(怎么總覺(jué)得有點(diǎn)像是廢話 ??)。

?? 解決循環(huán)依賴的方式

參考我們能想到的肯定是手動(dòng)處理的方式,先將對(duì)象都 new 出來(lái),然后進(jìn)行 set 屬性值,而 Spring 也是通過(guò)這樣的形式來(lái)處理的(你說(shuō)巧不巧?其實(shí)一點(diǎn)都不巧 ??,后面再說(shuō)為什么),其實(shí) Spring 管理 Bean 的實(shí)例化底層其實(shí)是由反射實(shí)現(xiàn)的。

而我們實(shí)例化的方式也有好多種,比如通過(guò)構(gòu)造函數(shù),一次性將屬性賦值,像下面這樣

// 假設(shè)有學(xué)生這個(gè)類
public class Student {
  private int id;
  private String name;

  public Student(int id, String name) {
    this.id = id;
    this.name = name;
  }
}

// 通過(guò)構(gòu)造器方式實(shí)例化并賦值
new Student(1, "Suremotoo");

但是使用構(gòu)造器這樣的方式,是無(wú)法解決循環(huán)依賴的!為什么不能呢?

我們還是以文中開(kāi)頭的 A 和 B 互相依賴來(lái)說(shuō), 要通過(guò)構(gòu)造器的方式實(shí)現(xiàn) A 的實(shí)例化,如下

new A(b);

?? Wow,是不是發(fā)現(xiàn)問(wèn)題了?要通過(guò)構(gòu)造器的方式,首先要將屬性值實(shí)例化出來(lái)??!A 要依賴屬性 b,就需要先將 B 實(shí)例化,可是 B 的實(shí)例化是不是還是需要依賴 A?? 這不就是文中開(kāi)頭描述的樣子嘛,所以通過(guò)構(gòu)造器的方式,Spring 也沒(méi)有辦法解決循環(huán)依賴

我們使用 set 可以解決,那么 Spring 也使用 set 方式呢?答案是可以的。

既然底層是通過(guò)反射實(shí)現(xiàn)的,我們自己也用反射實(shí)現(xiàn)的話,大概思路是這樣的(還是以 A 和 B 為例)

  1. 先實(shí)例化 A 類

  2. 再實(shí)例化 B 類

  3. set B 類中的 a 屬性

  4. set A 類中的 b 屬性

其實(shí)就是通過(guò)反射,實(shí)現(xiàn)以下代碼

      A a = new A();
      B b = new B();
      b.setA(a);
      a.setB(b);

這里可以稍微說(shuō)明一下,為什么這樣可以?

A a = new A(),說(shuō)明 A 只是實(shí)例化,還未初始化

同理,B b = new B(),也只是實(shí)例化,并未初始化

a.setB(b);, 對(duì) a 的屬性賦值,完成 a 的初始化

b.setA(a);, 對(duì) b 的屬性賦值,完成 b 的初始化

現(xiàn)在是不是有點(diǎn)感覺(jué)了,先把狗騙進(jìn)來(lái),再殺 ??

Spring 如何解決循環(huán)依賴問(wèn)題

先上個(gè)通俗的答案解釋,三級(jí)緩存。

    /**
     * 單例對(duì)象的緩存:bean 名稱——bean 實(shí)例,即:所謂的單例池。
     * 表示已經(jīng)經(jīng)歷了完整生命周期的 Bean 對(duì)象
     * <b>第一級(jí)緩存</b>
     */
    Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

    /**
     * 早期的單例對(duì)象的高速緩存:bean 名稱——bean 實(shí)例。
     * 表示 Bean 的生命周期還沒(méi)走完(Bean 的屬性還未填充)就把這個(gè) Bean 存入該緩存中
     * 也就是實(shí)例化但未初始化的 bean 放入該緩存里
     * <b>第二級(jí)緩存</b>
     */
    Map<String, Object> earlySingletonObjects = new HashMap<>(16);
    
    /**
     * 單例工廠的高速緩存:bean 名稱——ObjectFactory。
     * 表示存放生成 bean 的工廠
     * <b>第三級(jí)緩存</b>
     */
    Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);

代碼中注釋可能不清晰,我再給貼一下三級(jí)緩存:

第一級(jí)緩存(也叫單例池)Map<String, Object> singletonObjects,存放已經(jīng)經(jīng)歷了完整生命周期的 Bean 對(duì)象

第二級(jí)緩存Map<String, Object> earlySingletonObjects,存放早期暴露出來(lái)的 Bean 對(duì)象,Bean 的生命周期未結(jié)束(屬性還未填充完)

第三級(jí)緩存Map<String, ObjectFactory<?>> singletonFactories,存放可以生成 Bean 的工廠

Spring 管理的 Bean 其實(shí)默認(rèn)都是單例的,也就是說(shuō) Spring 將最終可以使用的 Bean 統(tǒng)一放入第一級(jí)緩存中,也就是 singletonObjects(單例池)里,以后凡是用到某個(gè) Bean 了都從這里獲取就行了。

僅使用一級(jí)緩存可以嗎?

既然都從 singletonObjects 里獲取,那么<u>僅僅使用這一個(gè) singletonObjects</u>,可以嗎?肯定不可以的。
首先 singletonObjects 存入的是完全初始化好的 Bean,可以拿來(lái)直接用的。
如果我們直接將未初始化完的 Bean 放在 singletonObjects 里面,注意,這個(gè)未初始化完的 Bean 極有可能會(huì)被其他的類拿去用,它都沒(méi)完事呢,就被拿去造了,肯定要出事?。?/strong>

我們以 A 、 B、C 舉栗子??

  1. 先實(shí)例化 A 類,叫 a
  2. 將 a 放入 singletonObjects 中(此時(shí) a 中的 b 屬性還是空的呢)
  3. C 類需要使用 A 類,去 singletonObjects 獲取,且獲取到了 a
  4. C 類使用 a,拿出 a 類的 b 屬性,然后 NPE了.

諾,出事了吧,這下就不是解決循環(huán)依賴的問(wèn)題了,反而設(shè)計(jì)就不對(duì)了。

NPE 就是 NullPointerException

使用二級(jí)緩存可以嗎?

再來(lái)回顧下循環(huán)依賴的問(wèn)題:A→B→A→B......

說(shuō)到底就是怎么打破這個(gè)循環(huán),一級(jí)緩存不行,我們就再加一級(jí),可以嗎?
我們看個(gè)圖

在這里插入圖片描述

圖中的緩存就是二級(jí)緩存

看完圖,可能還會(huì)有疑惑,A 沒(méi)初始化完成放入了緩存,那么 B 用的豈不是就是未完成的 A,是這樣的沒(méi)錯(cuò)!
在整個(gè)過(guò)程當(dāng)中,A 是只有1 個(gè),而 B 那里的 A 只是 A 的引用,所以后面 A 完成了初始化,B 中的 A 自然也就完成了。這里就是文中前面提到的手動(dòng) setA,setB那里,我再貼一下代碼:

  A a = new A();
  B b = new B();
  b.setA(a); // 這里設(shè)置 b 的屬性 a,其實(shí)就是 a 的引用
  a.setB(b); // 這里設(shè)置 a 的屬性 b,此時(shí)的 b 已經(jīng)完成了初始化,設(shè)置完 a 的屬性, a 也就完成了初始化,那么對(duì)應(yīng)的 b 也就完成了初始化

分析到這里呢,我們就會(huì)發(fā)現(xiàn)二級(jí)緩存就解決了循環(huán)依賴的問(wèn)題了,可是為什么還要三級(jí)緩存呢?
這里就要說(shuō)說(shuō) Spring 中 Bean 的生命周期。

Spring 中 Bean 的管理

在這里插入圖片描述

要明白 Spring 中的循環(huán)依賴,首先得了解下 Spring 中 Bean 的生命周期。

被 Spring 管理的對(duì)象叫 Bean
這里不會(huì)對(duì) Bean 的生命周期進(jìn)行詳細(xì)的描述,只是描述一下大概的過(guò)程,方便大家去理解循環(huán)依賴。

Spring 中 Bean 的生命周期,指的就是 Bean 從創(chuàng)建到銷毀的一系列生命活動(dòng)。

那么由 Spring 來(lái)管理 Bean,要經(jīng)過(guò)的主要步驟有:

  1. Spring 根據(jù)開(kāi)發(fā)人員的配置,掃描哪些類由 Spring 來(lái)管理,并為每個(gè)類生成一個(gè) BeanDefintion,里面封裝了類的一些信息,如全限定類名、哪些屬性、是否單例等等

  2. 根據(jù) BeanDefintion 的信息,通過(guò)反射,去實(shí)例化 Bean(此時(shí)就是實(shí)例化但未初始化 的 Bean)

  3. 填充上述未初始化對(duì)象中的屬性(依賴注入)

  4. 如果上述未初始化對(duì)象中的方法被 AOP 了,那么就需要生成代理類(也叫包裝類)

  5. 最后將完成初始化的對(duì)象存入緩存中(此處緩存 Spring 里叫: singletonObjects),下次用從緩存獲取 ok 了

如果沒(méi)有涉及到 AOP,那么第四步就沒(méi)有生成代理類,將第三步完成屬性填充的對(duì)象存入緩存中。

二級(jí)緩存有什么問(wèn)題?

如果 Bean 沒(méi)有 AOP,那么用二級(jí)緩存其實(shí)沒(méi)有什么問(wèn)題的,一旦有上述生命周期中第四步,就會(huì)導(dǎo)致的一個(gè)問(wèn)題。因?yàn)?AOP 處理后,往往是需要生成代理對(duì)象的,代理對(duì)象和原來(lái)的對(duì)象根本就不是 1 個(gè)對(duì)象。

以二級(jí)緩存的場(chǎng)景來(lái)說(shuō),假設(shè) A 類的某個(gè)方法會(huì)被 AOP,過(guò)程就是這樣的:

二級(jí)緩存問(wèn)題示例圖
  1. 生成 a 的實(shí)例,然后放入緩存,a 需要 b
  2. 再生成 b ,填充 b 的時(shí)候,需要 a,從緩存中取到了 a,完成 b 的初始化;
  3. 緊接著 a 把初始化好的 b 拿過(guò)來(lái)用,完成 a 的屬性填充和初始化
  4. 由于 A 類涉及到了 AOP,再然后 a 要生成一個(gè)代理類,這里就叫:代理 a 吧

結(jié)果就是:a 最終的產(chǎn)物是代理 a,那 b 中其實(shí)也應(yīng)該用代理 a,而現(xiàn)在 b 中用的卻是原始的 a
代理 a 和原始的 a 不是一個(gè)對(duì)象,現(xiàn)在這就有問(wèn)題了。

使用三級(jí)緩存如何解決?

二級(jí)緩存還是有問(wèn)題,那就再加一層緩存,也就是第三級(jí)緩存:Map<String, ObjectFactory<?>> singletonFactories,在 bean 的生命周期中,創(chuàng)建完對(duì)象之后,就會(huì)構(gòu)造一個(gè)這個(gè)對(duì)象對(duì)應(yīng)的 ObjectFactory 存入 singletonFactories 中。

singletonFactories 中存的是某個(gè) beanName 及對(duì)應(yīng)的 ObjectFactory,這個(gè) ObjectFactory 其實(shí)就是生成這個(gè) Bean 的工廠。實(shí)際中,這個(gè) ObjectFactory 是個(gè) Lambda 表達(dá)式:() -> getEarlyBeanReference(beanName, mbd, bean),而且,這個(gè)表達(dá)式<u>并沒(méi)有</u>執(zhí)行

那么 getEarlyBeanReference 具體做了什么事情?

核心就是兩步:

第一步:根據(jù) beanName 將它對(duì)應(yīng)的實(shí)例化后且未初始化完的 Bean,存入 java Map<Object, Object> earlyProxyReferences = new ConcurrentHashMap<>(16);

第二步:生成該 Bean 對(duì)應(yīng)的代理類返回

這個(gè) earlyProxyReferences 其實(shí)就是用于記錄哪些 Bean 執(zhí)行過(guò) AOP,防止后期再次對(duì) Bean 進(jìn)行 AOP

那么 getEarlyBeanReference 什么時(shí)候被觸發(fā),什么時(shí)候執(zhí)行?

在二級(jí)緩存示例中,填充 B 的屬性時(shí)候,需要 A,然后去緩存中拿 A,此時(shí)先去第三級(jí)緩存中去取 A,如果存在,此時(shí)就執(zhí)行 getEarlyBeanReference 函數(shù),然后該函數(shù)就會(huì)返回 A 對(duì)應(yīng)的代理對(duì)象。

后續(xù)再將該代理對(duì)象放入第二級(jí)緩存中,也就是 java Map<String, Object> earlySingletonObjects里。

為什么不放入第一級(jí)緩存呢?

此時(shí)就拿到的代理對(duì)象,也是未填充屬性的,也就是仍然是未初始化完的對(duì)象。

如果直接放入第一級(jí)緩存,此時(shí)被其他類拿去使用,肯定有問(wèn)題了。

那么什么時(shí)候放入第一級(jí)緩存?

這里需要再簡(jiǎn)單說(shuō)下第二級(jí)緩存的作用,假如 A 經(jīng)過(guò)第三級(jí)緩存,獲得代理對(duì)象,這個(gè)代理對(duì)象仍然是未初始化完的!那么就暫時(shí)把這個(gè)代理對(duì)象放入第二級(jí)緩存,然后刪除該代理對(duì)象原本在第三級(jí)緩存中的數(shù)據(jù)(確保后期不會(huì)每次都生成新的代理對(duì)象),后面其他類要用了 A,就去第二級(jí)緩存中找,就獲取到了 A 的代理對(duì)象,而且都用的是同一個(gè) A 的代理對(duì)象,這樣后面只需要對(duì)這一個(gè)代理對(duì)象進(jìn)行完善,其他引入該代理對(duì)象的類就都完善了。

再往后面,繼續(xù)完成 A 的初始化,那么先判斷 A 是否存在于 earlyProxyReferences 中, 存在就說(shuō)明 A 已經(jīng)經(jīng)歷過(guò) AOP 了,就無(wú)須再次 AOP。那 A 的操作就轉(zhuǎn)換從二級(jí)緩存中獲取,把 A 的代理類拿出來(lái),填充代理類的屬性。

完成后再將 A 的代理對(duì)象加入到第一級(jí)緩存,再把它原本在第二級(jí)緩存中的數(shù)據(jù)刪掉,確保后面還用到 A 的類,直接從第一級(jí)緩存中獲取。

看個(gè)圖理解下

在這里插入圖片描述

總結(jié)

說(shuō)了這么多,總結(jié)下三級(jí)緩存:

第一級(jí)緩存(也叫單例池)Map<String, Object> singletonObjects,存放已經(jīng)經(jīng)歷了完整生命周期的 Bean 對(duì)象

第二級(jí)緩存Map<String, Object> earlySingletonObjects,存放早期暴露出來(lái)的 Bean 對(duì)象,Bean 的生命周期未結(jié)束(屬性還未填充完),可能是代理對(duì)象,也可能是原始對(duì)象

第三級(jí)緩存Map<String, ObjectFactory<?>> singletonFactories,存放可以生成 Bean 的工廠,工廠主要用來(lái)生成 Bean 的代理對(duì)象

?? 附: 一個(gè)完整的 Spring 循環(huán)依賴時(shí)序圖

時(shí)序圖并不標(biāo)準(zhǔn),但是方便大家去理解 ??
在理解循環(huán)依賴的時(shí)候,整體是個(gè)遞歸,你要有種套中套、夢(mèng)中夢(mèng)的感覺(jué)

Spring 循環(huán)依賴時(shí)序圖

由于排版的原因,?????? 關(guān)注我的公眾號(hào): 漿果捕鼠草,發(fā)送關(guān)鍵字: 循環(huán)依賴時(shí)序圖
即可獲得超高清時(shí)序圖!

????????另外特殊福利 ?????? 關(guān)注我的公眾號(hào): 漿果捕鼠草,發(fā)送關(guān)鍵字: 循環(huán)依賴精美
即可獲得本文的精美 PDF 版本哦!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 最近面試的時(shí)候發(fā)現(xiàn)很多人會(huì)問(wèn)Spring是如何解決循環(huán)依賴的,雖然知道是通過(guò)三級(jí)緩存去解決的,但是也僅僅只是知其然...
    凱凱雄雄閱讀 851評(píng)論 0 6
  • 一、Spring bean生命周期 可以簡(jiǎn)化為以下5步。 1、構(gòu)建BeanDefinition 2、實(shí)例化 Ins...
    胡峻崢閱讀 1,038評(píng)論 0 0
  • 網(wǎng)上關(guān)于Spring循環(huán)依賴的博客太多了,有很多都分析的很深入,寫(xiě)的很用心,甚至還畫(huà)了時(shí)序圖、流程圖幫助讀者理解,...
    CoderBear閱讀 725評(píng)論 1 8
  • 我是黑夜里大雨紛飛的人啊 1 “又到一年六月,有人笑有人哭,有人歡樂(lè)有人憂愁,有人驚喜有人失落,有的覺(jué)得收獲滿滿有...
    陌忘宇閱讀 8,814評(píng)論 28 54
  • 信任包括信任自己和信任他人 很多時(shí)候,很多事情,失敗、遺憾、錯(cuò)過(guò),源于不自信,不信任他人 覺(jué)得自己做不成,別人做不...
    吳氵晃閱讀 6,355評(píng)論 4 8

友情鏈接更多精彩內(nèi)容