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 是如何獲取配合的:
- 獲取單例
Bean
的時候會通過BeanName
先去singletonObjects
(一級緩存) 查找完整的Bean
跃闹,如果找到則直接返回,否則進行步驟 2俺孙。 - 看對應的
Bean
是否在創(chuàng)建中,如果不在直接返回找不到掷贾,如果是睛榄,則會去earlySingletonObjects
(二級緩存)查找 Bean,如果找到則返回想帅,否則進行步驟 3 - 去
singletonFactories
(三級緩存)通過BeanName
查找到對應的工廠场靴,如果存著工廠則通過工廠創(chuàng)建Bean
,放置到二級緩存earlySingletonObjects
中,并把三級緩存中給移除掉旨剥。 -
如果三個緩存都沒找到咧欣,則返回 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);
}
}
CDemo2
在CDemo1
之前被初始化。
注意
:
要有注入關系昵时,如: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
的加載順序不受@Order
或Ordered接口
的影響,@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
-
AService
首先實例化友酱,實例化通過ObjectFactory
半成品暴露在三級緩存中 - 填充屬性
BService
,發(fā)現BService
還未進行過加載柔纵,就會先去加載BService
- 在加載
BService
的過程中缔杉,實例化,也通過ObjectFactory
半成品暴露在三級緩存 - 填充屬性
AService
搁料,(從三級緩存通過對象??拿到A或详,發(fā)現A雖然不太完善,但是存在郭计,把A放??級緩存霸琴,同時刪除三級緩存中的A
,此時昭伸,B已經實例化并且初始化完成梧乘,把B放入?級緩存)這時候能夠從三級緩存中拿到半成品的ObjectFactory
image.png
拿到ObjectFactory
對象后,調用ObjectFactory.getObject()
方法最終會調用getEarlyBeanReference()
方法庐杨,getEarlyBeanReference
這個方法主要邏輯大概描述下如果bean
被AOP
切面代理則返回的是beanProxy
對象选调,如果未被代理則返回的是原bean實例
- 接著A繼續(xù)屬性賦值,順利從?級緩存拿到實例化且初始化完成的B對象灵份,A對象創(chuàng)建也完成仁堪,刪除?級緩存中的A,同時把A放??級緩存
- 最后填渠,?級緩存中保存著實例化弦聂、初始化都完成的A、B對象
注意
: B注入的半成品A對象只是一個引用氛什,所以之后A初始化完成后横浑,B這個注入的A就隨之變成了完整的A
1.5 是否可以移除二級緩存
我們發(fā)現這個二級緩存好像顯得有點多余,好像可以去掉屉更,只需要一級和三級緩存也可以做到解決循環(huán)依賴的問題
只要兩個緩存確實可以做到解決循環(huán)依賴的問題徙融,但是有一個前提這個bean
沒被AOP
進行切面代理,如果這個bean
被AOP
進行了切面代理瑰谜,那么只使用兩個緩存是無法解決問題欺冀,下面來看一下bean
被AOP
進行了切面代理的場景
我們發(fā)現AService
的testAopProxy
被AOP
代理了树绩,看看傳入的匿名內部類的getEarlyBeanReference
返回的是什么對象。
發(fā)現singletonFactory.getObject()
返回的是一個AService
的代理對象隐轩,還是被CGLIB
代理的饺饭。再看一張再執(zhí)行一遍singletonFactory.getObject()
返回的是否是同一個AService
的代理對象
我們會發(fā)現再執(zhí)行一遍singleFactory.getObject()
方法又是一個新的代理對象,這就會有問題了职车,因為AService
是單例的瘫俊,每次執(zhí)行singleFactory.getObject()
方法又會產生新的代理對象。
假設這里只有一級和三級緩存的話悴灵,每次從三級緩存中拿到singleFactory
對象扛芽,執(zhí)行getObject()
方法又會產生新的代理對象,這是不行的积瞒,因為AService
是單例的川尖,所有這里我們要借助二級緩存來解決這個問題,將執(zhí)行了singleFactory.getObject()
產生的對象放到二級緩存中去茫孔,后面去二級緩存中拿叮喳,沒必要再執(zhí)行一遍singletonFactory.getObject()
方法再產生一個新的代理對象,保證始終只有一個代理對象缰贝。還有一個注意的點
既然singleFactory.getObject()
返回的是代理對象馍悟,那么注入的也應該是代理對象,我們可以看到注入的確實是經過CGLIB
代理的AService
對象剩晴。所以如果沒有AOP
的話確實可以兩級緩存就可以解決循環(huán)依賴的問題赋朦,如果加上AOP
,兩級緩存是無法解決的李破,不可能每次執(zhí)行singleFactory.getObject()
方法都給我產生一個新的代理對象宠哄,所以還要借助另外一個緩存來保存產生的代理對象