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