Spring迴圈依賴知多少?(不一樣的深度分析)
前言
結合Spring Bean載入流程,本文對Spring單例構造器迴圈依賴及Field迴圈依賴進行分析。對於構造器迴圈依賴,目前Spring是無法解決的;Field迴圈依賴,Spring通過提前暴露例項化Bean及快取不同階段的bean(三級快取)進行依賴排除。網上也有不少一些關於這方面的文章,但作者想從快取生命週期及多例Bean迴圈依賴這方面另闢蹊徑,深入理解下Spring Ioc的精髓。這是第二篇博文,希望能養成梳理筆記的好習慣。
什麼是迴圈依賴?
迴圈依賴,簡單地說,就是迴圈引用,兩個或者多個 bean 相互之間的持有對方,形成一個閉環。如,A 依賴 B,B 又依賴 A,它們之間形成了迴圈依賴,又或者是 A 依賴 B,B 依賴 C,C 又依賴 A。可以用一張簡圖描述這種依賴關係。
怎麼解決迴圈依賴?
Spring迴圈依賴的理論依據其實是Java基於引用傳遞,當我們獲取到物件的引用時,物件的field或者或屬性是可以延後設定的。接下來,將通過構造器迴圈依賴及Field迴圈依賴進行闡述。
Spring Bean載入流程
在分析迴圈依賴之前我們先回顧下Spring Bean載入的流程。
1)專案啟動時建立ServletContext例項,將context-param中鍵值對值存入ServletContext中;
2)當建立Context LoaderListener時,由於監聽器實現了ServletContextListener介面,而ServletContextListener提供了監聽web容器啟動時,初始化ServletContext後的事件監聽及銷燬ServletContext前的事件監聽;因此,contextLoaderListener預設實現contextInitialized和contextDestroyed這兩個方法;容器的初始化就是從contextInitialized開始的;
3)首先先建立WebApplicationContext的例項,如果配置了contextClass屬性值,則代表配置了相應的WebApplicationContext容器實現類,如果沒有配置,預設建立的例項物件是XmlWebApplicationContext;
4)通過contextConfigLocation獲取容器載入的配置檔案,迴圈遍歷configLocation,呼叫AbstractBeanDefinitionReader的loadDefinitionBeans方法進行解析並註冊,解析的過程主要有以下幾個步驟:
-
將xml轉換為Document物件,最終呼叫DefaultBeanDefinitionDocumentReader中的parseBeanDefinitions方法;
-
解析Document中的Node節點,如果是預設的bean標籤直接註冊(呼叫的是org.springframework.beans.factory.support.BeanDefinitionReaderUtils#registerBeanDefinition方法,如果是自定義的名稱空間標籤,獲得名稱空間後,拿到對應的NamespaceHandler(從spring中的jar包中的meta-inf/spring.handlers屬性檔案中獲取),呼叫其parse方法進行解析;
-
呼叫NamespaceHandler的init方法註冊每個標籤對應的解析器;
-
根據標籤名稱獲得對應的解析器,解析具體的標籤; 解析註冊這些步驟最終將解析所得的BeanDefinition放入一個map中,這時並沒有進行注入。
5)例項化 入口是AbstractApplicationContext#finishBeanFactoryInitialization方法,以getBean方法為入口,先從快取中獲取,如果拿不到時,通過工廠方法或構造器例項化一個Bean,對於構造器我們可以指定構造引數。
6)依賴注入(populateBean) 裝配bean依賴,專案中大都使用@Autowired註解,org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor#postProcessPropertyValues,這個過程可能進行遞迴進行依賴注入;最終通過反射將欄位設定到bean中。
7)初始化:通過後置處理器完成對bean的一些設定,如判斷否實現intializingBean,如果實現呼叫afterPropertiesSet方法,建立代理物件等;
8)最終將根上下文設定到servletContext屬性中;
Spring bean的載入最主要的過程集中在5,6,7這三個步驟中,對應著createBean、populateBean及intializeBean這三個方法上,迴圈依賴產生在createBean和populateBean這兩個方法中。
構造器迴圈依賴
對於構造器迴圈依賴,其依賴產生在例項化Bean上,也就是在createBean這個方法。對於這種迴圈依賴Spring是沒有辦法解決的。
<bean id = "aService" class="com.yfty.eagle.service.AService">
<constructor-arg index="0" ref="bService"/>
</bean>
<bean id = "bService" class="com.yfty.eagle.service.BService">
<constructor-arg index="0" ref="cService"/>
</bean>
<bean id = "cService" class="com.yfty.eagle.service.CService">
<constructor-arg index="0" ref="aService"/>
</bean>
複製程式碼
執行流程
分析: 在解析xml中的bean元素生成BeanDefination物件時,constructor-arg節點,最終會被賦值到constructorArgumentValues這個Map中,作為建構函式入參。在建立AService時解析建構函式物件時,發現有BService的引用,此時建立BService,發現又有CService的引用,而CService又引用了AService。在例項化Bean時,會將beanName存入singletonsCurrentlyInCreation集合中,當發現重複時,即說明有迴圈依賴,丟擲異常。綜上,對於構造器迴圈依賴,Spring也無法解決。/**
* Callback before singleton creation.
* <p>The default implementation register the singleton as currently in creation.
* @param beanName the name of the singleton about to be created
* @see #isSingletonCurrentlyInCreation
*/
protected void beforeSingletonCreation(String beanName) {
if (!this.inCreationCheckExclusions.contains(beanName) && !this.singletonsCurrentlyInCreation.add(beanName)) {
throw new BeanCurrentlyInCreationException(beanName);
}
}
複製程式碼
Field屬性或Property迴圈依賴
在xml檔案配置property屬性或者使用@Autowired註解,其實這兩種同屬於一類。下面分析Spring是如何通過提前曝光機制+三級快取來排除bean之間依賴的。
三級快取
/** Cache of singleton objects: bean name --> bean instance */
一級快取:維護著所有建立完成的Bean
private final Map<String,Object> singletonObjects = new ConcurrentHashMap<String,Object>(256);
/** Cache of early singleton objects: bean name --> bean instance */
二級快取:維護早期暴露的Bean(只進行了例項化,並未進行屬性注入)
private final Map<String,Object> earlySingletonObjects = new HashMap<String,Object>(16);
/** Cache of singleton factories: bean name --> ObjectFactory */
三級快取:維護建立中Bean的ObjectFactory(解決迴圈依賴的關鍵)
private final Map<String,ObjectFactory<?>> singletonFactories = new HashMap<String,ObjectFactory<?>>(16);
複製程式碼
依賴排除
由Spring Bean建立的過程,首先Spring會嘗試從快取中獲取,這個快取就是指singletonObjects,主要呼叫的方法是getSingleton;如果快取中沒有,則調下Spring bean建立過程中,最重要的一個方法doCreateBean。
protected Object getSingleton(String beanName,boolean allowEarlyReference) {
// 從一級快取中獲取
Object singletonObject = this.singletonObjects.get(beanName);
// 如果一級快取沒有,並且bean在建立中,會從二級快取中獲取
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
synchronized (this.singletonObjects) {
singletonObject = this.earlySingletonObjects.get(beanName);
// 二級快取不存在,並且允許從singletonFactories中通過getObject拿到物件
if (singletonObject == null && allowEarlyReference) {
ObjectFactory<?> singletonFactory = 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);
}
複製程式碼
分析: Spring首先從singletonObjects(一級快取)中嘗試獲取,如果獲取不到並且物件在建立中,則嘗試從earlySingletonObjects(二級快取)中獲取,如果還是獲取不到並且允許從singletonFactories通過getObject獲取,則通過三級快取獲取,即通過singletonFactory.getObject()。如果獲取到了,將其存入二級快取,並清除三級快取。
如果快取中沒有bean物件,那麼Spring會建立Bean物件,將例項化的bean提前曝光,並且加入快取中。
protected Object doCreateBean(final String beanName,final RootBeanDefinition mbd,final Object[] args)
throws BeanCreationException {
// Instantiate the bean.
BeanWrapper instanceWrapper = null;
......
if (instanceWrapper == null) {
//這個是例項化Bean的方法,會呼叫構造方法,生成一個原始型別的Bean
instanceWrapper = createBeanInstance(beanName,mbd,args);
}
// 提前曝光這個例項化的Bean,方便其他Bean使用
boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
isSingletonCurrentlyInCreation(beanName));
// 滿足單例 + allowCircularReferences預設為true + bean在singletonsCurrentlyInCreation集合中時,earlySingletonExposure為true
if (earlySingletonExposure) {
if (logger.isDebugEnabled()) {
logger.debug("Eagerly caching bean '" + beanName +
"' to allow for resolving potential circular references");
}
// 將bean加入三級快取中
addSingletonFactory(beanName,new ObjectFactory<Object>() {
@Override
public Object getObject() throws BeansException {
return getEarlyBeanReference(beanName,bean);
}
});
}
// Initialize the bean instance.
Object exposedObject = bean;
try {
// 屬性注入,這裡可能發生迴圈依賴
populateBean(beanName,instanceWrapper);
if (exposedObject != null) {
// 初始化bean
exposedObject = initializeBean(beanName,exposedObject,mbd);
}
}
// 由於AService提前暴露,會走這段程式碼
if (earlySingletonExposure) {
// 從二級快取中拿出AService(這個物件其實ObjectFactory.getObject()得來的,可能是個包裝類,
而exposedObject可能依然是例項化的那個bean,這時為保證最終BService中的AService屬性與AService本身
持有的引用一直,故再次進行exposedObject的賦值操作,保證beanName對應例項唯一性。)
Object earlySingletonReference = getSingleton(beanName,false);
if (earlySingletonReference != null) {
if (exposedObject == bean) {
exposedObject = earlySingletonReference;
}
}
// ...........
return exposedObject;
}
複製程式碼
分析: 當通過無參構造,獲得一個例項化bean時,Spring會將其提前曝光,即在例項化後注入屬性前將其加入三級快取中。下面以AService和BService相互依賴為例,說明依賴排除過程。
- AService例項化後,在注入屬性前提前曝光,將其加入三級快取singletonFactories中,供其他bean使用;
- AService通過populateBean注入BService,從快取中獲取BService,發現快取中沒有,開始建立BService例項;
- BService例項也會在屬性注入前提前曝光,加入三級快取中,此時三級快取中有AService和BService;
- BService在進行屬性注入時,發現有AService引用,此時,建立AService時,會先從快取中獲取AService(先從一級快取中取,沒有取到後,從二級快取中取,也沒有取到,這時,從三級快取中取出),這時會清除三級快取中的AService,將其將其加入二級快取earlySingletonObjects中,並返回給BService供其使用;
- BService在完成屬性注入,進行初始化,這時會加入一級快取,這時會清除三級快取中的BService,此時,三級快取為空,二級快取中有AService,一級快取中有BService;
- BService初始化後注入AService中,AService進行初始化,然後通過getSingleton方法獲取二級快取,賦值給exposedObject,最後將其加入一級快取,清除二級快取AService;
從上述分析可知,singletonFactories即三級快取才是解決迴圈依賴的關鍵,它是一個橋樑。當AService初始化後,會從二級快取中獲取提前暴露的物件,並且賦值給exposedObject。這主要是二級快取的物件earlySingletonReference可能是包裝類,BService持有的引用就是這個earlySingletonReference,賦值後保證beanName對應例項唯一性,這點回味無窮。
第三級快取ObjectFactory的作用
我們已經知道了Spring如何解決迴圈依賴了,但是對於Spring為什麼這麼設計,總感覺雲裡霧裡,網上的博文大都沒有講這點。下面作者談下自己的觀點。
三級快取採用工廠設計模式,通過getObject方法獲取bean,就迴圈依賴而言,當BService通過populateBean注入AService時,要保證BService中的AService記憶體地址a1和AService最終初始化後的地址a2一致,而此時AService才剛剛例項化,a1與a2不一定相等,通過三級快取可以獲取到最終引用地址,這保證了在迴圈依賴能獲取到真正的依賴。
@Override
public Object getObject() throws BeansException {
return getEarlyBeanReference(beanName,bean);
}
複製程式碼
protected Object getEarlyBeanReference(String beanName,RootBeanDefinition mbd,Object bean) {
Object exposedObject = bean;
if (bean != null && !mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
for (BeanPostProcessor bp : getBeanPostProcessors()) {
if (bp instanceof SmartInstantiationAwareBeanPostProcessor) {
SmartInstantiationAwareBeanPostProcessor ibp = (SmartInstantiationAwareBeanPostProcessor) bp;
exposedObject = ibp.getEarlyBeanReference(exposedObject,beanName);
if (exposedObject == null) {
return null;
}
}
}
}
return exposedObject;
}
複製程式碼
分析: 通過BeanFactory的getObject()方法,呼叫getEarlyBeanReference方法,對其包裝,最終生成代理物件。而AService例項化後,最終,通過從二級快取中獲取到的物件其實是就是BeanFactory對應的引用。為什麼設計二級快取,個人覺得其實主要是避免再次呼叫呼叫getEarlyBeanReference方法。只能說,Spring是個海洋。
快取生命週期
例項化的bean是何時加入快取中,又是何時將其刪除的,它們之間有什麼區別呢?接下來,本文會一一作答。
- 三級快取
當earlySingletonExposure屬性為true時,將beanFactory加入快取;當通過getSingleton從三級快取中取出例項化的原始bean時或者完成初始化後,並清除singletonFactories中bean的快取。
- 二級快取
當earlySingletonExposure屬性為true時,將beanFactory加入快取,當通過getSingleton從三級快取中取出例項化的原始bean時,此時,將獲取的bean加入二級快取。當完成bean初始化,將bean加入一級快取後,清除二級快取;
- 一級快取
當完成bean初始化,通過addSingleton將bean加入一級快取singletonObjects中,並且這個快取是常駐記憶體中的。
從上述分析可知,三級快取和二級快取是不共存的,且其在Spring完成初始化,都會被清除掉。
protected void addSingleton(String beanName,Object singletonObject) {
synchronized (this.singletonObjects) {
this.singletonObjects.put(beanName,(singletonObject != null ? singletonObject : NULL_OBJECT));
this.singletonFactories.remove(beanName);
this.earlySingletonObjects.remove(beanName);
this.registeredSingletons.add(beanName);
}
}
複製程式碼
總結
- Spring不能解決構造器迴圈依賴,主要原因迴圈獲取獲取構造引數時,將bean存入singletonsCurrentlyInCreation中,在建立bean的前置校驗中,發現有已經存在的且相互依賴的bean在建立中,校驗不通過,無法建立bean;
- Spring通過提前暴露機制+快取解決property或field迴圈依賴,每次獲取時,先從快取中取,取不到時,再進行例項化,例項化後,將其加入三級快取,供其他bean使用;
- 解決迴圈依賴中,三級快取自動升級為二級快取及bean初始化後,自動清除;在bean完成初始化後,二級快取將會清除;