寫(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 為例)
先實(shí)例化 A 類
再實(shí)例化 B 類
set B 類中的 a 屬性
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 舉栗子??
- 先實(shí)例化 A 類,叫 a
- 將 a 放入 singletonObjects 中(此時(shí) a 中的 b 屬性還是空的呢)
- C 類需要使用 A 類,去 singletonObjects 獲取,且獲取到了 a
- 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ò)的主要步驟有:
Spring 根據(jù)開(kāi)發(fā)人員的配置,掃描哪些類由 Spring 來(lái)管理,并為每個(gè)類生成一個(gè) BeanDefintion,里面封裝了類的一些信息,如全限定類名、哪些屬性、是否單例等等
根據(jù) BeanDefintion 的信息,通過(guò)反射,去實(shí)例化 Bean(此時(shí)就是實(shí)例化但未初始化 的 Bean)
填充上述未初始化對(duì)象中的屬性(依賴注入)
如果上述未初始化對(duì)象中的方法被 AOP 了,那么就需要生成代理類(也叫包裝類)
最后將完成初始化的對(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ò)程就是這樣的:
- 生成 a 的實(shí)例,然后放入緩存,a 需要 b
- 再生成 b ,填充 b 的時(shí)候,需要 a,從緩存中取到了 a,完成 b 的初始化;
- 緊接著 a 把初始化好的 b 拿過(guò)來(lái)用,完成 a 的屬性填充和初始化
- 由于 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é)

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