Spring系列:Spring循環依賴知多少?(不一樣的深度分析)

  1. <strong>
  2. <strong>
  3. <strong>
  4. <strong>

前言

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

什麼是循環依賴?

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


Spring系列:Spring循環依賴知多少?(不一樣的深度分析)

怎麼解決循環依賴?

   Spring循環依賴的理論依據其實是Java基於引用傳遞,當我們獲取到對象的引用時,對象的field或者或屬性是可以延後設置的。接下來,將通過構造器循環依賴及Field循環依賴進行闡述。

Spring Bean加載流程

   在分析循環依賴之前我們先回顧下Spring mvc中 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是沒有辦法解決的。

<code><bean><constructor-arg>/<bean><bean><constructor-arg>/<bean><bean><constructor-arg>/<bean>/<code>

執行流程

Spring系列:Spring循環依賴知多少?(不一樣的深度分析)

分析: 在解析xml中的bean元素生成BeanDefination對象時,constructor-arg節點,最終會被賦值到constructorArgumentValues這個Map中,作為構造函數入參。在創建AService時解析構造函數對象時,發現有BService的引用,此時創建BService,發現又有CService的引用,而CService又引用了AService。在實例化Bean時,會將beanName存入singletonsCurrentlyInCreation集合中,當發現重複時,即說明有循環依賴,拋出異常。綜上,對於構造器循環依賴,Spring也無法解決。

Spring系列:Spring循環依賴知多少?(不一樣的深度分析)


<code>/** * Callback before singleton creation. * 

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); }}

/<code>

Field屬性或Property循環依賴

   在xml文件配置property屬性或者使用@Autowired註解,其實這兩種同屬於一類。下面分析Spring是如何通過提前曝光機制+三級緩存來排除bean之間依賴的。

三級緩存

<code>/** Cache of singleton objects: bean name --> bean instance */一級緩存:維護著所有創建完成的Beanprivate final Map<string> singletonObjects = new ConcurrentHashMap<string>(256);/** Cache of early singleton objects: bean name --> bean instance */二級緩存:維護早期暴露的Bean(只進行了實例化,並未進行屬性注入)private final Map<string> earlySingletonObjects = new HashMap<string>(16);/** Cache of singleton factories: bean name --> ObjectFactory */三級緩存:維護創建中Bean的ObjectFactory(解決循環依賴的關鍵)private final Map<string>> singletonFactories = new HashMap<string>>(16);/<string>/<string>/<string>/<string>/<string>/<string>/<code>

依賴排除

    由Spring Bean創建的過程,首先Spring會嘗試從緩存中獲取,這個緩存就是指singletonObjects,主要調用的方法是getSingleton;如果緩存中沒有,則調下Spring bean創建過程中,最重要的一個方法doCreateBean。

<code>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);}/<code>

分析: Spring首先從singletonObjects(一級緩存)中嘗試獲取,如果獲取不到並且對象在創建中,則嘗試從earlySingletonObjects(二級緩存)中獲取,如果還是獲取不到並且允許從singletonFactories通過getObject獲取,則通過三級緩存獲取,即通過singletonFactory.getObject()。如果獲取到了,將其存入二級緩存,並清除三級緩存。

   如果緩存中沒有bean對象,那麼Spring會創建Bean對象,將實例化的bean提前曝光,並且加入緩存中。

<code>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;}/<object>/<code>

分析: 當通過無參構造,獲得一個實例化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對應實例唯一性,這點回味無窮。


Spring系列:Spring循環依賴知多少?(不一樣的深度分析)


第三級緩存ObjectFactory的作用

   我們已經知道了Spring如何解決循環依賴了,但是對於Spring為什麼這麼設計,總感覺雲裡霧裡,網上的博文大都沒有講這點。下面作者談下自己的觀點。

   三級緩存採用工廠設計模式,通過getObject方法獲取bean,通過工廠獲取bean最終是一個完整的bean,因為getObject這個方法本身就包含了一些生命週期回調的功能,判斷是否創建代理類,如果需要就會創建代理類。就循環依賴而言,當BService通過populateBean注入AService時,通過三級緩存可以獲取到最終引用地址,這就是AService最終初始化後形成的bean,保證了在循環依賴能獲取到真正的依賴。

<code>@Overridepublic Object getObject() throws BeansException {    return getEarlyBeanReference(beanName, mbd, bean);}/<code>
<code>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;}/<code>


Spring系列:Spring循環依賴知多少?(不一樣的深度分析)

分析: 通過BeanFactory的getObject()方法,調用getEarlyBeanReference方法,對其包裝,最終生成代理對象。而AService實例化後,最終,通過從二級緩存中獲取到的對象其實是就是BeanFactory對應的引用。


第二級緩存earlySingletonObjects的作用

既然有了三級緩存了,為什麼還要設計二級緩存呢?可能很多人覺得二級緩存是個雞肋,可有可無,其實這是Spring大量使用緩存提高性能的一點體現。每次都通過工廠去拿,需要遍歷所有的後置處理器、判斷是否創建代理對象,而判斷是否創建代理對象本身也是一個複雜耗時的過程。設計二級緩存避免再次調用調用getEarlyBeanReference方法,提高bean加載流程。只能說,Spring是個海洋。

緩存生命週期

   實例化的bean是何時加入緩存中,又是何時將其刪除的,它們之間有什麼區別呢?接下來,本文會一一作答。

  • 三級緩存

   當earlySingletonExposure屬性為true時,將beanFactory加入緩存;當通過getSingleton從三級緩存中取出實例化的原始bean時或者完成初始化後,並清除singletonFactories中bean的緩存。

  • 二級緩存

   當earlySingletonExposure屬性為true時,將beanFactory加入緩存,當通過getSingleton從三級緩存中取出實例化的原始bean時,此時,將獲取的bean加入二級緩存。當完成bean初始化,將bean加入一級緩存後,清除二級緩存;

  • 一級緩存

   當完成bean初始化,通過addSingleton將bean加入一級緩存singletonObjects中,並且這個緩存是常駐內存中的。

   從上述分析可知,三級緩存和二級緩存是不共存的,且其在Spring完成初始化,都會被清除掉。

<code>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);    }}/<code>

總結

  • Spring不能解決構造器循環依賴,主要原因循環獲取獲取構造參數時,將bean存入singletonsCurrentlyInCreation中,在創建bean的前置校驗中,發現有已經存在的且相互依賴的bean在創建中,校驗不通過,無法創建bean;
  • Spring通過提前暴露機制+緩存解決property或field循環依賴,每次獲取時,先從緩存中取,取不到時,再進行實例化,實例化後,將其加入三級緩存,供其他bean使用;
  • 解決循環依賴中,三級緩存自動升級為二級緩存及bean初始化後,自動清除;在bean完成初始化後,二級緩存將會清除;
  • 二級緩存的存在,避免循環依賴中再次通過工廠獲取bean這一複雜流程,提升加載效率;
  • 最後,作者還想對目前大多數網上所說Spring bean初始化時機進行下補充。目前大多數博文都說bean初始化是在initializeBean這個方法中,固然沒錯,但作者認為在存在依賴時,屬性注入populateBean這個方法本身也存在著bean的初始化,即循環依賴時獲取的bean本身就是一個初始化了的bean。


作者:逸風田園
原文鏈接:https://juejin.im/post/5dbb9fdef265da4d4c202483


分享到:


相關文章: