Spring Bean生命周期之三級緩存循環(huán)依賴

1 三級緩存

在使用 spring框架的日常開發(fā)中, bean之間的循環(huán)依賴太頻繁了皿桑, spring已經幫我們去解決循環(huán)依賴問題掰伸,對我們開發(fā)者來說是無感知的站辉,下面具體分析一下 spring是如何解決bean之間循環(huán)依賴,為什么要使用到三級緩存兑巾,而不是二級緩存条获?
點擊了解 Spring Bean生命周期之概述

1.1 引言

必須先對bean的生命周期做了一個整體的流程分析,對spring如何去解決循環(huán)依賴的很有幫助蒋歌。前面我們分析到填充屬性時帅掘,如果發(fā)現屬性還未在spring中生成,則會跑去生成屬性對象實例堂油。

在這里插入圖片描述

我們可以看到填充屬性的時候修档,spring會提前將已經實例化的bean通過ObjectFactory半成品暴露出去,為什么稱為半成品是因為這時候的bean對象實例化府框,但是未進行屬性填充吱窝,是一個不完整的bean實例對象
實例化 Bean 之后,會往 singletonFactories 塞入一個工廠迫靖,而調用這個工廠的 getObject 方法院峡,就能得到這個 Bean

在這里插入圖片描述

spring利用singletonObjects, earlySingletonObjects, singletonFactories三級緩存去解決的,所說的緩存其實也就是三個Map

1.2 三級緩存各個存放對象

三級緩存各個存放對象:

  • 一級緩存系宜,singletonObjects照激,存儲所有已創(chuàng)建完畢的單例 Bean (完整的 Bean)
  • 二級緩存earlySingletonObjects盹牧,存儲所有僅完成實例化俩垃,但還未進行屬性注入和初始化的 Bean
  • 三級緩存singletonFactories汰寓,存儲能建立這個 Bean 的一個工廠口柳,通過工廠能獲取這個 Bean,延遲化 Bean 的生成有滑,工廠生成的 Bean 會塞入二級緩存

這三個 map 是如何獲取配合的:

  1. 獲取單例 Bean 的時候會通過 BeanName 先去 singletonObjects(一級緩存) 查找完整的 Bean跃闹,如果找到則直接返回,否則進行步驟 2俺孙。
  2. 看對應的 Bean 是否在創(chuàng)建中,如果不在直接返回找不到掷贾,如果是睛榄,則會去 earlySingletonObjects (二級緩存)查找 Bean,如果找到則返回想帅,否則進行步驟 3
  3. singletonFactories (三級緩存)通過BeanName 查找到對應的工廠场靴,如果存著工廠則通過工廠創(chuàng)建 Bean ,放置到二級緩存earlySingletonObjects 中,并把三級緩存中給移除掉旨剥。
  4. 如果三個緩存都沒找到咧欣,則返回 null


    在這里插入圖片描述

可以看到三級緩存各自保存的對象,這里重點關注二級緩存earlySingletonObjects和三級緩存singletonFactory轨帜,一級緩存可以進行忽略魄咕。前面我們講過先實例化的bean會通過ObjectFactory半成品提前暴露在三級緩存中

在這里插入圖片描述

singletonFactory是傳入的一個匿名內部類,調用ObjectFactory.getObject()最終會調用getEarlyBeanReference方法蚌父。再來看看循環(huán)依賴中是怎么拿其它半成品的實例對象的哮兰。

1.3 解決循環(huán)依賴條件

1.3.1 解決循環(huán)依賴條件

Spring 中,只有同時滿足以下兩點才能解決循環(huán)依賴的問題:

  • 必須是單例
    依賴的 Bean 必須都是單例
    因為原型模式都需要創(chuàng)建新的對象苟弛,不能跟用以前的對象
  • 不能全是構造器注入
    依賴注入的方式喝滞,必須不全是構造器注入,且 beanName字母順序在前的不能是構造器注入
    在 Spring 中創(chuàng)建 Bean 分三步:
    實例化膏秫,createBeanInstance右遭,就是 new 了個對象
    屬性注入,populateBean缤削, 就是 set 一些屬性值
    初始化窘哈,initializeBean,執(zhí)行一些 aware 接口中的方法僻他,initMethod宵距,AOP代理等
    明確了上面這三點,再結合我上面說的“不完整的”吨拗,我們來理一下满哪。
    如果全是構造器注入,比如A(B b)劝篷,那表明在 new 的時候哨鸭,就需要得到 B,此時需要 new B 娇妓。但是 B 也是要在構造的時候注入 A 像鸡,即B(A a),這時候 B 需要在一個 map 中找到不完整的 A 哈恰,發(fā)現找不到只估。
    為什么找不到?因為 A 還沒 new 完呢着绷,所以找到不完整的 A蛔钙,因此如果全是構造器注入的話,那么 Spring 無法處理循環(huán)依賴
  • 一個set注入荠医,一個構造器注入能否成功
    假設我們 A 是通過 set 注入 B吁脱,B 通過構造函數注入 A桑涎,此時是成功的
    我們來分析下:實例化 A 之后,可以在 map 中存入 A兼贡,開始為 A 進行屬性注入攻冷,發(fā)現需要 B,此時 new B遍希,發(fā)現構造器需要 A等曼,此時從 map 中得到 A ,B 構造完畢孵班。
    B 進行屬性注入涉兽,初始化,然后 A 注入 B 完成屬性注入篙程,然后初始化 A枷畏。
    整個過程很順利,沒毛病
    假設 A 是通過構造器注入 B虱饿,B 通過 set 注入 A拥诡,此時是失敗的
    我們來分析下:實例化 A,發(fā)現構造函數需要 B氮发, 此時去實例化 B渴肉。
    然后進行 B 的屬性注入,從 map 里面找不到 A爽冕,因為 A 還沒 new 成功仇祭,所以 B 也卡住了,然后就 失敗
    看到這里颈畸,仔細思考的小伙伴可能會說乌奇,可以先實例化 B 啊,往 map 里面塞入不完整的 B眯娱,這樣就能成功實例化 A 了啊
    確實礁苗,思路沒錯但是 Spring 容器是按照字母序創(chuàng)建 Bean 的,A 的創(chuàng)建永遠排在 B 前面

現在我們總結一下:

  • 如果循環(huán)依賴都是構造器注入徙缴,則失敗
  • 如果循環(huán)依賴不完全是構造器注入试伙,則可能成功,可能失敗于样,具體跟BeanName的字母序有關系

1.3.2 Sprin中Bean的順序

spring容器載入bean順序是不確定的疏叨,在一定的范圍內bean的加載順序可以控制。
spring容器載入bean雖然順序不確定穿剖,但遵循一定的規(guī)則:

  • 按照字母順序加載(同一文件夾下按照字母順序蚤蔓;不同文件夾下,先按照文件夾命名的字母順序加載)
  • 不同的bean聲明方式不同的加載時機携御,順序總結:@ComponentScan > @Import > @Bean
    這里的ComponentScan@ComponentScan及其子注解昌粤,Bean指的是@configuration + @bean
  • 同時需要注意的是:
    • Component及其子注解申明的bean是按照字母順序加載的
    • @configuration + @bean是按照定義的順序依次加載的
    • @import的順序,就是bean的加載順序
    • xml中啄刹,通過<bean id="">方式聲明的bean也是按照代碼的編寫順序依次加載的
    • 同一類中加載順序:Constructor >> @Autowired >> @PostConstruct >> @Bean
    • 同一類中加載順序:靜態(tài)變量 / 靜態(tài)代碼塊 >> 構造代碼塊 >> 構造方法(需要特別注意的是靜態(tài)代碼塊的執(zhí)行并不是優(yōu)先所有的bean加載涮坐,只是在同一個類中,靜態(tài)代碼塊優(yōu)先加載)

1.3.3 更改加載順序

特別情況下誓军,如果想手動控制部分bean的加載順序袱讹,有如下方法:

1.3.3.1 構造方法依賴 (推薦)

@Component
public class CDemo1 {
    private String name = "cdemo 1";

    public CDemo1(CDemo2 cDemo2) {
        System.out.println(name);
    }
}

@Component
public class CDemo2 {
    private String name = "cdemo 2";

    public CDemo2() {
        System.out.println(name);
    }
}

CDemo2CDemo1之前被初始化。

注意
要有注入關系昵时,如:CDemo2通過構造方法注入到CDemo1中捷雕,若需要指定兩個沒有注入關系的bean之間優(yōu)先級,則不太合適(比如我希望某個bean在所有其他的Bean初始化之前執(zhí)行)
循環(huán)依賴問題壹甥,如過上面的CDemo2的構造方法有一個CDemo1參數救巷,那么循環(huán)依賴產生,應用無法啟動
另外一個需要注意的點是句柠,在構造方法中浦译,不應有復雜耗時的邏輯,會拖慢應用的啟動時間

1.3.3.2 參數注入

@Bean標注的方法上溯职,如果傳入了參數精盅,springboot會自動會為這個參數在spring上下文里尋找這個類型的引用。并先初始化這個類的實例谜酒。
利用此特性叹俏,我們也可以控制bean的加載順序。

@Bean
public BeanA beanA(BeanB beanB){
    System.out.println("bean A init");
    return new BeanA();
}

@Bean
public BeanB beanB(){
    System.out.println("bean B init");
    return new BeanB();
}

以上結果僻族,beanB先于beanA被初始化加載粘驰。
需要注意的是,springboot會按類型去尋找鹰贵。如果這個類型有多個實例被注冊到spring上下文晴氨,那就需要加上@Qualifier(“Bean的名稱”)來指定

1.3.3.3 @DependsOn(“xxx”)

沒有直接的依賴關系的,可以通過@DependsOn注解碉输,我們可以在bean A上使用@DependsOn注解 籽前,告訴容器bean B應該優(yōu)先被加載初始化。
不推薦的原因:這種方法是通過bean的名字(字符串)來控制順序的敷钾,如果改了bean的類名枝哄,很可能就會忘記來改所有用到它的注解,那就問題大了阻荒。

當一個bean需要在另一個bean實例化之后再實例化時挠锥,可使用這個注解。

@Component("dependson02")
public class Dependson02 {
 
    Dependson02(){
        System.out.println(" dependson02 Success ");
    }
}

@Component
@DependsOn("dependson02")
public class Dependson01 {
 
    Dependson01(){
        System.out.println("Dependson01 success");
    }
}

執(zhí)行結果:
dependson02 Success 
Dependson01 success

1.3.3.4 BeanDefinitionRegistryPostProcessor接口

通過實現BeanDefinitionRegistryPostProcessor接口侨赡,在postProcessBeanDefinitionRegistry方法中通過BeanDefinitionRegistry獲取到所有bean的注冊信息蓖租,將bean保存到LinkedHashMap中粱侣,并從BeanDefinitionRegistry中刪除,然后將保存的bean定義排序后蓖宦,重新再注冊到BeanDefinitionRegistry中齐婴,即可實現bean加載順序的控制。

參考于:https://blog.csdn.net/u014365523/article/details/127101157

1.3.4 執(zhí)行順序@Order

注解@Order或者接口Ordered的作用是定義Spring IOC容器中Bean的執(zhí)行順序的優(yōu)先級稠茂,而不是定義Bean的加載順序柠偶,Bean的加載順序不受@OrderOrdered接口的影響,@Order不控制Spring初始化順序

@Order(1)order的值越小越是最先執(zhí)行睬关,但更重要的是最先執(zhí)行的最后結束

以下內容選自官網:
https://docs.spring.io/spring-framework/docs/5.3.24/reference/html/core.html#spring-core

目標bean可以實現org.springframework.core.Ordered接口诱担,如果希望數組或列表中的項按特定順序排序,也可以使用@Order或標準@Priority注釋电爹。否則蔫仙,它們的順序將遵循容器中相應目標bean定義的注冊順序。
您可以在目標類級別和@Bean方法上聲明@Order注釋丐箩,可能用于單個bean定義(在使用相同bean類的多個定義的情況下)匀哄。@Order值可能會影響注入點的優(yōu)先級,但請注意雏蛮,它們不會影響單例啟動順序涎嚼,這是由依賴關系和@DependsOn聲明確定的正交關注點。
注意挑秉,標準的javax.annotation.Priority注釋在@Bean級別上是不可用的法梯,因為它不能在方法上聲明。它的語義可以通過在每個類型的單個bean上結合@Order值和@Primary來建模犀概。

@Component
@Order(0)
public class Test01 {
   ...
}

@Component
@Order(1)
public class Test02 {
   ...
}

@Component
@Order(2)
public class Test03 {
   ...
}

如上述代碼所示立哑,通過@Order注解定義優(yōu)先級,3個Bean對象從IOC容器中的執(zhí)行載順序為:Test01姻灶、Test02铛绰、Test03

1.3.5 延遲注入@Lazy

假設有如下情景:

類A依賴于類B,同時類B也依賴于類A产喉。這樣就形成了循環(huán)依賴捂掰。

為了解決這個問題,還以可以使用 @Lazy 注解曾沈,將類A或類B中的其中一個延遲加載这嚣。
例如,我們可以在類A中使用 @Lazy 注解塞俱,將類A延遲加載姐帚,這樣在啟動應用程序時,Spring容器不會立即加載類A障涯,而是在需要使用類A的時候才會進行加載罐旗。這樣就避免了循環(huán)依賴的問題膳汪。

示例代碼如下:

@Component
public class A {
    private final B b;
    public A(@Lazy B b) {
        this.b = b;
    }
    //...
}

@Component
public class B {
    private final A a;
    public B(A a) {
        this.a = a;
    }
    //...
}

在類A中,我們使用了 @Lazy 注解九秀,將類B延遲加載旅敷。這樣在啟動應用程序時,Spring容器不會立即加載類B颤霎,而是在需要使用類B的時候才會進行加載。
這樣就避免了類A和類B之間的循環(huán)依賴問題

1.4 循環(huán)依賴示例說明

我們假設現在有這樣的場景AService依賴BService涂滴,BService依賴AService

  1. AService首先實例化友酱,實例化通過ObjectFactory半成品暴露在三級緩存中
  2. 填充屬性BService,發(fā)現BService還未進行過加載柔纵,就會先去加載BService
  3. 在加載BService的過程中缔杉,實例化,也通過ObjectFactory半成品暴露在三級緩存
  4. 填充屬性AService搁料,(從三級緩存通過對象??拿到A或详,發(fā)現A雖然不太完善,但是存在郭计, 把A放??級緩存霸琴,同時刪除三級緩存中的A ,此時昭伸,B已經實例化并且初始化完成梧乘,把B放入?級緩存)這時候能夠從三級緩存中拿到半成品的ObjectFactory
    image.png

拿到ObjectFactory對象后,調用ObjectFactory.getObject()方法最終會調用getEarlyBeanReference()方法庐杨,getEarlyBeanReference這個方法主要邏輯大概描述下如果beanAOP切面代理則返回的是beanProxy對象选调,如果未被代理則返回的是原bean實例

  1. 接著A繼續(xù)屬性賦值,順利從?級緩存拿到實例化且初始化完成的B對象灵份,A對象創(chuàng)建也完成仁堪,刪除?級緩存中的A,同時把A放??級緩存
  2. 最后填渠,?級緩存中保存著實例化弦聂、初始化都完成的A、B對象

注意: B注入的半成品A對象只是一個引用氛什,所以之后A初始化完成后横浑,B這個注入的A就隨之變成了完整的A

1.5 是否可以移除二級緩存

我們發(fā)現這個二級緩存好像顯得有點多余,好像可以去掉屉更,只需要一級和三級緩存也可以做到解決循環(huán)依賴的問題

只要兩個緩存確實可以做到解決循環(huán)依賴的問題徙融,但是有一個前提這個bean沒被AOP進行切面代理,如果這個beanAOP進行了切面代理瑰谜,那么只使用兩個緩存是無法解決問題欺冀,下面來看一下beanAOP進行了切面代理的場景

image.png

我們發(fā)現AServicetestAopProxyAOP代理了树绩,看看傳入的匿名內部類的getEarlyBeanReference返回的是什么對象。

image.png

發(fā)現singletonFactory.getObject()返回的是一個AService的代理對象隐轩,還是被CGLIB代理的饺饭。再看一張再執(zhí)行一遍singletonFactory.getObject()返回的是否是同一個AService的代理對象

image.png

我們會發(fā)現再執(zhí)行一遍singleFactory.getObject()方法又是一個新的代理對象,這就會有問題了职车,因為AService是單例的瘫俊,每次執(zhí)行singleFactory.getObject()方法又會產生新的代理對象。

假設這里只有一級和三級緩存的話悴灵,每次從三級緩存中拿到singleFactory對象扛芽,執(zhí)行getObject()方法又會產生新的代理對象,這是不行的积瞒,因為AService是單例的川尖,所有這里我們要借助二級緩存來解決這個問題,將執(zhí)行了singleFactory.getObject()產生的對象放到二級緩存中去茫孔,后面去二級緩存中拿叮喳,沒必要再執(zhí)行一遍singletonFactory.getObject()方法再產生一個新的代理對象,保證始終只有一個代理對象缰贝。還有一個注意的點

image.png

既然singleFactory.getObject()返回的是代理對象馍悟,那么注入的也應該是代理對象,我們可以看到注入的確實是經過CGLIB代理的AService對象剩晴。所以如果沒有AOP的話確實可以兩級緩存就可以解決循環(huán)依賴的問題赋朦,如果加上AOP,兩級緩存是無法解決的李破,不可能每次執(zhí)行singleFactory.getObject()方法都給我產生一個新的代理對象宠哄,所以還要借助另外一個緩存來保存產生的代理對象

?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市嗤攻,隨后出現的幾起案子毛嫉,更是在濱河造成了極大的恐慌,老刑警劉巖妇菱,帶你破解...
    沈念sama閱讀 221,635評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件承粤,死亡現場離奇詭異,居然都是意外死亡闯团,警方通過查閱死者的電腦和手機辛臊,發(fā)現死者居然都...
    沈念sama閱讀 94,543評論 3 399
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來房交,“玉大人彻舰,你說我怎么就攤上這事。” “怎么了刃唤?”我有些...
    開封第一講書人閱讀 168,083評論 0 360
  • 文/不壞的土叔 我叫張陵隔心,是天一觀的道長。 經常有香客問我尚胞,道長硬霍,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,640評論 1 296
  • 正文 為了忘掉前任笼裳,我火速辦了婚禮唯卖,結果婚禮上,老公的妹妹穿的比我還像新娘躬柬。我一直安慰自己拜轨,他們只是感情好,可當我...
    茶點故事閱讀 68,640評論 6 397
  • 文/花漫 我一把揭開白布楔脯。 她就那樣靜靜地躺著,像睡著了一般胯甩。 火紅的嫁衣襯著肌膚如雪昧廷。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,262評論 1 308
  • 那天偎箫,我揣著相機與錄音木柬,去河邊找鬼。 笑死淹办,一個胖子當著我的面吹牛眉枕,可吹牛的內容都是我干的。 我是一名探鬼主播怜森,決...
    沈念sama閱讀 40,833評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼速挑,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了副硅?” 一聲冷哼從身側響起姥宝,我...
    開封第一講書人閱讀 39,736評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎恐疲,沒想到半個月后腊满,有當地人在樹林里發(fā)現了一具尸體,經...
    沈念sama閱讀 46,280評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡培己,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 38,369評論 3 340
  • 正文 我和宋清朗相戀三年碳蛋,在試婚紗的時候發(fā)現自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片省咨。...
    茶點故事閱讀 40,503評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡肃弟,死狀恐怖,靈堂內的尸體忽然破棺而出零蓉,到底是詐尸還是另有隱情愕乎,我是刑警寧澤阵苇,帶...
    沈念sama閱讀 36,185評論 5 350
  • 正文 年R本政府宣布,位于F島的核電站感论,受9級特大地震影響绅项,放射性物質發(fā)生泄漏。R本人自食惡果不足惜比肄,卻給世界環(huán)境...
    茶點故事閱讀 41,870評論 3 333
  • 文/蒙蒙 一快耿、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧芳绩,春花似錦掀亥、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,340評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至嘹害,卻和暖如春撮竿,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背笔呀。 一陣腳步聲響...
    開封第一講書人閱讀 33,460評論 1 272
  • 我被黑心中介騙來泰國打工幢踏, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人许师。 一個月前我還...
    沈念sama閱讀 48,909評論 3 376
  • 正文 我出身青樓房蝉,卻偏偏與公主長得像,于是被迫代替她去往敵國和親微渠。 傳聞我的和親對象是個殘疾皇子搭幻,可洞房花燭夜當晚...
    茶點故事閱讀 45,512評論 2 359

推薦閱讀更多精彩內容