1. 程式人生 > 程式設計 >Spring迴圈依賴知多少?(不一樣的深度分析)

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完成初始化後,二級快取將會清除;