Spring循環依賴知多少?(不同的深度分析)

前言

   結合Spring Bean加載流程,本文對Spring單例構造器循環依賴及Field循環依賴進行分析。對於構造器循環依賴,目前Spring是沒法解決的;Field循環依賴,Spring經過提早暴露實例化Bean及緩存不一樣階段的bean(三級緩存)進行依賴排除。網上也有很多一些關於這方面的文章,但做者想從緩存生命週期及多例Bean循環依賴這方面另闢蹊徑,深刻理解下Spring Ioc的精髓。這是第二篇博文,但願能養成梳理筆記的好習慣。web

什麼是循環依賴?

   循環依賴,簡單地說,就是循環引用,兩個或者多個 bean 相互之間的持有對方,造成一個閉環。如,A 依賴 B,B 又依賴 A,它們之間造成了循環依賴,又或者是 A 依賴 B,B 依賴 C,C 又依賴 A。能夠用一張簡圖描述這種依賴關係。spring

怎麼解決循環依賴?

   Spring循環依賴的理論依據實際上是Java基於引用傳遞,當咱們獲取到對象的引用時,對象的field或者或屬性是能夠延後設置的。接下來,將經過構造器循環依賴及Field循環依賴進行闡述。設計模式

Spring Bean加載流程

   在分析循環依賴以前咱們先回顧下Spring Bean加載的流程。緩存

1)項目啓動時建立ServletContext實例,將context-param中鍵值對值存入ServletContext中;bash

2)當建立Context LoaderListener時,因爲監聽器實現了ServletContextListener接口,而ServletContextListener提供了監聽web容器啓動時,初始化ServletContext後的事件監聽及銷燬ServletContext前的事件監聽;所以,contextLoaderListener默認實現contextInitialized和contextDestroyed這兩個方法;容器的初始化就是從contextInitialized開始的;app

3)首先先建立WebApplicationContext的實例,若是配置了contextClass屬性值,則表明配置了相應的WebApplicationContext容器實現類,若是沒有配置,默認建立的實例對象是XmlWebApplicationContext;ide

4)經過contextConfigLocation獲取容器加載的配置文件,循環遍歷configLocation,調用AbstractBeanDefinitionReader的loadDefinitionBeans方法進行解析並註冊,解析的過程主要有如下幾個步驟:函數

  • 將xml轉換爲Document對象,最終調用DefaultBeanDefinitionDocumentReader中的parseBeanDefinitions方法;post

  • 解析Document中的Node節點,若是是默認的bean標籤直接註冊(調用的是org.springframework.beans.factory.support.BeanDefinitionReaderUtils#registerBeanDefinition方法,若是是自定義的命名空間標籤,得到命名空間後,拿到對應的NamespaceHandler(從spring中的jar包中的meta-inf/spring.handlers屬性文件中獲取),調用其parse方法進行解析;ui

  • 調用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, mbd, bean);
            }
        });
    }
    
    // Initialize the bean instance.
    Object exposedObject = bean;
    try {
        // 屬性注入,這裏可能發生循環依賴
        populateBean(beanName, mbd, 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, mbd, 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完成初始化後,二級緩存將會清除;
相關文章
相關標籤/搜索