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

本文來(lái)源:https://my.oschina.net/zhangxufeng/blog/3096394

在關(guān)于Spring的面試中菌仁,我們經(jīng)常會(huì)被問(wèn)到一個(gè)問(wèn)題,就是Spring是如何解決循環(huán)依賴的問(wèn)題的零酪。這個(gè)問(wèn)題算是關(guān)于Spring的一個(gè)高頻面試題了讨,因?yàn)槿绻豢桃庋凶x,相信即使讀過(guò)源碼钧排,面試者也不一定能夠一下子思考出個(gè)中奧秘敦腔。本文主要針對(duì)這個(gè)問(wèn)題,從源碼的角度對(duì)其實(shí)現(xiàn)原理進(jìn)行講解恨溜。

  1. 過(guò)程演示

關(guān)于Spring bean的創(chuàng)建符衔,其本質(zhì)上還是一個(gè)對(duì)象的創(chuàng)建,既然是對(duì)象糟袁,讀者朋友一定要明白一點(diǎn)就是判族,一個(gè)完整的對(duì)象包含兩部分:當(dāng)前對(duì)象實(shí)例化和對(duì)象屬性的實(shí)例化。在Spring中项戴,對(duì)象的實(shí)例化是通過(guò)反射實(shí)現(xiàn)的形帮,而對(duì)象的屬性則是在對(duì)象實(shí)例化之后通過(guò)一定的方式設(shè)置的。這個(gè)過(guò)程可以按照如下方式進(jìn)行理解:


理解這一個(gè)點(diǎn)之后,對(duì)于循環(huán)依賴的理解就已經(jīng)幫助一大步了辩撑,我們這里以兩個(gè)類A和B為例進(jìn)行講解界斜,如下是A和B的聲明:

@Component
public class A {

  private B b;

  public void setB(B b) {
    this.b = b;
  }
}
@Component
public class B {

  private A a;

  public void setA(A a) {
    this.a = a;
  }
}

可以看到,這里A和B中各自都以對(duì)方為自己的全局屬性合冀。這里首先需要說(shuō)明的一點(diǎn)是各薇,Spring實(shí)例化bean是通過(guò)ApplicationContext.getBean()方法來(lái)進(jìn)行的。如果要獲取的對(duì)象依賴了另一個(gè)對(duì)象君躺,那么其首先會(huì)創(chuàng)建當(dāng)前對(duì)象峭判,然后通過(guò)遞歸的調(diào)用ApplicationContext.getBean()方法來(lái)獲取所依賴的對(duì)象,最后將獲取到的對(duì)象注入到當(dāng)前對(duì)象中棕叫。這里我們以上面的首先初始化A對(duì)象實(shí)例為例進(jìn)行講解林螃。首先Spring嘗試通過(guò)ApplicationContext.getBean()方法獲取A對(duì)象的實(shí)例,由于Spring容器中還沒(méi)有A對(duì)象實(shí)例俺泣,因而其會(huì)創(chuàng)建一個(gè)A對(duì)象疗认,然后發(fā)現(xiàn)其依賴了B對(duì)象,因而會(huì)嘗試遞歸的通過(guò)ApplicationContext.getBean()方法獲取B對(duì)象的實(shí)例砌滞,但是Spring容器中此時(shí)也沒(méi)有B對(duì)象的實(shí)例侮邀,因而其還是會(huì)先創(chuàng)建一個(gè)B對(duì)象的實(shí)例。讀者需要注意這個(gè)時(shí)間點(diǎn)贝润,此時(shí)A對(duì)象和B對(duì)象都已經(jīng)創(chuàng)建了绊茧,并且保存在Spring容器中了,只不過(guò)A對(duì)象的屬性b和B對(duì)象的屬性a都還沒(méi)有設(shè)置進(jìn)去打掘。在前面Spring創(chuàng)建B對(duì)象之后华畏,Spring發(fā)現(xiàn)B對(duì)象依賴了屬性A,因而此時(shí)還是會(huì)嘗試遞歸的調(diào)用ApplicationContext.getBean()方法獲取A對(duì)象的實(shí)例尊蚁,因?yàn)镾pring中已經(jīng)有一個(gè)A對(duì)象的實(shí)例亡笑,雖然只是半成品(其屬性b還未初始化),但其也還是目標(biāo)bean横朋,因而會(huì)將該A對(duì)象的實(shí)例返回仑乌。此時(shí),B對(duì)象的屬性a就設(shè)置進(jìn)去了琴锭,然后還是ApplicationContext.getBean()方法遞歸的返回晰甚,也就是將B對(duì)象的實(shí)例返回,此時(shí)就會(huì)將該實(shí)例設(shè)置到A對(duì)象的屬性b中决帖。這個(gè)時(shí)候厕九,注意A對(duì)象的屬性b和B對(duì)象的屬性a都已經(jīng)設(shè)置了目標(biāo)對(duì)象的實(shí)例了。讀者朋友可能會(huì)比較疑惑的是地回,前面在為對(duì)象B設(shè)置屬性a的時(shí)候扁远,這個(gè)A類型屬性還是個(gè)半成品俊鱼。但是需要注意的是,這個(gè)A是一個(gè)引用畅买,其本質(zhì)上還是最開(kāi)始就實(shí)例化的A對(duì)象并闲。而在上面這個(gè)遞歸過(guò)程的最后,Spring將獲取到的B對(duì)象實(shí)例設(shè)置到了A對(duì)象的屬性b中了皮获,這里的A對(duì)象其實(shí)和前面設(shè)置到實(shí)例B中的半成品A對(duì)象是同一個(gè)對(duì)象焙蚓,其引用地址是同一個(gè),這里為A對(duì)象的b屬性設(shè)置了值洒宝,其實(shí)也就是為那個(gè)半成品的a屬性設(shè)置了值。下面我們通過(guò)一個(gè)流程圖來(lái)對(duì)這個(gè)過(guò)程進(jìn)行講解:

image.png

圖中getBean()表示調(diào)用Spring的ApplicationContext.getBean()方法萌京,而該方法中的參數(shù)雁歌,則表示我們要嘗試獲取的目標(biāo)對(duì)象。圖中的黑色箭頭表示一開(kāi)始的方法調(diào)用走向知残,走到最后靠瞎,返回了Spring中緩存的A對(duì)象之后,表示遞歸調(diào)用返回了求妹,此時(shí)使用綠色的箭頭表示乏盐。從圖中我們可以很清楚的看到,B對(duì)象的a屬性是在第三步中注入的半成品A對(duì)象制恍,而A對(duì)象的b屬性是在第二步中注入的成品B對(duì)象父能,此時(shí)半成品的A對(duì)象也就變成了成品的A對(duì)象,因?yàn)槠鋵傩砸呀?jīng)設(shè)置完成了净神。

  1. 源碼講解

對(duì)于Spring處理循環(huán)依賴問(wèn)題的方式何吝,我們這里通過(guò)上面的流程圖其實(shí)很容易就可以理解,需要注意的一個(gè)點(diǎn)就是鹃唯,Spring是如何標(biāo)記開(kāi)始生成的A對(duì)象是一個(gè)半成品爱榕,并且是如何保存A對(duì)象的。這里的標(biāo)記工作Spring是使用ApplicationContext的屬性Set<String> singletonsCurrentlyInCreation來(lái)保存的坡慌,而半成品的A對(duì)象則是通過(guò)Map<String, ObjectFactory<?>> singletonFactories來(lái)保存的黔酥,這里的ObjectFactory是一個(gè)工廠對(duì)象,可通過(guò)調(diào)用其getObject()方法來(lái)獲取目標(biāo)對(duì)象洪橘。在AbstractBeanFactory.doGetBean()方法中獲取對(duì)象的方法如下:

這里的doGetBean()方法是非常關(guān)鍵的一個(gè)方法(中間省略了其他代碼)跪者,上面也主要有兩個(gè)步驟,第一個(gè)步驟的getSingleton()方法的作用是嘗試從緩存中獲取目標(biāo)對(duì)象梨树,如果沒(méi)有獲取到坑夯,則嘗試獲取半成品的目標(biāo)對(duì)象;如果第一個(gè)步驟沒(méi)有獲取到目標(biāo)對(duì)象的實(shí)例抡四,那么就進(jìn)入第二個(gè)步驟柜蜈,第二個(gè)步驟的getSingleton()方法的作用是嘗試創(chuàng)建目標(biāo)對(duì)象仗谆,并且為該對(duì)象注入其所依賴的屬性。

這里其實(shí)就是主干邏輯淑履,我們前面圖中已經(jīng)標(biāo)明隶垮,在整個(gè)過(guò)程中會(huì)調(diào)用三次doGetBean()方法,第一次調(diào)用的時(shí)候會(huì)嘗試獲取A對(duì)象實(shí)例秘噪,此時(shí)走的是第一個(gè)getSingleton()方法狸吞,由于沒(méi)有已經(jīng)創(chuàng)建的A對(duì)象的成品或半成品,因而這里得到的是null指煎,然后就會(huì)調(diào)用第二個(gè)getSingleton()方法蹋偏,創(chuàng)建A對(duì)象的實(shí)例,然后遞歸的調(diào)用doGetBean()方法至壤,嘗試獲取B對(duì)象的實(shí)例以注入到A對(duì)象中威始,此時(shí)由于Spring容器中也沒(méi)有B對(duì)象的成品或半成品,因而還是會(huì)走到第二個(gè)getSingleton()方法像街,在該方法中創(chuàng)建B對(duì)象的實(shí)例黎棠,創(chuàng)建完成之后,嘗試獲取其所依賴的A的實(shí)例作為其屬性镰绎,因而還是會(huì)遞歸的調(diào)用doGetBean()方法脓斩,此時(shí)需要注意的是,在前面由于已經(jīng)有了一個(gè)半成品的A對(duì)象的實(shí)例畴栖,因而這個(gè)時(shí)候随静,再嘗試獲取A對(duì)象的實(shí)例的時(shí)候,會(huì)走第一個(gè)getSingleton()方法驶臊,在該方法中會(huì)得到一個(gè)半成品的A對(duì)象的實(shí)例挪挤。然后將該實(shí)例返回,并且將其注入到B對(duì)象的屬性a中关翎,此時(shí)B對(duì)象實(shí)例化完成扛门。然后將實(shí)例化完成的B對(duì)象遞歸的返回,此時(shí)就會(huì)將該實(shí)例注入到A對(duì)象中纵寝,這樣就得到了一個(gè)成品的A對(duì)象论寨。我們這里可以閱讀上面的第一個(gè)getSingleton()方法:

到這里,Spring整個(gè)解決循環(huán)依賴問(wèn)題的實(shí)現(xiàn)思路已經(jīng)比較清楚了爽茴。對(duì)于整體過(guò)程葬凳,讀者朋友只要理解兩點(diǎn):

  • Spring是通過(guò)遞歸的方式獲取目標(biāo)bean及其所依賴的bean的;
  • Spring實(shí)例化一個(gè)bean的時(shí)候室奏,是分兩步進(jìn)行的火焰,首先實(shí)例化目標(biāo)bean,然后為其注入屬性胧沫。

結(jié)合這兩點(diǎn)昌简,也就是說(shuō)占业,Spring在實(shí)例化一個(gè)bean的時(shí)候,是首先遞歸的實(shí)例化其所依賴的所有bean纯赎,直到某個(gè)bean沒(méi)有依賴其他bean谦疾,此時(shí)就會(huì)將該實(shí)例返回,然后反遞歸的將獲取到的bean設(shè)置為各個(gè)上層bean的屬性的犬金。

  1. 小結(jié)

本文首先通過(guò)圖文的方式對(duì)Spring是如何解決循環(huán)依賴的問(wèn)題進(jìn)行了講解念恍,然后從源碼的角度詳細(xì)講解了Spring是如何實(shí)現(xiàn)各個(gè)bean的裝配工作的。


  1. 個(gè)人總結(jié)

Spring中循環(huán)依賴有2種情況:

1晚顷、構(gòu)造器注入形成的循環(huán)依賴峰伙。也就是bean B需要在bean A的構(gòu)造函數(shù)中完成初始化,bean A也需要在bean B的構(gòu)造函數(shù)中完成初始化音同,這種情況的結(jié)果就是兩個(gè)bean都無(wú)法完成初始化词爬,循環(huán)依賴難以解決。
2权均、setter注入形成的循環(huán)依賴。bean A需要在bean B的setter()方法中完成初始化锅锨,bean B也需要在bean A的setter()方法中完成初始化叽赊,Spring設(shè)計(jì)的機(jī)制主要就是解決這種循環(huán)依賴,這也是上文闡述的內(nèi)容必搞。

再此處需要提及一下必指,Spring中Bean的作用域:

在默認(rèn)情況下,Spring應(yīng)用上下文中所有bean都是以單例singleton 的形式創(chuàng)建的恕洲。也就是說(shuō)塔橡,不管給定的一個(gè)bean被注入到其他bean多少次, 每次所注入的都是同一個(gè)實(shí)例霜第。

在大多數(shù)情況下葛家, 單例bean是很理想的方案。 初始化和垃圾回收對(duì)象實(shí)例所帶來(lái)的成本只留給一些小規(guī)模任務(wù)泌类, 在這些任務(wù)中癞谒, 讓對(duì)象保持無(wú)狀態(tài)并且在應(yīng)用中反復(fù)重用這些對(duì)象可能并不合理。

有時(shí)候刃榨,可能會(huì)發(fā)現(xiàn)弹砚,你所使用的類是易變的mutable,它們會(huì)保持一些狀態(tài)枢希,因此重用是不安全的桌吃。在這種情況下,將class聲明為單例的bean就不再是好的方案了苞轿,因?yàn)閷?duì)象可能會(huì)被改變茅诱, 稍后重用的時(shí)候會(huì)出現(xiàn)意想不到的問(wèn)題逗物。

Spring中定義了多種作用域, 可以基于這些作用域創(chuàng)建bean让簿, 包括:

  • 單例Singleton:在整個(gè)應(yīng)用中敬察, 只創(chuàng)建bean的一個(gè)實(shí)例。
  • 原型Prototype: 每次注入或者通過(guò)Spring應(yīng)用上下文獲取的時(shí)候尔当, 都會(huì)創(chuàng)建一個(gè)新的bean實(shí)例莲祸。
  • 會(huì)話Session: 在Web應(yīng)用中, 為每個(gè)會(huì)話創(chuàng)建一個(gè)bean實(shí)例椭迎。
  • 請(qǐng)求Request : 在Web應(yīng)用中锐帜, 為每個(gè)請(qǐng)求創(chuàng)建一個(gè)bean實(shí)例。

單例是默認(rèn)的作用域畜号, 但是正如之前所述缴阎, 對(duì)于易變的類型, 這并不合適简软。 如果選擇其他的作用域蛮拔, 要使用@Scope注解, 它可以與@Component@Bean一起使用痹升。

例如建炫, 如果你使用組件掃描來(lái)發(fā)現(xiàn)和聲明bean, 那么你可以在bean的
類上使用@Scope注解疼蛾, 將其聲明為原型bean:

@Component
@Scope{"prototype"}
public class Notepad{    }

當(dāng)然你也可以使用@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)肛跌,但是使用SCOPE_PROTOTYPE常量更加安全并不易出錯(cuò)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末察郁,一起剝皮案震驚了整個(gè)濱河市衍慎,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌皮钠,老刑警劉巖稳捆,帶你破解...
    沈念sama閱讀 222,000評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異鳞芙,居然都是意外死亡眷柔,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,745評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門原朝,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)驯嘱,“玉大人,你說(shuō)我怎么就攤上這事喳坠【掀溃” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 168,561評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵壕鹉,是天一觀的道長(zhǎng)剃幌。 經(jīng)常有香客問(wèn)我聋涨,道長(zhǎng),這世上最難降的妖魔是什么负乡? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,782評(píng)論 1 298
  • 正文 為了忘掉前任牍白,我火速辦了婚禮,結(jié)果婚禮上抖棘,老公的妹妹穿的比我還像新娘茂腥。我一直安慰自己,他們只是感情好切省,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,798評(píng)論 6 397
  • 文/花漫 我一把揭開(kāi)白布最岗。 她就那樣靜靜地躺著,像睡著了一般朝捆。 火紅的嫁衣襯著肌膚如雪般渡。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 52,394評(píng)論 1 310
  • 那天芙盘,我揣著相機(jī)與錄音驯用,去河邊找鬼。 笑死儒老,一個(gè)胖子當(dāng)著我的面吹牛晨汹,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播贷盲,決...
    沈念sama閱讀 40,952評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼剥扣!你這毒婦竟也來(lái)了巩剖?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,852評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤钠怯,失蹤者是張志新(化名)和其女友劉穎佳魔,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體晦炊,經(jīng)...
    沈念sama閱讀 46,409評(píng)論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡鞠鲜,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,483評(píng)論 3 341
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了断国。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片贤姆。...
    茶點(diǎn)故事閱讀 40,615評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖稳衬,靈堂內(nèi)的尸體忽然破棺而出霞捡,到底是詐尸還是另有隱情,我是刑警寧澤薄疚,帶...
    沈念sama閱讀 36,303評(píng)論 5 350
  • 正文 年R本政府宣布碧信,位于F島的核電站赊琳,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏砰碴。R本人自食惡果不足惜躏筏,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,979評(píng)論 3 334
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望呈枉。 院中可真熱鬧趁尼,春花似錦、人聲如沸碴卧。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,470評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)住册。三九已至婶博,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間荧飞,已是汗流浹背凡人。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,571評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留叹阔,地道東北人挠轴。 一個(gè)月前我還...
    沈念sama閱讀 49,041評(píng)論 3 377
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像耳幢,于是被迫代替她去往敵國(guó)和親岸晦。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,630評(píng)論 2 359