Spring用三級(jí)緩存處理循環(huán)依賴的方法詳解
循環(huán)依賴問題的本質(zhì)
循環(huán)依賴的定義與常見場(chǎng)景
在 Spring 框架中,依賴注入是其核心特性之一,它允許對(duì)象之間的依賴關(guān)系在運(yùn)行時(shí)動(dòng)態(tài)注入。然而,當(dāng)多個(gè) Bean 之間的依賴關(guān)系形成一個(gè)閉環(huán)時(shí),就會(huì)出現(xiàn)循環(huán)依賴問題。簡(jiǎn)單來說,循環(huán)依賴就是 Bean A 依賴于 Bean B,而 Bean B 又依賴于 Bean A,或者存在更復(fù)雜的多 Bean 循環(huán)依賴鏈。
常見的循環(huán)依賴場(chǎng)景包括構(gòu)造器注入循環(huán)依賴和屬性注入循環(huán)依賴。下面分別通過代碼示例來展示這兩種場(chǎng)景。
構(gòu)造器注入循環(huán)依賴
@Component public class ConstructorBeanA { private ConstructorBeanB beanB; @Autowired public ConstructorBeanA(ConstructorBeanB beanB) { this.beanB = beanB; } } @Component public class ConstructorBeanB { private ConstructorBeanA beanA; @Autowired public ConstructorBeanB(ConstructorBeanA beanA) { this.beanA = beanA; } }
在上述代碼中,ConstructorBeanA 通過構(gòu)造器依賴于 ConstructorBeanB,而 ConstructorBeanB 同樣通過構(gòu)造器依賴于 ConstructorBeanA。當(dāng) Spring 容器嘗試創(chuàng)建這兩個(gè) Bean 時(shí),會(huì)陷入無限循環(huán),因?yàn)閯?chuàng)建 ConstructorBeanA 需要先創(chuàng)建 ConstructorBeanB,而創(chuàng)建 ConstructorBeanB 又需要先創(chuàng)建 ConstructorBeanA。
屬性注入循環(huán)依賴
@Component public class PropertyBeanA { @Autowired private PropertyBeanB beanB; } @Component public class PropertyBeanB { @Autowired private PropertyBeanA beanA; }
在屬性注入循環(huán)依賴的場(chǎng)景中,PropertyBeanA 通過屬性注入依賴于 PropertyBeanB,PropertyBeanB 也通過屬性注入依賴于 PropertyBeanA。雖然屬性注入的循環(huán)依賴可以通過 Spring 的三級(jí)緩存機(jī)制解決,但構(gòu)造器注入的循環(huán)依賴無法通過該機(jī)制解決,這是因?yàn)闃?gòu)造器注入要求在 Bean 實(shí)例化時(shí)就完成依賴注入,而此時(shí) Bean 還未進(jìn)入可以暴露的階段。
循環(huán)依賴帶來的問題
循環(huán)依賴會(huì)導(dǎo)致 Spring 容器在創(chuàng)建 Bean 時(shí)陷入無限循環(huán),最終拋出 BeanCurrentlyInCreationException 異常。這是因?yàn)樵跊]有合適解決方案的情況下,Spring 會(huì)不斷嘗試創(chuàng)建相互依賴的 Bean,無法完成 Bean 的初始化過程。例如,當(dāng)創(chuàng)建 ConstructorBeanA 時(shí),需要調(diào)用其構(gòu)造器并傳入 ConstructorBeanB 的實(shí)例,而此時(shí) ConstructorBeanB 還未創(chuàng)建,于是 Spring 開始創(chuàng)建 ConstructorBeanB。但在創(chuàng)建 ConstructorBeanB 時(shí),又需要 ConstructorBeanA 的實(shí)例,這樣就會(huì)陷入死循環(huán),導(dǎo)致程序無法正常運(yùn)行。
三級(jí)緩存的構(gòu)成與作用
一級(jí)緩存-完全初始化Bean的倉庫
一級(jí)緩存的定義與數(shù)據(jù)結(jié)構(gòu)
一級(jí)緩存,也稱為單例池,在 Spring 中通常用 ConcurrentHashMap<String, Object> 來實(shí)現(xiàn)。其中,鍵是 Bean 的名稱,值是已經(jīng)完全初始化好的 Bean 實(shí)例。ConcurrentHashMap 是線程安全的,這保證了在多線程環(huán)境下對(duì)一級(jí)緩存的訪問和操作是安全的。
// 一級(jí)緩存的示例定義 private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
一級(jí)緩存的作用
一級(jí)緩存是 Spring 中最終存儲(chǔ)可用 Bean 的地方。當(dāng)一個(gè) Bean 經(jīng)歷了所有的初始化步驟,包括實(shí)例化、屬性注入、初始化方法調(diào)用等,最終成為一個(gè)可以直接使用的完整 Bean 時(shí),它會(huì)被放入一級(jí)緩存中。后續(xù)其他 Bean 如果需要依賴這個(gè) Bean,就可以直接從一級(jí)緩存中獲取,避免了重復(fù)創(chuàng)建 Bean 的開銷。例如,當(dāng)一個(gè)服務(wù)層的 Bean 需要依賴一個(gè)數(shù)據(jù)訪問層的 Bean 時(shí),Spring 會(huì)首先從一級(jí)緩存中查找該數(shù)據(jù)訪問層 Bean,如果存在則直接使用。
一級(jí)緩存的使用時(shí)機(jī)
在 Bean 創(chuàng)建的最后階段,當(dāng) Bean 完成了所有的初始化操作并且通過了各種后置處理器的處理后,Spring 會(huì)將該 Bean 放入一級(jí)緩存中。同時(shí),會(huì)從二級(jí)緩存和三級(jí)緩存中移除該 Bean 的相關(guān)信息,以確保 Bean 只在一級(jí)緩存中存在。
二級(jí)緩存-早期暴露的Bean臨時(shí)存儲(chǔ)
二級(jí)緩存的定義與數(shù)據(jù)結(jié)構(gòu)
二級(jí)緩存也是一個(gè) Map 結(jié)構(gòu),通常使用 ConcurrentHashMap<String, Object> 實(shí)現(xiàn)。它用于存放早期暴露的 Bean 對(duì)象,這些 Bean 已經(jīng)完成了實(shí)例化,但還沒有進(jìn)行屬性填充等后續(xù)操作。
// 二級(jí)緩存的示例定義 private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16);
二級(jí)緩存的作用
二級(jí)緩存的主要作用是在解決循環(huán)依賴時(shí),提供一個(gè)臨時(shí)存儲(chǔ)早期暴露 Bean 的地方。當(dāng)一個(gè) Bean 剛剛被實(shí)例化出來,但還沒有完成所有的初始化步驟時(shí),為了防止在循環(huán)依賴場(chǎng)景下出現(xiàn)問題,Spring 會(huì)將這個(gè)早期暴露的 Bean 先放入二級(jí)緩存中。這樣,當(dāng)其他 Bean 需要依賴它時(shí),可以先從二級(jí)緩存中獲取到這個(gè)尚未完全初始化的 Bean 引用,避免了不必要的遞歸創(chuàng)建。例如,在前面提到的 PropertyBeanA 和 PropertyBeanB 的循環(huán)依賴場(chǎng)景中,當(dāng)創(chuàng)建 PropertyBeanA 并將其早期暴露到二級(jí)緩存后,在創(chuàng)建 PropertyBeanB 時(shí)就可以從二級(jí)緩存中獲取到 PropertyBeanA 的引用,從而打破循環(huán)。
二級(jí)緩存的使用時(shí)機(jī)
在 Bean 實(shí)例化完成后,Spring 會(huì)將該 Bean 的早期引用放入二級(jí)緩存中。在后續(xù)的屬性注入和初始化過程中,如果發(fā)現(xiàn)有其他 Bean 依賴于該 Bean,就會(huì)先從二級(jí)緩存中嘗試獲取該 Bean 的引用。當(dāng) Bean 最終完成所有初始化操作并放入一級(jí)緩存后,會(huì)從二級(jí)緩存中移除該 Bean 的引用。
三級(jí)緩存-延遲創(chuàng)建Bean的工廠
三級(jí)緩存的定義與數(shù)據(jù)結(jié)構(gòu)
三級(jí)緩存是一個(gè) Map<String, ObjectFactory<?>> 結(jié)構(gòu),其中鍵是 Bean 的名稱,值是一個(gè) ObjectFactory 對(duì)象。ObjectFactory 是一個(gè)函數(shù)式接口,它只有一個(gè) getObject() 方法,用于創(chuàng)建 Bean 對(duì)象。
// 三級(jí)緩存的示例定義 private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
三級(jí)緩存的作用
三級(jí)緩存的主要作用是提供一種延遲加載和創(chuàng)建 Bean 的機(jī)制。在 Bean 創(chuàng)建過程中,尤其是在處理循環(huán)依賴時(shí),它能夠通過 ObjectFactory 來獲取早期暴露的 Bean 對(duì)象。通過這種方式,Spring 可以在合適的時(shí)機(jī)創(chuàng)建 Bean,避免了因?yàn)橹苯觿?chuàng)建而可能導(dǎo)致的循環(huán)依賴問題。例如,在某些情況下,可能需要對(duì) Bean 進(jìn)行一些額外的處理或增強(qiáng),通過 ObjectFactory 可以在需要的時(shí)候才進(jìn)行這些操作,而不是在 Bean 實(shí)例化時(shí)就進(jìn)行。
三級(jí)緩存的使用時(shí)機(jī)
在 Bean 實(shí)例化完成后,Spring 會(huì)創(chuàng)建一個(gè) ObjectFactory 對(duì)象,并將其放入三級(jí)緩存中。這個(gè) ObjectFactory 對(duì)象會(huì)在需要獲取早期暴露的 Bean 時(shí)被調(diào)用。當(dāng)從三級(jí)緩存中獲取到 ObjectFactory 并調(diào)用其 getObject() 方法后,會(huì)將獲取到的 Bean 放入二級(jí)緩存中,并從三級(jí)緩存中移除該 ObjectFactory。
利用三級(jí)緩存解決循環(huán)依賴的詳細(xì)過程
創(chuàng)建Bean A
實(shí)例化Bean A
當(dāng) Spring 容器開始創(chuàng)建 BeanA 時(shí),首先會(huì)調(diào)用 BeanA 的構(gòu)造函數(shù)來實(shí)例化它。在這個(gè)階段,BeanA 只是一個(gè)剛被創(chuàng)建出來的對(duì)象,它的所有屬性都還是默認(rèn)值,還沒有進(jìn)行屬性注入等操作。例如,如果 BeanA 有一個(gè) name 屬性,此時(shí) name 的值為 null。
// BeanA 的構(gòu)造函數(shù)示例 public class BeanA { private BeanB beanB; public BeanA() { // 構(gòu)造函數(shù)邏輯 } }
將Bean A的早期引用放入三級(jí)緩存
在 BeanA 實(shí)例化完成后,Spring 會(huì)創(chuàng)建一個(gè) ObjectFactory 對(duì)象,該對(duì)象封裝了獲取 BeanA 早期引用的邏輯。然后將這個(gè) ObjectFactory 對(duì)象放入三級(jí)緩存中。
// 將 BeanA 的早期引用放入三級(jí)緩存的示例邏輯 singletonFactories.put("beanA", () -> getEarlyBeanReference(beanName, mbd, bean));
屬性注入時(shí)發(fā)現(xiàn)對(duì)Bean B的依賴
開始屬性注入
在 BeanA 實(shí)例化完成并將其早期引用放入三級(jí)緩存后,Spring 開始進(jìn)行 BeanA 的屬性注入。在這個(gè)過程中,會(huì)檢查 BeanA 的所有依賴項(xiàng)。
發(fā)現(xiàn)對(duì)Bean B的依賴
當(dāng)檢查到 BeanA 依賴于 BeanB 時(shí),Spring 容器會(huì)嘗試去獲取 BeanB。由于此時(shí)容器中還沒有完全初始化的 BeanB,所以 Spring 會(huì)進(jìn)入創(chuàng)建 BeanB 的流程。
創(chuàng)建Bean B及發(fā)現(xiàn)對(duì)Bean A的依賴
實(shí)例化Bean B
Spring 容器開始創(chuàng)建 BeanB,同樣會(huì)調(diào)用 BeanB 的構(gòu)造函數(shù)來實(shí)例化它。此時(shí),BeanB 也只是一個(gè)剛被創(chuàng)建出來的對(duì)象,其屬性還未進(jìn)行注入。
// BeanB 的構(gòu)造函數(shù)示例 public class BeanB { private BeanA beanA; public BeanB() { // 構(gòu)造函數(shù)邏輯 } }
將Bean B的早期引用放入三級(jí)緩存
與 BeanA 類似,在 BeanB 實(shí)例化完成后,Spring 會(huì)創(chuàng)建一個(gè) ObjectFactory 對(duì)象,并將其放入三級(jí)緩存中。
發(fā)現(xiàn)對(duì)Bean A的依賴
在進(jìn)行 BeanB 的屬性注入時(shí),發(fā)現(xiàn) BeanB 依賴于 BeanA。此時(shí),Spring 會(huì)嘗試從緩存中獲取 BeanA。
從三級(jí)緩存獲取Bean A
檢查緩存
Spring 首先會(huì)檢查一級(jí)緩存中是否存在 BeanA,由于 BeanA 還未完全初始化,所以一級(jí)緩存中不存在。然后會(huì)檢查二級(jí)緩存,二級(jí)緩存中也沒有。最后會(huì)檢查三級(jí)緩存,發(fā)現(xiàn)三級(jí)緩存中有 BeanA 對(duì)應(yīng)的 ObjectFactory。
獲取早期暴露的Bean A
Spring 會(huì)調(diào)用 BeanA 對(duì)應(yīng)的 ObjectFactory 的 getObject() 方法,獲取到早期暴露的 BeanA 的引用。然后將這個(gè) BeanA 的引用從三級(jí)緩存中移除,并放入二級(jí)緩存中。
完成Bean B的創(chuàng)建
注入Bean A的引用
將從二級(jí)緩存中獲取到的 BeanA 的引用注入到 BeanB 中。此時(shí),BeanB 雖然獲取到的是一個(gè)尚未完全初始化的 BeanA 引用,但這已經(jīng)足夠打破循環(huán)依賴。
完成Bean B的初始化
BeanB 繼續(xù)完成其他屬性的注入和初始化操作。在這個(gè)過程中,由于已經(jīng)有了 BeanA 的引用,所以不會(huì)再觸發(fā)對(duì) BeanA 的重新創(chuàng)建,從而避免了循環(huán)依賴。最終,BeanB 完成所有的初始化操作,被放入一級(jí)緩存中。
完成Bean A的創(chuàng)建
獲取Bean B的引用
BeanA 獲取到已經(jīng)完全創(chuàng)建好的 BeanB,這個(gè) BeanB 是從一級(jí)緩存中獲取的。
完成Bean A的初始化
BeanA 繼續(xù)完成自身剩余的屬性注入和其他初始化操作。最后,BeanA 也完成了所有的初始化步驟,被放入一級(jí)緩存中。此時(shí),BeanA 和 BeanB 都已經(jīng)成功創(chuàng)建,并且它們之間的循環(huán)依賴問題也得到了妥善解決。
通過這樣精細(xì)的三級(jí)緩存機(jī)制,Spring 成功地打破了循環(huán)依賴的僵局,確保了 Bean 之間能夠正確地進(jìn)行依賴注入和初始化,為開發(fā)者提供了一個(gè)穩(wěn)定、可靠的依賴管理環(huán)境。同時(shí),我們也應(yīng)該注意到,這種機(jī)制對(duì)于構(gòu)造器注入的循環(huán)依賴是無法解決的,在開發(fā)過程中應(yīng)盡量避免構(gòu)造器注入的循環(huán)依賴情況的發(fā)生。
以上就是Spring用三級(jí)緩存處理循環(huán)依賴的方法詳解的詳細(xì)內(nèi)容,更多關(guān)于Spring三級(jí)緩存處理循環(huán)依賴的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
SpringBoot啟動(dòng)之SpringApplication初始化詳解
這篇文章主要介紹了SpringBoot啟動(dòng)之SpringApplication初始化詳解,首先初始化資源加載器,默認(rèn)為null;斷言判斷主要資源類不能為null,否則報(bào)錯(cuò),需要的朋友可以參考下2024-01-01Spring Boot 簡(jiǎn)單使用EhCache緩存框架的方法
本篇文章主要介紹了Spring Boot 簡(jiǎn)單使用EhCache緩存框架的方法,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-07-07Java8 Instant 時(shí)間戳實(shí)例講解
Instant類是Java8 中補(bǔ)充的一個(gè) 時(shí)間戳類,nstant 可以使用靜態(tài)方法 now()或者of()方法來創(chuàng)建一個(gè)實(shí)例對(duì)象,本文通過實(shí)例代碼講解Java8 Instant 時(shí)間戳,感興趣的朋友跟隨小編一起看看吧2022-11-11詳解JDK自帶javap命令反編譯class文件和Jad反編譯class文件(推薦使用jad)
這篇文章主要介紹了JDK自帶javap命令反編譯class文件和Jad反編譯class文件(推薦使用jad),本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-09-09java并發(fā)編程工具類JUC之ArrayBlockingQueue
類ArrayBlockingQueue是BlockingQueue接口的實(shí)現(xiàn)類,它是有界的阻塞隊(duì)列,內(nèi)部使用數(shù)組存儲(chǔ)隊(duì)列元素,通過代碼給大家說明如何初始化一個(gè)ArrayBlockingQueue,并向其中添加一個(gè)對(duì)象,對(duì)java并發(fā)編程工具類ArrayBlockingQueue相關(guān)知識(shí)感興趣的朋友一起看看吧2021-05-05淺談DetachedCriteria和Criteria的使用方法(必看)
下面小編就為大家?guī)硪黄獪\談DetachedCriteria和Criteria的使用方法(必看)。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-05-05mybatis數(shù)組和集合的長(zhǎng)度判斷及插入方式
這篇文章主要介紹了mybatis數(shù)組和集合的長(zhǎng)度判斷及插入方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-01-01java + dom4j.jar提取xml文檔內(nèi)容
這篇文章主要為大家詳細(xì)介紹了java + dom4j.jar提取xml文檔內(nèi)容,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-08-08