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

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

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

什么是循環(huán)依賴呢挟秤?可以把它拆分成循環(huán)和依賴兩個(gè)部分來(lái)看凛澎,循環(huán)是指計(jì)算機(jī)領(lǐng)域中的循環(huán)双妨,執(zhí)行流程形成閉合回路隐锭;依賴就是完成這個(gè)動(dòng)作的前提準(zhǔn)備條件觉阅,和我們平常說(shuō)的依賴大體上含義一致缔恳。放到 Spring 中來(lái)看就一個(gè)或多個(gè) Bean 實(shí)例之間存在直接或間接的依賴關(guān)系猪杭,構(gòu)成循環(huán)調(diào)用樊拓,循環(huán)依賴可以分為直接循環(huán)依賴和間接循環(huán)依賴纠亚,直接循環(huán)依賴的簡(jiǎn)單依賴場(chǎng)景:Bean A 依賴于 Bean B,然后 Bean B 又反過(guò)來(lái)依賴于 Bean A(Bean A -> Bean B -> Bean A)筋夏,間接循環(huán)依賴的一個(gè)依賴場(chǎng)景: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)依賴鸿染,一般情況下不會(huì)發(fā)生這種循環(huán)依賴,因?yàn)樗苋菀妆晃覀儼l(fā)現(xiàn)乞巧。

1.png

第二種是直接依賴涨椒,發(fā)生在兩個(gè)對(duì)象之間,比如:Bean A 依賴于 Bean B绽媒,然后 Bean B 又反過(guò)來(lái)依賴于 Bean A蚕冬,如果比較細(xì)心的話肉眼也不難發(fā)現(xiàn)。

2.png

第三種是間接依賴是辕,這種依賴類型發(fā)生在 3 個(gè)或者以上的對(duì)象依賴的場(chǎng)景囤热,間接依賴最簡(jiǎn)單的場(chǎng)景:Bean A 依賴于 Bean B,Bean B 依賴于 Bean C获三,Bean C 依賴于 Bean A旁蔼,可以想象當(dāng)中間依賴的對(duì)象很多時(shí)锨苏,是很難發(fā)現(xiàn)這種循環(huán)依賴的,一般都是借助一些工具排查牌芋。

3.png

Spring 對(duì)幾種循環(huán)依賴場(chǎng)景支持情況

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

4.png

有句話說(shuō)得好躺屁,源碼之下無(wú)秘密,下面就通過(guò)源碼探究這些場(chǎng)景 Spring 是否支持经宏,以及支持的原因或者不支持的原因犀暑,話不多說(shuō),下面進(jìn)入正題烁兰。

第 ① 種場(chǎng)景——單例 Bean 的 setter 注入

這種使用方式也是最常用的方式之一耐亏,假設(shè)有兩個(gè) 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)依賴場(chǎng)景沪斟,程序是可以正常運(yùn)行的广辰,從代碼上看確實(shí)是有循環(huán)依賴了抽碌,也就是說(shuō) Spring 是支持這種循環(huán)依賴場(chǎng)景的贺奠,這里我們察覺不到循環(huán)依賴的原因是 Spring 已經(jīng)默默地解決了纠俭。

假設(shè)沒有做任何處理铜涉,按照正常的創(chuàng)建邏輯來(lái)執(zhí)行的話锌杀,流程是這樣的:容器先創(chuàng)建 OrderService稻励,發(fā)現(xiàn)依賴于 TradeService邓深,再創(chuàng)建 OrderService浓冒,又發(fā)現(xiàn)依賴于 TradeService ... 粤攒,發(fā)生無(wú)限死循環(huán)所森,最后發(fā)生棧溢出錯(cuò)誤,程序停止夯接。為了支持這種常見的循環(huán)依賴場(chǎng)景焕济,Spring 將創(chuàng)建對(duì)象分為如下幾個(gè)步驟:

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

為此肝匆,Spring 引入了三級(jí)緩存來(lái)處理這個(gè)問(wèn)題(三級(jí)緩存定義在 org.springframework.beans.factory.support.DefaultSingletonBeanRegistry 中),第一級(jí)緩存 singletonObjects 用于存放完全初始化好的 Bean顺献,從該緩存中取出的 Bean 可以直接使用旗国,第二級(jí)緩存 earlySingletonObjects 用于存放提前暴露的單例對(duì)象的緩存,存放原始的 Bean 對(duì)象(屬性尚未賦值)注整,用于解決循環(huán)依賴能曾,第三級(jí)緩存 singletonFactories 用于存放單例對(duì)象工廠的緩存度硝,存放 Bean 工廠對(duì)象,用于解決循環(huán)依賴寿冕。上述實(shí)例使用三級(jí)緩存的處理流程如下所示:

5.png

如果你看過(guò)三級(jí)緩存的定義源碼的話蕊程,可能也有這樣的疑問(wèn):為什么第三級(jí)的緩存的要定義成 Map<String, ObjectFactory<?>>,不能直接緩存對(duì)象嗎驼唱?這里不能直接保存對(duì)象實(shí)例藻茂,因?yàn)檫@樣就無(wú)法對(duì)其做增強(qiáng)處理了。詳情可見類 org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#doCreateBean 方法部分源碼如下:

6.png

第 ② 種場(chǎng)景——多例 Bean 的 setter 注入

這種方式平常使用得相對(duì)較少玫恳,還是使用前文的兩個(gè) 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 中運(yùn)行以上代碼京办,是可以正常啟動(dòng)成功的掀序,原因是在類 org.springframework.beans.factory.support.DefaultListableBeanFactory 的 preInstantiateSingletons() 方法預(yù)實(shí)例化處理時(shí),過(guò)濾掉了多例類型的 Bean惭婿,方法部分代碼如下:

7.png

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

8.png

第 ③ 種場(chǎng)景——代理對(duì)象的 setter 注入

這種場(chǎng)景也會(huì)經(jīng)常碰到财饥,有時(shí)候?yàn)榱藢?shí)現(xiàn)異步調(diào)用會(huì)在 XXXXService 類的方法上添加 @Async 注解换吧,讓方法對(duì)外部變成異步調(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 ...
   }

}

在標(biāo)有 @Async 注解的場(chǎng)景下佑力,在添加啟用異步注解(@EnableAsync)后式散,代理對(duì)象會(huì)通過(guò) AOP 自動(dòng)生成。以上代碼運(yùn)行會(huì)拋出 BeanCurrentlyInCreationException 異常打颤。運(yùn)行的大致流程如下圖所示:

9.png

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

10.png

二級(jí)緩存存放的對(duì)象是 AOP 生成出來(lái)的代理對(duì)象编饺,和原始對(duì)象不相等乖篷,所以拋出了循環(huán)依賴錯(cuò)誤。如果細(xì)看源碼的話透且,會(huì)發(fā)現(xiàn)如果二級(jí)緩存是空的話會(huì)直接返回(因?yàn)楸容^的對(duì)象都沒有撕蔼,根本無(wú)法校驗(yàn)了),就不會(huì)報(bào)循環(huán)依賴的錯(cuò)誤了秽誊,默認(rèn)情況下鲸沮,Spring 是按照文件全路徑遞歸搜索,按路徑 + 文件名 排序锅论,排序靠前先加載讼溺,所以我們只要調(diào)整這兩個(gè)類名稱,讓方法標(biāo)有 @Async 注解的類排序在后面即可最易。

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

構(gòu)造器注入的場(chǎng)景很少怒坯,到目前為止我所接觸過(guò)的公司項(xiàng)目和開源項(xiàng)目中還沒遇到使用構(gòu)造器注入的炫狱,雖然用得不多,但是需要知道 Spring 為什么不支持這種場(chǎng)景的循環(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)造器注入無(wú)法加入到第三級(jí)緩存當(dāng)中视译,Spring 框架中的三級(jí)緩存在此場(chǎng)景下無(wú)用武之地,所以只能拋出異常归敬,整體流程如下(虛線表示無(wú)法執(zhí)行酷含,為了直觀也把下一步畫出來(lái)了):

11.png

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

這種 DependsOn 循環(huán)依賴場(chǎng)景很少,一般情況下不怎么使用弄慰,了解一下會(huì)導(dǎo)致循環(huán)依賴的問(wèn)題即可第美,@DependsOn 注解主要是用來(lái)指定實(shí)例化順序的蝶锋,示例代碼如下:

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

}

通過(guò)上文陆爽,我們知道,如果這里的類沒有標(biāo)注 @DependsOn 注解的話是可以正常運(yùn)行的扳缕,因?yàn)?Spring 支持單例 setter 注入慌闭,但是加了示例代碼的 @DependsOn 注解后會(huì)報(bào)循環(huán)依賴錯(cuò)誤,原因是在類 org.springframework.beans.factory.support.AbstractBeanFactory 的方法 doGetBean() 中檢查了 dependsOn 的實(shí)例是否有循環(huán)依賴躯舔,如果有循環(huán)依賴則拋出循環(huán)依賴異常驴剔,方法判斷部分代碼如下:

12.png

總結(jié)

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


13.png
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末惜互,一起剝皮案震驚了整個(gè)濱河市布讹,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌训堆,老刑警劉巖描验,帶你破解...
    沈念sama閱讀 211,817評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異坑鱼,居然都是意外死亡膘流,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,329評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門鲁沥,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)呼股,“玉大人,你說(shuō)我怎么就攤上這事画恰∨硭” “怎么了?”我有些...
    開封第一講書人閱讀 157,354評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵阐枣,是天一觀的道長(zhǎng)马靠。 經(jīng)常有香客問(wèn)我奄抽,道長(zhǎng),這世上最難降的妖魔是什么甩鳄? 我笑而不...
    開封第一講書人閱讀 56,498評(píng)論 1 284
  • 正文 為了忘掉前任逞度,我火速辦了婚禮,結(jié)果婚禮上妙啃,老公的妹妹穿的比我還像新娘档泽。我一直安慰自己,他們只是感情好揖赴,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,600評(píng)論 6 386
  • 文/花漫 我一把揭開白布馆匿。 她就那樣靜靜地躺著,像睡著了一般燥滑。 火紅的嫁衣襯著肌膚如雪渐北。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,829評(píng)論 1 290
  • 那天铭拧,我揣著相機(jī)與錄音赃蛛,去河邊找鬼。 笑死搀菩,一個(gè)胖子當(dāng)著我的面吹牛呕臂,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播肪跋,決...
    沈念sama閱讀 38,979評(píng)論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼歧蒋,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了州既?” 一聲冷哼從身側(cè)響起谜洽,我...
    開封第一講書人閱讀 37,722評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎易桃,沒想到半個(gè)月后褥琐,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,189評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡晤郑,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,519評(píng)論 2 327
  • 正文 我和宋清朗相戀三年敌呈,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片造寝。...
    茶點(diǎn)故事閱讀 38,654評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡磕洪,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出诫龙,到底是詐尸還是另有隱情析显,我是刑警寧澤,帶...
    沈念sama閱讀 34,329評(píng)論 4 330
  • 正文 年R本政府宣布签赃,位于F島的核電站谷异,受9級(jí)特大地震影響分尸,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜歹嘹,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,940評(píng)論 3 313
  • 文/蒙蒙 一箩绍、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧尺上,春花似錦材蛛、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,762評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至马绝,卻和暖如春豆赏,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背迹淌。 一陣腳步聲響...
    開封第一講書人閱讀 31,993評(píng)論 1 266
  • 我被黑心中介騙來(lái)泰國(guó)打工河绽, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人唉窃。 一個(gè)月前我還...
    沈念sama閱讀 46,382評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像纹笼,于是被迫代替她去往敵國(guó)和親纹份。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,543評(píng)論 2 349

推薦閱讀更多精彩內(nèi)容