天天看點

如何通過三級緩存解決 Spring 循環依賴

作者:Java架構學習指南

以下内容基于 Spring6.0.4。

這個其實是一個特别高頻的面試題,也一直很想和大家仔細來聊一聊這個話題,網上關于這塊的文章很多,但是我一直覺得要把這個問題講清楚還有點難度,今天我來試一試,看能不能和小夥伴們把這個問題梳理清楚。

1. 循環依賴

1.1 什麼是循環依賴

首先,什麼是循環依賴?這個其實好了解,就是兩個 Bean 互相依賴,類似下面這樣:

@Service
public class AService {
    @Autowired
    BService bService;
}
@Service
public class BService {
    @Autowired
    AService aService;
}           

AService 和 BService 互相依賴:

如何通過三級緩存解決 Spring 循環依賴

這個應該很好了解。

1.2 循環依賴的類型

一般來說,循環依賴有三種不同的形态,上面 1.1 小節是其中一種。

另外兩種分别是三者依賴,如下圖:

如何通過三級緩存解決 Spring 循環依賴

這種循環依賴一般隐藏比較深,不易發覺。

還有自我依賴,如下圖:

如何通過三級緩存解決 Spring 循環依賴
一般來說,如果我們的代碼中出現了循環依賴,則說明我們的代碼在設計的過程中可能存在問題,我們應該盡量避免循環依賴的發生。不過一旦發生了循環依賴,Spring 預設也幫我們處理好了,當然這并不能說明循環依賴這種代碼就沒問題。實際上在目前最新版的 Spring 中,循環依賴是要額外開啟的,如果不額外配置,發生了循環依賴就直接報錯了。
另外,Spring 并不能處理所有的循環依賴,後面松哥會和大家進行分析。

2. 循環依賴解決思路

2.1 解決思路

那麼對于循環依賴該如何解決呢?其實很簡單,中加加入一個緩存就可以了,小夥伴們來看下面這張圖:

如何通過三級緩存解決 Spring 循環依賴

我們在這裡引入了一個緩存池。

當我們需要建立 AService 的執行個體的時候,會首先通過 Java 反射建立出來一個原始的 AService,這個原始 AService 可以簡單了解為剛剛 new 出來(實際是剛剛通過反射建立出來)還沒設定任何屬性的 AService,此時,我們把這個 AService 先存入到一個緩存池中。

接下來我們就需要給 AService 的屬性設定值了,同時還要處理 AService 的依賴,這時我們發現 AService 依賴 BService,那麼就去建立 BService 對象,結果建立 BService 的時候,發現 BService 依賴 AService,那麼此時就先從緩存池中取出來 AService 先用着,然後繼續 BService 建立的後續流程,直到 BService 建立完成後,将之指派給 AService,此時 AService 和 BService 就都建立完成了。

可能有小夥伴會說,BService 從緩存池中拿到的 AService 是一個半成品,并不是真正的最終的 AService,但是小夥伴們要知道,咱們 Java 是引用傳遞(也可以認為是值傳遞,隻不過這個值是記憶體位址),BService 當時拿到的是 AService 的引用,說白了就是一塊記憶體位址而已,根據這個位址找到的就是 AService,是以,後續如果 AService 建立完成後,BService 所拿到的 AService 就是完整的 AService 了。

那麼上面提到的這個緩存池,在 Spring 容器中有一個專門的名字,就叫做 earlySingletonObjects,這是 Spring 三級緩存中的二級緩存,這裡儲存的是剛剛通過反射建立出來的 Bean,這些 Bean 還沒有經曆過完整生命周期,Bean 的屬性可能都還沒有設定,Bean 需要的依賴都還沒有注入進來。另外兩級緩存分别是:

  • singletonObjects:這是一級緩存,一級緩存中儲存的是所有經曆了完整生命周期的 Bean,即一個 Bean 從建立、到屬性指派、到各種處理器的執行等等,都經曆過了,就存到 singletonObjects 中,當我們需要擷取一個 Bean 的時候,首先會去一級緩存中查找,當一級緩存中沒有的時候,才會考慮去二級緩存。
  • singletonFactories:這是三級緩存。在一級緩存和二級緩存中,緩存的 key 是 beanName,緩存的 value 則是一個 Bean 對象,但是在三級緩存中,緩存的 value 是一個 Lambda 表達式,通過這個 Lambda 表達式可以建立出來目标對象的一個代理對象。

有的小夥伴可能會覺得奇怪,按照上文的介紹,一級緩存和二級緩存就足以解決循環依賴了,為什麼還冒出來一個三級緩存?那就得考慮 AOP 的情況了!

2.2 存在 AOP 怎麼辦

上面給大家介紹的是普通的 Bean 建立,那确實沒有問題。但是 Spring 中還有一個非常重要的能力,那就是 AOP。

說到這裡,我得先和小夥伴麼說一說 Spring 中 AOP 的建立流程。

正常來說是我們首先通過反射擷取到一個 Bean 的執行個體,然後就是給這個 Bean 填充屬性,屬性填充完畢之後,接下來就是執行各種 BeanPostProcessor 了,如果這個 Bean 中有需要代理的方法,那麼系統就會自動配置對應的後置處理器,松哥舉一個簡單例子,假設我有如下一個 Service:

@Service
public class UserService {

    @Async
    public void hello() {
        System.out.println("hello>>>"+Thread.currentThread().getName());
    }
}           

那麼系統就會自動提供一個名為 AsyncAnnotationBeanPostProcessor 的處理器,在這個處理器中,系統會生成一個代理的 UserService 對象,并用這個對象代替原本的 UserService。

那麼小夥伴們要搞清楚的是,原本的 UserService 和新生成的代理的 UserService 是兩個不同的對象,占兩塊不同的記憶體位址!!!

我們再來回顧下面這張圖:

如何通過三級緩存解決 Spring 循環依賴

如果 AService 最終是要生成一個代理對象的話,那麼 AService 存到緩存池的其實還是原本的 AService,因為此時還沒到處理 AOP 那一步(要先給各個屬性指派,然後才是 AOP 處理),這就導緻 BService 從緩存池裡拿到的 AService 是原本的 AService,等到 BService 建立完畢之後,AService 的屬性指派才完成,接下來在 AService 後續的建立流程中,AService 會變成了一個代理對象了,不是緩存池裡的 AService 了,最終就導緻 BService 所依賴的 AService 和最終建立出來的 AService 不是同一個。

為了解決這個問題,Spring 引入了三級緩存 singletonFactories。

singletonFactories 的工作機制是這樣的(假設 AService 最終是一個代理對象):

當我們建立一個 AService 的時候,通過反射剛把原始的 AService 建立出來之後,先去判斷目前一級緩存中是否存在目前 Bean,如果不存在,則:

  1. 首先向三級緩存中添加一條記錄,記錄的 key 就是目前 Bean 的 beanName,value 則是一個 Lambda 表達式 ObjectFactory,通過執行這個 Lambda 可以給目前 AService 生成代理對象。
  2. 然後如果二級緩存中存在目前 AService Bean,則移除掉。

現在繼續去給 AService 各個屬性指派,結果發現 AService 需要 BService,然後就去建立 BService,建立 BService 的時候,發現 BService 又需要用到 AService,于是就先去一級緩存中查找是否有 AService,如果有,就使用,如果沒有,則去二級緩存中查找是否有 AService,如果有,就使用,如果沒有,則去三級緩存中找出來那個 ObjectFactory,然後執行這裡的 getObject 方法,這個方法在執行的過程中,會去判斷是否需要生成一個代理對象,如果需要就生成代理對象傳回,如果不需要生成代理對象,則将原始對象傳回即可。最後,把拿到手的對象存入到二級緩存中以備下次使用,同時删除掉三級緩存中對應的資料。這樣 AService 所依賴的 BService 就建立好了。

接下來繼續去完善 AService,去執行各種後置的處理器,此時,有的後置處理器想給 AService 生成代理對象,發現 AService 已經是代理對象了,就不用生成了,直接用已有的代理對象去代替 AService 即可。

至此,AService 和 BService 都搞定。

本質上,singletonFactories 是把 AOP 的過程提前了。

3. 小結

總的來說,Spring 解決循環依賴把握住兩個關鍵點:

  • 提前暴露:剛剛建立好的對象還沒有進行任何指派的時候,将之暴露出來放到緩存中,供其他 Bean 提前引用(二級緩存)。
  • 提前 AOP:A 依賴 B 的時候,去檢查是否發生了循環依賴(檢查的方式就是将正在建立的 A 标記出來,然後 B 需要 A,B 去建立 A 的時候,發現 A 正在建立,就說明發生了循環依賴),如果發生了循環依賴,就提前進行 AOP 處理,處理完成後再使用(三級緩存)。
原本 AOP 這個過程是屬性賦完值之後,再由各種後置處理器去處理 AOP 的(AbstractAutoProxyCreator),但是如果發生了循環依賴,就先 AOP,然後屬性指派,最後等到後置處理器執行的時候,就不再做 AOP 的處理了。

不過需要注意,三級緩存并不能解決所有的循環依賴,這個後面繼續整文章和大家細聊。