1. 程式人生 > 其它 >Spring 的迴圈依賴問題

Spring 的迴圈依賴問題

什麼是迴圈依賴

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

迴圈依賴的型別

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

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

第三種是間接依賴,這種依賴型別發生在 3 個或者以上的物件依賴的場景,間接依賴最簡單的場景:Bean A 依賴於 Bean B,Bean B 依賴於 Bean C,Bean 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.DefaultListableBeanFactory 的 preInstantiateSingletons() 方法預例項化處理時,過濾掉了多例型別的 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):

Java 搬運工 & 終身學習者 @ 微信公眾號「mghio」