Spring 循環(huán)依賴:三級緩存的獨特優(yōu)勢與二級緩存的局限
前言
在 Java
開發(fā)領(lǐng)域,Spring
框架以其強大的功能和高度的靈活性被廣泛應(yīng)用。其中,循環(huán)依賴問題是 Spring
在 Bean
管理過程中必須妥善處理的關(guān)鍵挑戰(zhàn)之一。Spring
采用了獨特的三級緩存機制來有效應(yīng)對這一難題。
什么是Spring的循環(huán)依賴
圖片
循環(huán)依賴,簡單來說,就是兩個或多個 Bean
之間相互持有對方的引用,形成一個閉環(huán)。例如,Bean A
依賴于 Bean B
,而 Bean B
又依賴于 Bean A
,這就構(gòu)成了一個典型的循環(huán)依賴場景。在 Spring
容器初始化這些 Bean
時,如果不能妥善處理循環(huán)依賴,將會導(dǎo)致程序陷入無限循環(huán),無法完成 Bean
的創(chuàng)建與初始化,進而使應(yīng)用程序無法正常啟動。
注意:
Spring
解決循環(huán)依賴是有一定限制的:首先就是要求互相依賴的
Bean
必須要是單例的Bean
。另外就是依賴注入的方式不能都是構(gòu)造函數(shù)注入的方式。
為什么只支持單例
Spring
循環(huán)依賴的解決方案主要是通過對象的提前暴露來實現(xiàn)的。當一個對象在創(chuàng)建過程中需要引用到另一個正在創(chuàng)建的對象時,Spring
會先提前暴露一個尚未完全初始化的對象實例,以解決循環(huán)依賴的問題。這個尚未完全初始化的對象實例就是半成品對象。
在 Spring
容器中,單例對象的創(chuàng)建和初始化只會發(fā)生一次,并且在容器啟動時就完成了。這意味著,在容器運行期間,單例對象的依賴關(guān)系不會發(fā)生變化。因此,可以通過提前暴露半成品對象的方式來解決循環(huán)依賴的問題。
相比之下,原型對象的創(chuàng)建和初始化可以發(fā)生多次,并且可能在容器運行期間動態(tài)地發(fā)生變化。因此,對于原型對象,提前暴露半成品對象并不能解決循環(huán)依賴的問題,因為在后續(xù)的創(chuàng)建過程中,可能會涉及到不同的原型對象實例,無法像單例對象那樣緩存并復(fù)用半成品對象。
因此,Spring
只支持通過單例對象的提前暴露來解決循環(huán)依賴問題。
為什么不支持構(gòu)造函數(shù)注入
Spring
無法解決構(gòu)造函數(shù)的循環(huán)依賴,是因為在對象實例化過程中,構(gòu)造函數(shù)是最先被調(diào)用的,而此時對象還未完成實例化,無法注入一個尚未完全創(chuàng)建的對象,因此Spring
容器無法在構(gòu)造函數(shù)注入中實現(xiàn)循環(huán)依賴的解決。
什么是Spring的三級緩存
在Spring
的BeanFactory
體系中,BeanFactory
是Spring IoC
容器的基礎(chǔ)接口,其DefaultSingletonBeanRegistry
類實現(xiàn)了BeanFactory
接口,并且維護了三級緩存:
public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements SingletonBeanRegistry {
//一級緩存,保存完成的Bean對象
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
//三級緩存,保存單例Bean的創(chuàng)建工廠
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
//二級緩存,存儲"半成品"的Bean對象
private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16);
}
singletonObjects
是一級緩存,存儲的是完整創(chuàng)建好的單例bean
對象。在創(chuàng)建一個單例bean
時,會先從singletonObjects
中嘗試獲取該bean
的實例,如果能夠獲取到,則直接返回該實例,否則繼續(xù)創(chuàng)建該bean
。earlySingletonObjects
是二級緩存,存儲的是尚未完全創(chuàng)建好的單例bean
對象。在創(chuàng)建單例bean
時,如果發(fā)現(xiàn)該bean
存在循環(huán)依賴,則會先創(chuàng)建該bean
的半成品對象,并將半成品對象存儲到earlySingletonObjects
中。當循環(huán)依賴的bean
創(chuàng)建完成后,Spring
會將完整的bean
實例對象存儲到singletonObjects
中,并將earlySingletonObjects
中存儲的代理對象替換為完整的bean
實例對象。這樣可以保證單例bean
的創(chuàng)建過程不會出現(xiàn)循環(huán)依賴問題。singletonFactories
是三級緩存,存儲的是單例bean
的創(chuàng)建工廠。當一個單例bean
被創(chuàng)建時,Spring
會先將該bean
的創(chuàng)建工廠存儲到singletonFactories
中,然后再執(zhí)行創(chuàng)建工廠的getObject()
方法,生成該bean
的實例對象。在該bean
被其他bean
引用時,Spring
會從singletonFactories
中獲取該bean
的創(chuàng)建工廠,并將這個早期引用放入二級緩存earlySingletonObjects
中,同時從三級緩存中移除BeanA
的工廠對象。
以下是DefaultSingletonBeanRegistry#getSingleton
方法,代碼中,包括一級緩存、二級緩存、三級緩存的處理邏輯,該方法是獲取bean
的單例實例對象的核心方法:
@Nullable
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
// 首先從一級緩存中獲取bean實例對象,如果已經(jīng)存在,則直接返回
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
// 如果一級緩存中不存在bean實例對象,而且當前bean正在創(chuàng)建中,則從二級緩存中獲取bean實例對象
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null && allowEarlyReference) {
// 如果二級緩存中也不存在bean實例對象,并且允許提前引用,則需要在鎖定一級緩存之前,
// 先鎖定二級緩存,然后再進行一系列處理
synchronized (this.singletonObjects) {
// 進行一系列安全檢查后,再次從一級緩存和二級緩存中獲取bean實例對象
singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null) {
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null) {
// 如果二級緩存中也不存在bean實例對象,則從三級緩存中獲取bean的ObjectFactory,并創(chuàng)建bean實例對象
ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
if (singletonFactory != null) {
singletonObject = singletonFactory.getObject();
// 將創(chuàng)建好的bean實例對象存儲到二級緩存中
this.earlySingletonObjects.put(beanName, singletonObject);
// 從三級緩存中移除bean的ObjectFactory
this.singletonFactories.remove(beanName);
}
}
}
}
}
}
return singletonObject;
}
二級緩存
圖片
其實,使用二級緩存也能解決循環(huán)依賴的問題,一級緩存用于存儲已經(jīng)完全初始化好的 Bean
實例,這些 Bean
可以直接被應(yīng)用程序使用。二級緩存則存儲那些已經(jīng)實例化,但尚未完成屬性注入和其他初始化操作的 Bean
,即 半成品Bean
。
當 Spring
容器創(chuàng)建一個 Bean
時,首先會嘗試從一級緩存中獲取該 Bean
。如果一級緩存中不存在,再去二級緩存中查找。若在二級緩存中找到,則返回這個 半成品Bean
,等待后續(xù)完成剩余的初始化步驟。
但是如果完全依靠二級緩存解決循環(huán)依賴,意味著當我們依賴了一個代理類的時候,就需要在Bean
實例化之后完成AOP
代理。而在Spring
的設(shè)計中,為了解耦Bean
的初始化和代理,是通過AnnotationAwareAspectJAutoProxyCreator
這個后置處理器來在Bean
生命周期的最后一步來完成AOP
代理的。
因為在Spring
的初始化過程中,他是不知道哪些Bean
可能有循環(huán)依賴的,那么,這時候Spring
面臨兩個選擇:
- 不管有沒有循環(huán)依賴,都提前把代理對象創(chuàng)建出來,并將代理對象緩存起來,出現(xiàn)循環(huán)依賴時,其他對象直接就可以取到代理對象并注入。
- 不提前創(chuàng)建代理對象,在出現(xiàn)循環(huán)依賴時,再生成代理對象。這樣在沒有循環(huán)依賴的情況下,
Bean
就可以按著Spring
設(shè)計原則的步驟來創(chuàng)建。
二級緩存看上去比較簡單,但是他也意味著Spring
需要在所有的bean
的創(chuàng)建過程中就要先生成代理對象再初始化,那么這就和Spring
的aop
的設(shè)計原則是相悖的。
AOP
代理問題:當Bean
涉及AOP
代理時,二級緩存的局限性就凸顯出來了。在Spring
中,AOP
通過代理機制為Bean
添加額外的功能,如事務(wù)管理、日志記錄等。在循環(huán)依賴場景下,如果使用二級緩存,可能會出現(xiàn)獲取到的Bean
不是代理對象的情況。因為二級緩存中的Bean
在被放入時,可能還未經(jīng)過AOP
代理處理。當其他Bean
依賴這個未代理的Bean
時,后續(xù)在使用該Bean
的代理功能時就會出現(xiàn)問題,導(dǎo)致AOP
功能無法正常發(fā)揮。- 破壞設(shè)計原則:
Spring
的設(shè)計理念強調(diào)將Bean
的創(chuàng)建和初始化過程進行解耦,以提高代碼的可維護性和擴展性。二級緩存機制在某些情況下可能會破壞這一設(shè)計原則。由于需要在Bean
未完全初始化時就將其放入二級緩存,可能會導(dǎo)致在后續(xù)的初始化過程中,需要對這些 半成品Bean
進行額外的特殊處理,增加了代碼的復(fù)雜性和耦合度,不符合Spring
設(shè)計的初衷。
而Spring
為了不破壞AOP
的代理設(shè)計原則,則引入第三級緩存,在三級緩存中保存對象工廠,因為通過對象工廠我們可以在想要創(chuàng)建對象的時候直接獲取對象。有了它,在后續(xù)發(fā)生循環(huán)依賴時,如果依賴的Bean
被AOP
代理,那么通過這個工廠獲取到的就是代理后的對象,如果沒有被AOP
代理,那么這個工廠獲取到的就是實例化的真實對象。
總結(jié)
Spring
選擇使用三級緩存而不是二級緩存來解決循環(huán)依賴問題,是基于對 AOP
代理支持和遵循自身設(shè)計原則的綜合考量。三級緩存機制通過巧妙地分離 Bean
的實例化、初始化以及代理對象的創(chuàng)建過程,在復(fù)雜的循環(huán)依賴場景下,能夠確保 Bean
的正確創(chuàng)建和初始化,同時保證 AOP
功能的正常運行。