Spring 的循環依賴問題

什麼是循環依賴

什麼是循環依賴呢?可以把它拆分成循環依賴兩個部分來看,循環是指計算機領域中的循環,執行流程形成閉合迴路;依賴就是完成這個動作的前提準備條件,和我們平常說的依賴大體上含義一致。放到 Spring 中來看就一個或多個 Bean 實例之間存在直接或間接的依賴關係,構成循環調用,循環依賴可以分爲直接循環依賴間接循環依賴,直接循環依賴的簡單依賴場景:Bean A 依賴於 Bean B,然後 Bean B 又反過來依賴於 Bean ABean A -> Bean B -> Bean A),間接循環依賴的一個依賴場景:Bean A 依賴於 Bean BBean B 依賴於 Bean CBean C 依賴於 Bean A,中間多了一層,但是最終還是形成循環(Bean A -> Bean B -> Bean C -> Bean A)。

循環依賴的類型

第一種是自依賴,自己依賴自己從而形成循環依賴,一般情況下不會發生這種循環依賴,因爲它很容易被我們發現。

第二種是直接依賴,發生在兩個對象之間,比如:Bean A 依賴於 Bean B,然後 Bean B 又反過來依賴於 Bean A,如果比較細心的話肉眼也不難發現。

第三種是間接依賴,這種依賴類型發生在 3 個或者以上的對象依賴的場景,間接依賴最簡單的場景:Bean A 依賴於 Bean BBean B 依賴於 Bean CBean C 依賴於 Bean A,可以想象當中間依賴的對象很多時,是很難發現這種循環依賴的,一般都是藉助一些工具排查。

Spring 對幾種循環依賴場景支持情況

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

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

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

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

/**
 * @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 ...
   }

}

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

假設沒有做任何處理,按照正常的創建邏輯來執行的話,流程是這樣的:容器先創建 OrderService,發現依賴於 TradeService,再創建 OrderService,又發現依賴於 TradeService ... ,發生無限死循環,最後發生棧溢出錯誤,程序停止。爲了支持這種常見的循環依賴場景,Spring 將創建對象分爲如下幾個步驟:

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

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

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


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

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

/**
 * @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.DefaultListableBeanFactorypreInstantiateSingletons() 方法預實例化處理時,過濾掉了多例類型的 Bean,方法部分代碼如下:

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


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

這種場景也會經常碰到,有時候爲了實現異步調用會在 XXXXService 類的方法上添加 @Async 註解,讓方法對外部變成異步調用(前提要是要在啓用類上添加啓用註解哦 @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 異常。運行的大致流程如下圖所示:

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

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


第 ④ 種場景——構造器注入

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

/**
 * @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 ...
  }

}

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


第 ⑤ 種場景——DependsOn 循環依賴

這種 DependsOn 循環依賴場景很少,一般情況下不怎麼使用,瞭解一下會導致循環依賴的問題即可,@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 註解後會報循環依賴錯誤,原因是在類 org.springframework.beans.factory.support.AbstractBeanFactory 的方法 doGetBean() 中檢查了 dependsOn 的實例是否有循環依賴,如果有循環依賴則拋出循環依賴異常,方法判斷部分代碼如下:

總結

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章