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

寫在開頭

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

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

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

? Spring 循環(huán)依賴

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

舉個栗子??

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

再看個簡單的圖:

在這里插入圖片描述

像這樣,創(chuàng)建 a 的時候需要依賴 b究珊,那就創(chuàng)建 b薪者,結(jié)果創(chuàng)建 b 的時候又需要依賴 a,那就創(chuàng)建 a剿涮,創(chuàng)建 a 的時候需要依賴 b言津,那就創(chuàng)建 b,結(jié)果創(chuàng)建 b 的時候又需要依賴 a ......

??互相依賴何時了取试,死循環(huán)了吧悬槽?
??諾,這就是循環(huán)依賴瞬浓!


循環(huán)依賴其實不算個問題或者錯誤初婆,我們實際在開發(fā)的時候,也可能會用到瑟蜈。
再拿最開始的 A 和 B 來說烟逊,我們手動使用的時候會用以下方式:

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

其實這樣就解決了循環(huán)依賴,功能上是沒有問題的铺根,但是為什么 Spring 要解決循環(huán)依賴宪躯?

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

首先簡單了解下位迂,我們用 Spring 框架访雪,它幫我們做了什么事情详瑞?總結(jié)性來說,六字真言:IoC 和 AOP臣缀。

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

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

IoC,主要是將對象的創(chuàng)建脂倦、管理都交給了 Spring 來管理番宁,能夠解決對象之間的耦合問題,對開發(fā)人員來說也是省時省力的赖阻。

AOP蝶押,主要是在不改變原有業(yè)務(wù)邏輯情況下,增強(qiáng)橫切邏輯代碼火欧,也是解耦合棋电,避免橫切邏輯代碼重復(fù);也是對 OOP 的延續(xù)苇侵、補(bǔ)充赶盔。

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

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

參考我們能想到的肯定是手動處理的方式招刨,先將對象都 new 出來,然后進(jìn)行 set 屬性值哀军,而 Spring 也是通過這樣的形式來處理的(你說巧不巧沉眶?其實一點都不巧 ??,后面再說為什么)杉适,其實 Spring 管理 Bean 的實例化底層其實是由反射實現(xiàn)的谎倔。

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

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

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

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

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

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

new A(b);

?? Wow秽五,是不是發(fā)現(xiàn)問題了孽查?要通過構(gòu)造器的方式,首先要將屬性值實例化出來疤勾盲再!A 要依賴屬性 b西设,就需要先將 B 實例化,可是 B 的實例化是不是還是需要依賴 A?? 這不就是文中開頭描述的樣子嘛答朋,所以通過構(gòu)造器的方式贷揽,Spring 也沒有辦法解決循環(huán)依賴

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

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

  1. 先實例化 A 類

  2. 再實例化 B 類

  3. set B 類中的 a 屬性

  4. set A 類中的 b 屬性

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

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

這里可以稍微說明一下淹冰,為什么這樣可以?

A a = new A()巨柒,說明 A 只是實例化樱拴,還未初始化

同理,B b = new B()洋满,也只是實例化晶乔,并未初始化

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

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

現(xiàn)在是不是有點感覺了正罢,先把狗騙進(jìn)來,再殺 ??

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

先上個通俗的答案解釋驻民,三級緩存翻具。

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

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

代碼中注釋可能不清晰柠硕,我再給貼一下三級緩存:

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

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

第三級緩存Map<String, ObjectFactory<?>> singletonFactories闻葵,存放可以生成 Bean 的工廠

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

僅使用一級緩存可以嗎坎缭?

既然都從 singletonObjects 里獲取竟痰,那么<u>僅僅使用這一個 singletonObjects</u>签钩,可以嗎?肯定不可以的坏快。
首先 singletonObjects 存入的是完全初始化好的 Bean铅檩,可以拿來直接用的。
如果我們直接將未初始化完的 Bean 放在 singletonObjects 里面莽鸿,注意昧旨,這個未初始化完的 Bean 極有可能會被其他的類拿去用,它都沒完事呢祥得,就被拿去造了兔沃,肯定要出事啊级及!

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

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

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

NPE 就是 NullPointerException

使用二級緩存可以嗎谴返?

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

說到底就是怎么打破這個循環(huán)煞肾,一級緩存不行,我們就再加一級亏镰,可以嗎扯旷?
我們看個圖

在這里插入圖片描述

圖中的緩存就是二級緩存

看完圖,可能還會有疑惑索抓,A 沒初始化完成放入了緩存钧忽,那么 B 用的豈不是就是未完成的 A,是這樣的沒錯逼肯!
在整個過程當(dāng)中耸黑,A 是只有1 個,而 B 那里的 A 只是 A 的引用篮幢,所以后面 A 完成了初始化大刊,B 中的 A 自然也就完成了。這里就是文中前面提到的手動 setA三椿,setB那里缺菌,我再貼一下代碼:

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

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

Spring 中 Bean 的管理

在這里插入圖片描述

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

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

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

那么由 Spring 來管理 Bean捏题,要經(jīng)過的主要步驟有:

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

  2. 根據(jù) BeanDefintion 的信息窟社,通過反射券勺,去實例化 Bean(此時就是實例化但未初始化 的 Bean)

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

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

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

如果沒有涉及到 AOP关炼,那么第四步就沒有生成代理類,將第三步完成屬性填充的對象存入緩存中匣吊。

二級緩存有什么問題儒拂?

如果 Bean 沒有 AOP,那么用二級緩存其實沒有什么問題的色鸳,一旦有上述生命周期中第四步社痛,就會導(dǎo)致的一個問題。因為 AOP 處理后命雀,往往是需要生成代理對象的蒜哀,代理對象和原來的對象根本就不是 1 個對象

以二級緩存的場景來說吏砂,假設(shè) A 類的某個方法會被 AOP撵儿,過程就是這樣的:

二級緩存問題示例圖
  1. 生成 a 的實例乘客,然后放入緩存,a 需要 b
  2. 再生成 b 淀歇,填充 b 的時候易核,需要 a,從緩存中取到了 a房匆,完成 b 的初始化耸成;
  3. 緊接著 a 把初始化好的 b 拿過來用,完成 a 的屬性填充和初始化
  4. 由于 A 類涉及到了 AOP浴鸿,再然后 a 要生成一個代理類井氢,這里就叫:代理 a 吧

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

使用三級緩存如何解決掸哑?

二級緩存還是有問題约急,那就再加一層緩存,也就是第三級緩存:Map<String, ObjectFactory<?>> singletonFactories苗分,在 bean 的生命周期中厌蔽,創(chuàng)建完對象之后,就會構(gòu)造一個這個對象對應(yīng)的 ObjectFactory 存入 singletonFactories 中摔癣。

singletonFactories 中存的是某個 beanName 及對應(yīng)的 ObjectFactory奴饮,這個 ObjectFactory 其實就是生成這個 Bean 的工廠。實際中择浊,這個 ObjectFactory 是個 Lambda 表達(dá)式:() -> getEarlyBeanReference(beanName, mbd, bean)戴卜,而且,這個表達(dá)式<u>并沒有</u>執(zhí)行琢岩。

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

核心就是兩步:

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

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

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

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

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

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

為什么不放入第一級緩存呢债蓝?

此時就拿到的代理對象壳鹤,也是未填充屬性的,也就是仍然是未初始化完的對象饰迹。

如果直接放入第一級緩存芳誓,此時被其他類拿去使用,肯定有問題了啊鸭。

那么什么時候放入第一級緩存锹淌?

這里需要再簡單說下第二級緩存的作用,假如 A 經(jīng)過第三級緩存赠制,獲得代理對象赂摆,這個代理對象仍然是未初始化完的!那么就暫時把這個代理對象放入第二級緩存钟些,然后刪除該代理對象原本在第三級緩存中的數(shù)據(jù)(確保后期不會每次都生成新的代理對象)烟号,后面其他類要用了 A,就去第二級緩存中找政恍,就獲取到了 A 的代理對象汪拥,而且都用的是同一個 A 的代理對象,這樣后面只需要對這一個代理對象進(jìn)行完善篙耗,其他引入該代理對象的類就都完善了喷楣。

再往后面,繼續(xù)完成 A 的初始化鹤树,那么先判斷 A 是否存在于 earlyProxyReferences 中, 存在就說明 A 已經(jīng)經(jīng)歷過 AOP 了逊朽,就無須再次 AOP罕伯。那 A 的操作就轉(zhuǎn)換從二級緩存中獲取,把 A 的代理類拿出來叽讳,填充代理類的屬性追他。

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

看個圖理解下

在這里插入圖片描述

總結(jié)

說了這么多涤妒,總結(jié)下三級緩存:

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

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

第三級緩存Map<String, ObjectFactory<?>> singletonFactories,存放可以生成 Bean 的工廠渐逃,工廠主要用來生成 Bean 的代理對象

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

時序圖并不標(biāo)準(zhǔn)够掠,但是方便大家去理解 ??
在理解循環(huán)依賴的時候,整體是個遞歸茄菊,你要有種套中套疯潭、夢中夢的感覺

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

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

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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末期丰,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子吃挑,更是在濱河造成了極大的恐慌钝荡,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,185評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件舶衬,死亡現(xiàn)場離奇詭異埠通,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)逛犹,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,652評論 3 393
  • 文/潘曉璐 我一進(jìn)店門端辱,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人虽画,你說我怎么就攤上這事舞蔽。” “怎么了码撰?”我有些...
    開封第一講書人閱讀 163,524評論 0 353
  • 文/不壞的土叔 我叫張陵渗柿,是天一觀的道長。 經(jīng)常有香客問我脖岛,道長朵栖,這世上最難降的妖魔是什么胞枕? 我笑而不...
    開封第一講書人閱讀 58,339評論 1 293
  • 正文 為了忘掉前任歌粥,我火速辦了婚禮,結(jié)果婚禮上葱绒,老公的妹妹穿的比我還像新娘绍在。我一直安慰自己门扇,他們只是感情好雹有,可當(dāng)我...
    茶點故事閱讀 67,387評論 6 391
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著悯嗓,像睡著了一般件舵。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上脯厨,一...
    開封第一講書人閱讀 51,287評論 1 301
  • 那天铅祸,我揣著相機(jī)與錄音,去河邊找鬼合武。 笑死临梗,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的稼跳。 我是一名探鬼主播盟庞,決...
    沈念sama閱讀 40,130評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼汤善!你這毒婦竟也來了什猖?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,985評論 0 275
  • 序言:老撾萬榮一對情侶失蹤红淡,失蹤者是張志新(化名)和其女友劉穎不狮,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體在旱,經(jīng)...
    沈念sama閱讀 45,420評論 1 313
  • 正文 獨居荒郊野嶺守林人離奇死亡摇零,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,617評論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了桶蝎。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片驻仅。...
    茶點故事閱讀 39,779評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖登渣,靈堂內(nèi)的尸體忽然破棺而出噪服,到底是詐尸還是另有隱情,我是刑警寧澤胜茧,帶...
    沈念sama閱讀 35,477評論 5 345
  • 正文 年R本政府宣布芯咧,位于F島的核電站,受9級特大地震影響竹揍,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜邪铲,卻給世界環(huán)境...
    茶點故事閱讀 41,088評論 3 328
  • 文/蒙蒙 一芬位、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧带到,春花似錦昧碉、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,716評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽四康。三九已至,卻和暖如春狭握,著一層夾襖步出監(jiān)牢的瞬間闪金,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,857評論 1 269
  • 我被黑心中介騙來泰國打工论颅, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留哎垦,地道東北人。 一個月前我還...
    沈念sama閱讀 47,876評論 2 370
  • 正文 我出身青樓恃疯,卻偏偏與公主長得像漏设,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子今妄,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,700評論 2 354

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

  • 最近面試的時候發(fā)現(xiàn)很多人會問Spring是如何解決循環(huán)依賴的郑口,雖然知道是通過三級緩存去解決的,但是也僅僅只是知其然...
    凱凱雄雄閱讀 686評論 0 6
  • 一盾鳞、Spring bean生命周期 可以簡化為以下5步犬性。 1、構(gòu)建BeanDefinition 2雁仲、實例化 Ins...
    胡峻崢閱讀 927評論 0 0
  • 網(wǎng)上關(guān)于Spring循環(huán)依賴的博客太多了仔夺,有很多都分析的很深入,寫的很用心攒砖,甚至還畫了時序圖缸兔、流程圖幫助讀者理解,...
    CoderBear閱讀 675評論 1 8
  • 我是黑夜里大雨紛飛的人啊 1 “又到一年六月吹艇,有人笑有人哭惰蜜,有人歡樂有人憂愁,有人驚喜有人失落受神,有的覺得收獲滿滿有...
    陌忘宇閱讀 8,536評論 28 53
  • 信任包括信任自己和信任他人 很多時候抛猖,很多事情,失敗鼻听、遺憾财著、錯過,源于不自信撑碴,不信任他人 覺得自己做不成撑教,別人做不...
    吳氵晃閱讀 6,187評論 4 8