Spring是如何解決循環依賴的
前言
在面試的時候這兩年有一個非常高頻的關於spring的問題,那就是spring是如何解決循環依賴的。這個問題聽著就是輕描淡寫的一句話,其實考察的內容還是非常多的,主要還是考察的應聘者有沒有研究過spring的源碼。但是說實話,spring的源碼其實非常複雜的,研究起來並不是個簡單的事情,所以我們此篇文章只是為了解釋清楚Spring是如何解決循環依賴的這個問題。
什麼樣的依賴算是循環依賴?
用過Spring框架的人都對依賴注入這個詞不陌生,一個Java類A中存在一個屬性是類B的一個對象,那麼我們就說類A的對象依賴類B,而在Spring中是依靠的IOC來實現的對象注入,也就是說創建對象的過程是IOC容器來實現的,並不需要自己在使用的時候通過new關鍵字來創建對象。
那麼當類A中依賴類B的對象,而類B中又依賴類C的對象,最後類C中又依賴類A的對象的時候,這種情況最終的依賴關係會形成一個環,這就是循環依賴。
循環依賴的類型
根據注入的時機可以分為兩種:
- 構造器循環依賴
依賴的對象是通過構造方法傳入的,在實例化bean的時候發生。 - 賦值屬性循環依賴
依賴的對象是通過setter方法傳入的,對象已經實例化,在屬性賦值和依賴注入的時候發生。
構造器循環依賴,本質上是無解的,實例化A的時候調用A的構造器,發現依賴了B,又去實例化B,然後調用B的構造器,發現又依賴的C,然後調用C的構造器去實例化,結果發起C的構造器里依賴了A,這就是個死循環無解。所以Spring也是不支援構造器循環依賴的,當發現存在構造器循環依賴時,會直接拋出BeanCurrentlyInCreationException
異常。
賦值屬性循環依賴,Spring只支援bean在單例模式下的循環依賴,其他模式下的循環依賴Spring也是會拋出BeanCurrentlyInCreationException
異常的。Spring通過對還在創建過程中的單例bean,進行快取並提前暴露該單例,使得其他實例可以提前引用到該單例bean。
Spring為什麼只支援單例模式下的bean的賦值情況下的循環依賴
在prototype的模式下的bean,使用了一個ThreadLocal變數prototypesCurrentlyInCreation
來記錄當前執行緒正在創建中的bean,這個變數在AbtractBeanFactory
類里。在創建前用beanName記錄bean,在創建完成後刪除bean。在prototypesCurrentlyInCreation
里採用了一個Set對象來存儲正在創建中的bean。我們都知道Set是不允許存在重複對象的,這樣就能保證同一個bean在一個執行緒中只能有一個正在創建。
下面是prototypesCurrentlyInCreation
變數在刪除bean時的操作,在AbtractBeanFactory
的beforePrototypeCreation
操作里。
protected void afterPrototypeCreation(String beanName) {
Object curVal = this.prototypesCurrentlyInCreation.get();
if (curVal instanceof String) {
this.prototypesCurrentlyInCreation.remove();
}
else if (curVal instanceof Set) {
Set<String> beanNameSet = (Set<String>) curVal;
beanNameSet.remove(beanName);
if (beanNameSet.isEmpty()) {
this.prototypesCurrentlyInCreation.remove();
}
}
}
從上面的程式碼中看出,當變數為一個的時候採用了一個String對象來存儲,節省了一些記憶體空間。
在AbstractBeanFactory
類的doGetBean
方法里先判斷是否為單例對象,不是單例對象,則直接判斷當前執行緒是否已經存在了正在創建的bean。存在的話直接拋出異常。
這個isPrototypeCurrentlyInCreation()
方法的實現程式碼如下:
protected boolean isPrototypeCurrentlyInCreation(String beanName) {
Object curVal = this.prototypesCurrentlyInCreation.get();
return curVal != null && (curVal.equals(beanName) || curVal instanceof Set && ((Set)curVal).contains(beanName));
}
因為有了這個機制,spring在原型模式下是解決不了bean的循環依賴的,當發現有循環依賴的時候會直接拋出BeanCurrentlyInCreationException
異常的。
那麼為什麼spring在單例模式下的構造賦值也不支援循環依賴呢?
其實原理和原型模式下的情況類似,在單例模式下,bean也會用一個Set集合來保存正在創建中的bean,在創建前保存,創建完成後刪除。
這個對象在DefaultSingletonBeanRegistry
類下變數名為:singletonsCurrentlyInCreation
public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements SingletonBeanRegistry {
private final Set<String> singletonsCurrentlyInCreation = Collections.newSetFromMap(new ConcurrentHashMap(16));
}
判定程式碼在DefaultSingletonBeanRegistry
類的beforeSingletonCreation
方法下。
protected void beforeSingletonCreation(String beanName) {
if (!this.inCreationCheckExclusions.contains(beanName) && !this.singletonsCurrentlyInCreation.add(beanName)) {
throw new BeanCurrentlyInCreationException(beanName);
}
}
在上面這個方法中,判定singletonsCurrentlyInCreation
是否能成功的保存一個單例bean。如果不能成功保存,那麼就會直接拋出BeanCurrentlyInCreationException
異常。
單例模式下的Setter賦值循環依賴
終於到了我們的重點,Spring是如何解決單例模式下的Setter賦值的循環依賴了。
其實主要的就是靠提前暴露創建中的單例實例。
那麼具體是一個怎樣的過程呢?
例如:上面那個圖的例子,A依賴B,B依賴C,C又依賴B。
過程如下:
創建A,調用構造方法,完成構造,進行屬性賦值注入,發現依賴B,去實例化B
。創建B,調用構造方法,完成構造,進行屬性賦值注入,發現依賴C,去實例化C
。- 創建C,調用構造方法,完成構造,進行屬性賦值注入,發現依賴A。
這個時候就是解決循環依賴的關鍵了,因為A已經通過構造方法已經構造完成了,也就是說已經將Bean的在堆中分配好了記憶體,這樣即使A再填充屬性值也不會更改記憶體地址了,所以此時可以提前拿出來A的引用,來完成C的實例化。
這樣上面創建C過程就會變成了: 創建C,調用構造方法,完成構造,進行屬性賦值注入,發現依賴A,A已經構造完成,直接引用,完成C的實例化
。C完成實例化後,注入B,B也完成了實例化,然後B注入A,A也完成了實例化
。
為了能獲取到創建中單例bean,spring提供了三級快取來將正在創建中的bean提前暴露。
在類DefaultSingletonBeanRegistry
下,即下圖紅框中的三個Map對象。
這三個快取Map的作用如下:- 一級快取,
singletonObjects
單例快取,存儲已經實例化的單例bean。 - 二級快取,
earlySingletonObjects
提前暴露的單例快取,這裡存儲的bean是剛剛構造完成,但還會通過屬性注入bean。 - 三級快取,
singletonFactories
生產單例的工廠快取,存儲工廠。
首先在創建bean的時候會先創建一個和bean同名的單例工廠,並將bean先放入到單例工廠中。程式碼在AbstractAutowireCapableBeanFactory
類的doCreateBean
方法中。
protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, Object[] args) throws BeanCreationException {
......
this.addSingletonFactory(beanName, new ObjectFactory<Object>() {
public Object getObject() throws BeansException {
return AbstractAutowireCapableBeanFactory.this.getEarlyBeanReference(beanName, mbd, bean);
}
});
.....
}
而上面的程式碼中的addSingletonFactory
方法的程式碼如下:
protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
Assert.notNull(singletonFactory, "Singleton factory must not be null");
Map var3 = this.singletonObjects;
synchronized(this.singletonObjects) {
if (!this.singletonObjects.containsKey(beanName)) {
this.singletonFactories.put(beanName, singletonFactory);
this.earlySingletonObjects.remove(beanName);
this.registeredSingletons.add(beanName);
}
}
}
addSingletonFactory
方法的作用通過程式碼就可以看到是將存在了正在創建中的bean的單例工廠,放在三級快取里,這樣保證了在循環依賴查找的時候是可以找到bean的引用的。
具體讀取快取獲取bean的過程在類DefaultSingletonBeanRegistry
的getSingleton
方法里。
如下源碼:
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null && this.isSingletonCurrentlyInCreation(beanName)) {
Map var4 = this.singletonObjects;
synchronized(this.singletonObjects) {
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null && allowEarlyReference) {
ObjectFactory<?> singletonFactory = (ObjectFactory)this.singletonFactories.get(beanName);
if (singletonFactory != null) {
singletonObject = singletonFactory.getObject();
this.earlySingletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);
}
}
}
}
return singletonObject != NULL_OBJECT ? singletonObject : null;
}
通過上面的源碼我們可以看到,在獲取單例Bean的時候,會先從一級快取singletonObjects
里獲取,如果沒有獲取到(說明不存在或沒有實例化完成),會去第二級快取earlySingletonObjects
中去找,如果還是沒有找到的話,就會三級快取中獲取單例工廠singletonFactory
,通過從singletonFactory
中獲取正在創建中的引用,將singletonFactory
存儲在earlySingletonObjects
二級快取中,這樣就將創建中的單例引用從三級快取中升級到了二級快取中,二級快取earlySingletonObjects
,是會提前暴露已完成構造,還可以執行屬性注入的單例bean的。
這個時候如何還有其他的bean也是需要屬性注入,那麼就可以直接從earlySingletonObjects
中獲取了。
上面的例子中的過程中的A,在注入C的時候,其實並沒有真正的初始化完成,等到順利的注入了B才算是真正的初始化完成。
整個過程如下圖: