溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

怎么使用Spring三級緩存解決循環(huán)依賴問題

發(fā)布時間:2023-05-09 15:57:35 來源:億速云 閱讀:119 作者:zzz 欄目:開發(fā)技術

這篇文章主要介紹了怎么使用Spring三級緩存解決循環(huán)依賴問題的相關知識,內容詳細易懂,操作簡單快捷,具有一定借鑒價值,相信大家閱讀完這篇怎么使用Spring三級緩存解決循環(huán)依賴問題文章都會有所收獲,下面我們一起來看看吧。

循環(huán)依賴

什么是循環(huán)依賴?很簡單,看下方的代碼就知曉了

@Service
public class A {
    @Autowired
    private B b;
}

@Service
public class B {
    @Autowired
    private A a;
}

上面這兩種方式都是循環(huán)依賴,應該很好理解,當然也可以是三個 Bean 甚至更多的 Bean 相互依賴,原理都是一樣的,今天我們主要分析兩個 Bean 的依賴。

怎么使用Spring三級緩存解決循環(huán)依賴問題

這種循環(huán)依賴可能會產生問題,例如 A 要依賴 B,發(fā)現 B 還沒創(chuàng)建。
于是開始創(chuàng)建 B ,創(chuàng)建的過程發(fā)現 B 要依賴 A, 而 A 還沒創(chuàng)建好呀,因為它要等 B 創(chuàng)建好。
就這樣它們倆就擱這卡 bug 了。

解決思路

上面這種循環(huán)依賴在實際場景中是會出現的,所以 Spring 需要解決這個問題,那如何解決呢?
關鍵就是提前暴露未完全創(chuàng)建完畢的 Bean。
在 Spring 中,只有同時滿足以下兩點才能解決循環(huán)依賴的問題:

依賴的 Bean 必須都是單例依賴注入的方式,必須不全是構造器注入,且 beanName 字母序在前的不能是構造器注入

Spring 只支持單例的循環(huán)依賴,因為如果兩個 Bean 都是原型模式的話:
創(chuàng)建 A1 需要創(chuàng)建一個 B1。
創(chuàng)建 B1 的時候要創(chuàng)建一個 A2。
創(chuàng)建 A2 又要創(chuàng)建一個 B2。
創(chuàng)建 B2 又要創(chuàng)建一個 A3。
創(chuàng)建 A3 又要創(chuàng)建一個 B3…
就又卡 BUG 了,是吧,因為原型模式都需要創(chuàng)建新的對象,不能跟用以前的對象。
如果是單例的話,創(chuàng)建 A 需要創(chuàng)建 B,而創(chuàng)建的 B 需要的是之前的個 A, 不然就不叫單例了,對吧?
也是基于這點, Spring 就能操作操作了。
具體做法就是:先創(chuàng)建 A,此時的 A 是不完整的(沒有注入 B),用個 map 保存這個不完整的 A,再創(chuàng)建 B ,B 需要 A。
所以從那個 map 得到“不完整”的 A,此時的 B 就完整了,然后 A 就可以注入 B,然后 A 就完整了,B 也完整了,且它們是相互依賴的。

那為啥必須不全是構造器注入,因為在 Spring 中創(chuàng)建 Bean 分三步:

  • 實例化,createBeanInstance,就是 new 了個對象

  • 屬性注入,populateBean, 就是 set 一些屬性值

  • 初始化,initializeBean,執(zhí)行一些 aware 接口中的方法,initMethod,AOP代理等

明確了上面這三點,再結合我上面說的“不完整的”,我們來理一下。
如果全是構造器注入,比如A(B b),那表明在 new 的時候,就需要得到 B,此時需要 new B 。
但是 B 也是要在構造的時候注入 A ,即B(A a),這時候 B 需要在一個 map 中找到不完整的 A ,發(fā)現找不到。
為什么找不到?因為 A 還沒 new 完呢,所以找到不完整的 A,因此如果全是構造器注入的話,那么 Spring 無法處理循環(huán)依賴。

解決流程

經過上面的鋪墊,我想你對 Spring 如何解決循環(huán)依賴應該已經有點感覺了,接下來我們就來看看它到底是如何實現的。
明確了 Spring 創(chuàng)建 Bean 的三步驟之后,我們再來看看它為單例搞的三個 map:

  • 一級緩存,singletonObjects,存儲所有已創(chuàng)建完畢的單例 Bean (完整的 Bean)

  • 二級緩存,earlySingletonObjects,存儲所有僅完成實例化,但還未進行屬性注入和初始化的 Bean

  • 三級緩存,singletonFactories,存儲能建立這個 Bean 的一個工廠,通過工廠能獲取這個 Bean,延遲化 Bean 的生成,工廠生成的 Bean 會塞入二級緩存

怎么使用Spring三級緩存解決循環(huán)依賴問題

這三個 map 是如何配合的呢?

  • 首先,獲取單例 Bean 的時候會通過 BeanName 先去 singletonObjects(一級緩存) 查找完整的 Bean,如果找到則直接返回,否則進行步驟 2。

  • 看對應的 Bean 是否在創(chuàng)建中,如果不在直接返回找不到,如果是,則會去 earlySingletonObjects (二級緩存)查找 Bean,如果找到則返回,否則進行步驟 3

  • 去 singletonFactories (三級緩存)通過 BeanName 查找到對應的工廠,如果存著工廠則通過工廠創(chuàng)建 Bean ,并且放置到 earlySingletonObjects 中。

  • 如果三個緩存都沒找到,則返回 null。

從上面的步驟我們可以得知,如果查詢發(fā)現 Bean 還未創(chuàng)建,到第二步就直接返回 null,不會繼續(xù)查二級和三級緩存。
返回 null 之后,說明這個 Bean 還未創(chuàng)建,這個時候會標記這個 Bean 正在創(chuàng)建中,然后再調用 createBean 來創(chuàng)建 Bean,而實際創(chuàng)建是調用方法 doCreateBean。
doCreateBean 這個方法就會執(zhí)行上面我們說的三步驟:

  • 實例化

  • 屬性注入

  • 初始化

以上面的例子來講解,在實例化A 之后,會往三級緩存 singletonFactories 塞入一個工廠A,而調用這個工廠A的 getObject 方法,就能得到這個 A。
要注意,此時 Spring 是不知道會不會有循環(huán)依賴發(fā)生的,但是它不管,反正往 singletonFactories 塞這個工廠,這里就是提前暴露
然后就開始執(zhí)行屬性注入,這個時候 A 發(fā)現需要注入 B,所以去 getBean(B),此時又會走一遍上面描述的邏輯,到了 B 的屬性注入這一步。
此時 B 調用 getBean(A),這時候一級緩存里面找不到,但是發(fā)現 A 正在創(chuàng)建中的,于是去二級緩存找,發(fā)現沒找到,于是去三級緩存找,然后找到了。
并且通過上面提前在三級緩存里暴露的工廠得到 A,然后將這個工廠從三級緩存里刪除,并將 A 加入到二級緩存中。
然后結果就是 B 屬性注入成功。
緊接著 B 調用 initializeBean 初始化,最終返回,此時 B 已經被加到了一級緩存里 。
這時候就回到了 A 的屬性注入,此時注入了 B,接著執(zhí)行初始化,最后 A 也會被加到一級緩存里,且從二級緩存中刪除 A。
Spring 解決依賴循環(huán)就是按照上面所述的邏輯來實現的。
重點就是在對象實例化之后,都會在三級緩存里加入一個工廠,提前對外暴露還未完整的 Bean,這樣如果被循環(huán)依賴了,對方就可以利用這個工廠得到一個不完整的 Bean,破壞了循環(huán)的條件。

二個緩存不行?

上面都說了那么多了,那我們思考下,解決循環(huán)依賴需要三級緩存嗎?
很明顯,如果僅僅只是為了破解循環(huán)依賴,二個緩存夠了,壓根就不必要三級。
你思考一下,在實例化 Bean A 之后,我在二級 map 里面塞入這個 A,然后繼續(xù)屬性注入。
發(fā)現 A 依賴 B 所以要創(chuàng)建 Bean B,這時候 B 就能從二級 map 得到 A ,完成 B 的建立之后, A 自然而然能完成。
所以為什么要搞個三級緩存,且里面存的是創(chuàng)建 Bean 的工廠呢?
我們來看下調用工廠的 getObject 到底會做什么,實際會調用下面這個方法:

protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
    Object exposedObject = bean;
    if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
        for (SmartInstantiationAwareBeanPostProcessor bp : getBeanPostProcessorCache().smartInstantiationAware) {
            exposedObject = bp.getEarlyBeanReference(exposedObject, beanName);
        }
    }
    return exposedObject;
}

重點就在中間的判斷,如果 false,返回就是參數傳進來的 bean,沒任何變化。
如果是 true 說明有 InstantiationAwareBeanPostProcessors 。
且循環(huán)的是 smartInstantiationAware 類型,如有這個 BeanPostProcessor 說明 Bean 需要被 aop 代理。
我們都知道如果有代理的話,那么我們想要直接拿到的是代理對象。
也就是說如果 A 需要被代理,那么 B 依賴的 A 是已經被代理的 A,所以我們不能返回 A 給 B,而是返回代理的 A 給 B。
這個工廠的作用就是判斷這個對象是否需要代理,如果否則直接返回,如果是則返回代理對象。
看到這明白的小伙伴肯定會問,那跟三級緩存有什么關系,我可以在要放到二級緩存的時候判斷這個 Bean 是否需要代理,如果要直接放代理的對象不就完事兒了。
是的,這個思路看起來沒任何問題,問題就出在時機,這跟 Bean 的生命周期有關系。
Spring 原本的設計是,bean 的創(chuàng)建過程分三個階段:
1 創(chuàng)建實例 createBeanInstance – 創(chuàng)建出 bean 的原始對象
2 填充依賴 populateBean – 利用反射,使用 BeanWrapper 來設置屬性值
3 initializeBean – 執(zhí)行 bean 創(chuàng)建后的處理,包括 AOP 對象的產生
在沒有循環(huán)依賴的場景下:第 1,2 步都是 bean 的原始對象,第 3 步 initializeBean 時,才會生成 AOP 代理對象。
所以,循環(huán)依賴打破了 AOP 代理 bean 生成的時機,需要在 populateBean 之前就生成 AOP 代理 bean。
而且,生成 AOP 代理需要執(zhí)行 BeanPostProcessor,而 Spring 原本的設計是在第 3 步 initializeBean 時才去調用 BeanPostProcessor 的。
所以 Spring 先在一個三級緩存放置一個工廠,用于代表Bean的引用。只有在上面的第三步時,才會通過這個工廠去創(chuàng)建代理對象,這樣生命周期就不會亂套了。

理論上來說,使用二級緩存是可以解決 AOP 代理 bean 的循環(huán)依賴的。只是 Spring 沒有選擇這樣去實現。Spring 選擇了三級緩存來實現,讓 bean 的創(chuàng)建流程更加符合常理,更加清晰明了。

關于“怎么使用Spring三級緩存解決循環(huán)依賴問題”這篇文章的內容就介紹到這里,感謝各位的閱讀!相信大家對“怎么使用Spring三級緩存解決循環(huán)依賴問題”知識都有一定的了解,大家如果還想學習更多知識,歡迎關注億速云行業(yè)資訊頻道。

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內容(圖片、視頻和文字)以原創(chuàng)、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI