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

Spring 的循環(huán)依賴問題

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

什么是循環(huán)依賴呢?可以把它拆分成循環(huán)和依賴兩個部分來看,循環(huán)是指計算機領(lǐng)域中的循環(huán),執(zhí)行流程形成閉合回路;依賴就是完成這個動作的前提準備條件,和我們平常說的依賴大體上含義一致。放到 Spring 中來看就一個或多個 Bean 實例之間存在直接或間接的依賴關(guān)系,構(gòu)成循環(huán)調(diào)用,循環(huán)依賴可以分為直接循環(huán)依賴和間接循環(huán)依賴,直接循環(huán)依賴的簡單依賴場景:Bean A 依賴于 Bean B,然后 Bean B 又反過來依賴于 Bean A(Bean A -> Bean B -> Bean A),間接循環(huán)依賴的一個依賴場景:Bean A 依賴于 Bean B,Bean B 依賴于 Bean C,Bean C 依賴于 Bean A,中間多了一層,但是最終還是形成循環(huán)(Bean A -> Bean B -> Bean C -> Bean A)。

循環(huán)依賴的類型

第一種是自依賴,自己依賴自己從而形成循環(huán)依賴,一般情況下不會發(fā)生這種循環(huán)依賴,因為它很容易被我們發(fā)現(xiàn)。

1.png

第二種是直接依賴,發(fā)生在兩個對象之間,比如:Bean A 依賴于 Bean B,然后 Bean B 又反過來依賴于 Bean A,如果比較細心的話肉眼也不難發(fā)現(xiàn)。

2.png

第三種是間接依賴,這種依賴類型發(fā)生在 3 個或者以上的對象依賴的場景,間接依賴最簡單的場景:Bean A 依賴于 Bean B,Bean B 依賴于 Bean C,Bean C 依賴于 Bean A,可以想象當中間依賴的對象很多時,是很難發(fā)現(xiàn)這種循環(huán)依賴的,一般都是借助一些工具排查。

3.png

Spring 對幾種循環(huán)依賴場景支持情況

在介紹 Spring 對幾種循環(huán)依賴場景的處理方式之前,先來看看在 Spring 中循環(huán)依賴會有哪些場景,大部分常見的場景總結(jié)如下圖所示:

4.png

有句話說得好,源碼之下無秘密,下面就通過源碼探究這些場景 Spring 是否支持,以及支持的原因或者不支持的原因,話不多說,下面進入正題。

第 ① 種場景——單例 Bean 的 setter 注入

這種使用方式也是最常用的方式之一,假設(shè)有兩個 Service 分別為 OrderService(訂單相關(guān)業(yè)務(wù)邏輯)和 TradeService(交易相關(guān)業(yè)務(wù)邏輯),代碼如下:

/**
 * @author mghio
 * @since 2021-07-17
 */
@Service
public class OrderService {

  @Autowired
  private TradeService tradeService;

  public void testCreateOrder() {
    // omit business logic ...
  }

}

/**
 * @author mghio
 * @since 2021-07-17
 */
@Service
public class TradeService {

  @Autowired
  private OrderService orderService;

  public void testCreateTrade() { 
    // omit business logic ...
   }

}

這種循環(huán)依賴場景,程序是可以正常運行的,從代碼上看確實是有循環(huán)依賴了,也就是說 Spring 是支持這種循環(huán)依賴場景的,這里我們察覺不到循環(huán)依賴的原因是 Spring 已經(jīng)默默地解決了。

假設(shè)沒有做任何處理,按照正常的創(chuàng)建邏輯來執(zhí)行的話,流程是這樣的:容器先創(chuàng)建 OrderService,發(fā)現(xiàn)依賴于 TradeService,再創(chuàng)建 OrderService,又發(fā)現(xiàn)依賴于 TradeService ... ,發(fā)生無限死循環(huán),最后發(fā)生棧溢出錯誤,程序停止。為了支持這種常見的循環(huán)依賴場景,Spring 將創(chuàng)建對象分為如下幾個步驟:

  1. 實例化一個新對象(在堆中),但此時尚未給對象屬性賦值
  2. 給對象賦值
  3. 調(diào)用 BeanPostProcessor 的一些實現(xiàn)類的方法,在這個階段,Bean 已經(jīng)創(chuàng)建并賦值屬性完成。這時候容器中所有實現(xiàn) BeanPostProcessor 接口的類都會被調(diào)用(e.g. AOP)
  4. 初始化(如果實現(xiàn)了 InitializingBean,就會調(diào)用這個類的方法來完成類的初始化)
  5. 返回創(chuàng)建出來的實例

為此,Spring 引入了三級緩存來處理這個問題(三級緩存定義在 org.springframework.beans.factory.support.DefaultSingletonBeanRegistry 中),第一級緩存 singletonObjects 用于存放完全初始化好的 Bean,從該緩存中取出的 Bean 可以直接使用,第二級緩存 earlySingletonObjects 用于存放提前暴露的單例對象的緩存,存放原始的 Bean 對象(屬性尚未賦值),用于解決循環(huán)依賴,第三級緩存 singletonFactories 用于存放單例對象工廠的緩存,存放 Bean 工廠對象,用于解決循環(huán)依賴。上述實例使用三級緩存的處理流程如下所示:

5.png

如果你看過三級緩存的定義源碼的話,可能也有這樣的疑問:為什么第三級的緩存的要定義成 Map<String, ObjectFactory<?>>,不能直接緩存對象嗎?這里不能直接保存對象實例,因為這樣就無法對其做增強處理了。詳情可見類 org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#doCreateBean 方法部分源碼如下:

6.png

第 ② 種場景——多例 Bean 的 setter 注入

這種方式平常使用得相對較少,還是使用前文的兩個 Service 作為示例,唯一不同的地方是現(xiàn)在都聲明為多例了,示例代碼如下:

/**
 * @author mghio
 * @since 2021-07-17
 */
@Service
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class OrderService {

  @Autowired
  private TradeService tradeService;

  public void testCreateOrder() {
    // omit business logic ...
  }

}

/**
 * @author mghio
 * @since 2021-07-17
 */
@Service
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class TradeService {

  @Autowired
  private OrderService orderService;

  public void testCreateTrade() { 
    // omit business logic ...
   }

}

如果你在 Spring 中運行以上代碼,是可以正常啟動成功的,原因是在類 org.springframework.beans.factory.support.DefaultListableBeanFactory 的 preInstantiateSingletons() 方法預(yù)實例化處理時,過濾掉了多例類型的 Bean,方法部分代碼如下:

7.png

但是如果此時有其它單例類型的 Bean 依賴到這些多例類型的 Bean 的時候,就會報如下所示的循環(huán)依賴錯誤了。

8.png

第 ③ 種場景——代理對象的 setter 注入

這種場景也會經(jīng)常碰到,有時候為了實現(xiàn)異步調(diào)用會在 XXXXService 類的方法上添加 @Async 注解,讓方法對外部變成異步調(diào)用(前提要是要在啟用類上添加啟用注解哦 @EnableAsync),示例代碼如下:

/**
 * @author mghio
 * @since 2021-07-17
 */
@EnableAsync
@SpringBootApplication
public class BlogMghioCodeApplication {

  public static void main(String[] args) {
    SpringApplication.run(BlogMghioCodeApplication.class, args);
  }

}

/**
 * @author mghio
 * @since 2021-07-17
 */
@Service
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class OrderService {

  @Autowired
  private TradeService tradeService;

  @Async
  public void testCreateOrder() {
    // omit business logic ...
  }

}

/**
 * @author mghio
 * @since 2021-07-17
 */
@Service
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class TradeService {

  @Autowired
  private OrderService orderService;

  public void testCreateTrade() { 
    // omit business logic ...
   }

}

在標有 @Async 注解的場景下,在添加啟用異步注解(@EnableAsync)后,代理對象會通過 AOP 自動生成。以上代碼運行會拋出 BeanCurrentlyInCreationException 異常。運行的大致流程如下圖所示:

9.png

源碼在 org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory 類的方法 doCreateBean 中,會判斷第二級緩存 earlySingletonObjects 中的對象是否等于原始對象,方法判斷部分的源碼如下:

10.png

二級緩存存放的對象是 AOP 生成出來的代理對象,和原始對象不相等,所以拋出了循環(huán)依賴錯誤。如果細看源碼的話,會發(fā)現(xiàn)如果二級緩存是空的話會直接返回(因為比較的對象都沒有,根本無法校驗了),就不會報循環(huán)依賴的錯誤了,默認情況下,Spring 是按照文件全路徑遞歸搜索,按路徑 + 文件名 排序,排序靠前先加載,所以我們只要調(diào)整這兩個類名稱,讓方法標有 @Async 注解的類排序在后面即可。

第 ④ 種場景——構(gòu)造器注入

構(gòu)造器注入的場景很少,到目前為止我所接觸過的公司項目和開源項目中還沒遇到使用構(gòu)造器注入的,雖然用得不多,但是需要知道 Spring 為什么不支持這種場景的循環(huán)依賴,構(gòu)造器注入的示例代碼如下:

/**
 * @author mghio
 * @since 2021-07-17
 */
@Service
public class OrderService {

  private TradeService tradeService;

  public OrderService(TradeService tradeService) {
    this.tradeService = tradeService;
  }

  public void testCreateOrder() {
    // omit business logic ...
  }

}

/**
 * @author mghio
 * @since 2021-07-17
 */
@Service
public class TradeService {

  private OrderService orderService;

  public TradeService(OrderService orderService) {
    this.orderService = orderService;
  }

  public void testCreateTrade() {
    // omit business logic ...
  }

}

構(gòu)造器注入無法加入到第三級緩存當中,Spring 框架中的三級緩存在此場景下無用武之地,所以只能拋出異常,整體流程如下(虛線表示無法執(zhí)行,為了直觀也把下一步畫出來了):

11.png

第 ⑤ 種場景——DependsOn 循環(huán)依賴

這種 DependsOn 循環(huán)依賴場景很少,一般情況下不怎么使用,了解一下會導(dǎo)致循環(huán)依賴的問題即可,@DependsOn 注解主要是用來指定實例化順序的,示例代碼如下:

/**
 * @author mghio
 * @since 2021-07-17
 */
@Service
@DependsOn("tradeService")
public class OrderService {

  @Autowired
  private TradeService tradeService;

  public void testCreateOrder() {
    // omit business logic ...
  }

}

/**
 * @author mghio
 * @since 2021-07-17
 */
@Service
@DependsOn("orderService")
public class TradeService {

  @Autowired
  private OrderService orderService;

  public void testCreateTrade() {
    // omit business logic ...
  }

}

通過上文,我們知道,如果這里的類沒有標注 @DependsOn 注解的話是可以正常運行的,因為 Spring 支持單例 setter 注入,但是加了示例代碼的 @DependsOn 注解后會報循環(huán)依賴錯誤,原因是在類 org.springframework.beans.factory.support.AbstractBeanFactory 的方法 doGetBean() 中檢查了 dependsOn 的實例是否有循環(huán)依賴,如果有循環(huán)依賴則拋出循環(huán)依賴異常,方法判斷部分代碼如下:

12.png

總結(jié)

本文主要介紹了什么是循環(huán)依賴以及 Spring 對各種循環(huán)依賴場景的處理,文中只列出了部分涉及到的源碼,都標了所在源碼中的位置,感興趣的朋友可以去看看完整源碼,最后 Spring 對各種循環(huán)依賴場景的支持情況如下圖所示(P.S. Spring 版本:5.1.9.RELEASE):


13.png
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容