spring源碼閱讀之循環依賴的解決

循環依賴就是循環引用,就是兩個或多個Bean相互之間的持有對方,比如CircleA引用CircleB,CircleB引用CircleC,CircleC引用CircleA,則它們最終反映爲一個環。此處不是循環調用,循環調用是方法之間的環調用。如下圖所示:

循環調用是無法解決的,除非有終結條件,否則就是死循環,最終導致內存溢出錯誤。

循環依賴的產生和解決的前提

循環依賴的產生可能有很多種情況,例如:

  • A的構造方法中依賴了B的實例對象,同時B的構造方法中依賴了A的實例對象
  • A的構造方法中依賴了B的實例對象,同時B的某個field或者setter需要A的實例對象,以及反之
  • A的某個field或者setter依賴了B的實例對象,同時B的某個field或者setter依賴了A的實例對象,以及反之

當然,Spring對於循環依賴的解決不是無條件的,首先前提條件是針對scope單例並且沒有顯式指明不需要解決循環依賴的對象,而且要求該對象沒有被代理過。同時Spring解決循環依賴也不是萬能,以上三種情況只能解決兩種,第一種在構造方法中相互依賴的情況Spring也無力迴天。結論先給在這,下面來看看Spring的解決方法,知道了解決方案就能明白爲啥第一種情況無法解決了。

Spring對於循環依賴的解決

Spring循環依賴的理論依據其實是Java基於引用傳遞,當我們獲取到對象的引用時,對象的field或者或屬性是可以延後設置的。
Spring單例對象的初始化其實可以分爲三步:

  • createBeanInstance, 實例化,實際上就是調用對應的構造方法構造對象,此時只是調用了構造方法,spring xml中指定的property並沒有進行populate
  • populateBean,填充屬性,這步對spring xml中指定的property進行populate
  • initializeBean,調用spring xml中指定的init方法,或者AfterPropertiesSet方法
    會發生循環依賴的步驟集中在第一步和第二步。

三級緩存

對於單例對象來說,在Spring的整個容器的生命週期內,有且只存在一個對象,很容易想到這個對象應該存在Cache中,Spring大量運用了Cache的手段,在循環依賴問題的解決過程中甚至使用了“三級緩存”。

在DefaultSingletonBeanRegistry使用三級緩存,“三級緩存”主要是指:

// 第一層: 初始化完備的單例bean
/** Cache of singleton objects: bean name to bean instance. */
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
// 第二層: 提前曝光的單例對象的Cache 
/** Cache of early singleton objects: bean name to bean instance. */
private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);
// 第三層: ObjectFactory工廠bean緩存, 存儲實例話後的bean Factory
/** Cache of singleton factories: bean name to ObjectFactory. */
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);

從字面意思來說:singletonObjects指單例對象的cache,singletonFactories指單例對象工廠的cache,earlySingletonObjects指提前曝光的單例對象的cache。以上三個cache構成了三級緩存,Spring就用這三級緩存巧妙的解決了循環依賴問題。

解決辦法:

Spring關於Bean創建的過程getBean(),首先Spring會嘗試從緩存中獲取單例對象,這個緩存就是指singletonObjects,

緩存中獲取單例對象---主要調用的方法是:

	/**
	 * Return the (raw) singleton object registered under the given name.
	 * <p>Checks already instantiated singletons and also allows for an early
	 * reference to a currently created singleton (resolving a circular reference).
	 * @param beanName the name of the bean to look for
	 * @param allowEarlyReference whether early references should be created or not
	 * @return the registered singleton object, or {@code null} if none found
	 */
	@Nullable
	protected Object getSingleton(String beanName, boolean allowEarlyReference) {
		Object singletonObject = this.singletonObjects.get(beanName);
		if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
			synchronized (this.singletonObjects) {
				singletonObject = this.earlySingletonObjects.get(beanName);
				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;
	}

首先解釋兩個參數:

  • isSingletonCurrentlyInCreation 判斷對應的單例對象是否在創建中,當單例對象沒有被初始化完全(例如A定義的構造函數依賴了B對象,得先去創建B對象,或者在populatebean過程中依賴了B對象,得先去創建B對象,此時A處於創建中)
  • allowEarlyReference 是否允許從singletonFactories中通過getObject拿到對象

分析getSingleton的整個過程,Spring首先從singletonObjects(一級緩存)中嘗試獲取,如果獲取不到並且對象在創建中,則嘗試從earlySingletonObjects(二級緩存)中獲取,如果還是獲取不到並且允許從singletonFactories通過getObject獲取,則通過singletonFactory.getObject()(三級緩存)獲取。如果獲取到了則

this.earlySingletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);

則移除對應的singletonFactory,將singletonObject放入到earlySingletonObjects,其實就是將三級緩存提升到二級緩存中!

Spring解決循環依賴的訣竅就在於singletonFactories這個cache,這個cache中存的是類型爲ObjectFactory,其定義如下:

public interface ObjectFactory<T> {
    T getObject() throws BeansException;}

在bean創建過程中,有兩處比較重要的匿名內部類實現了該接口。一處是

new ObjectFactory<Object>() {
   @Override   public Object getObject() throws BeansException {
      try {
         return createBean(beanName, mbd, args);
      }      catch (BeansException ex) {
         destroySingleton(beanName);
         throw ex;
      }   }

另一處就是:

addSingletonFactory(beanName, new ObjectFactory<Object>() {
   @Override   public Object getObject() throws BeansException {
      return getEarlyBeanReference(beanName, mbd, bean);
   }});

此處就是解決循環依賴的關鍵,這段代碼發生在createBeanInstance之後,也就是說單例對象此時已經被創建出來的。這個對象已經被生產出來了,雖然還不完美(還沒有進行初始化的第二步和第三步),但是已經能被人認出來了(根據對象引用能定位到堆中的對象),所以Spring此時將這個對象提前曝光出來讓大家認識,讓大家使用。

這樣做有什麼好處呢?讓我們來分析一下“A的某個field或者setter依賴了B的實例對象,同時B的某個field或者setter依賴了A的實例對象”這種循環依賴的情況。A首先完成了初始化的第一步,並且將自己提前曝光到singletonFactories中,此時進行初始化的第二步,發現自己依賴對象B,此時就嘗試去get(B),發現B還沒有被create,所以走create流程,B在初始化第一步的時候發現自己依賴了對象A,於是嘗試get(A),嘗試一級緩存singletonObjects(肯定沒有,因爲A還沒初始化完全),嘗試二級緩存earlySingletonObjects(也沒有),嘗試三級緩存singletonFactories,由於A通過ObjectFactory將自己提前曝光了,所以B能夠通過ObjectFactory.getObject拿到A對象(雖然A還沒有初始化完全,但是總比沒有好呀),B拿到A對象後順利完成了初始化階段1、2、3,完全初始化之後將自己放入到一級緩存singletonObjects中。此時返回A中,A此時能拿到B的對象順利完成自己的初始化階段2、3,最終A也完成了初始化,長大成人,進去了一級緩存singletonObjects中,而且更加幸運的是,由於B拿到了A的對象引用,所以B現在hold住的A對象也蛻變完美了!一切都是這麼神奇!!

知道了這個原理時候,肯定就知道爲啥Spring不能解決“A的構造方法中依賴了B的實例對象,同時B的構造方法中依賴了A的實例對象”這類問題了!

Spring通過三級緩存加上“提前曝光”機制,配合Java的對象引用原理,比較完美地解決了某些情況下的循環依賴問題!

Spring容器循環依賴包括構造器循環依賴和setter循環依賴,那Spring容器如何解決循環依賴呢?首先讓我們來定義三個循環引用類:

CircleA.java

 

public class CircleA {

    private CircleB circleB;
    public CircleA() {
    }
    public CircleA(CircleB circleB) {
        this.circleB = circleB;
    }
    public void setCircleB(CircleB circleB)
    {
        this.circleB = circleB;
    }
    public void a() {
        circleB.b();
    }

}

CircleB.java 

public class CircleB {

    private CircleC circleC;
    public CircleB() {
    }
    public CircleB(CircleC circleC) {
        this.circleC = circleC;
    }
    public void setCircleC(CircleC circleC)
    {
        this.circleC = circleC;
    }
    public void b() {
        circleC.c();
    }

}

CircleC.java

public class CircleC {

    private CircleA circleA;
    public CircleC() {
    }
    public CircleC(CircleA circleA) {
        this.circleA = circleA;
    }
    public void setCircleA(CircleA circleA)
    {
        this.circleA = circleA;
    }
    public void c() {
        circleA.a();
    }

}

1、構造器循環依賴(此依賴是無法解決)

構造器循環依賴:表示通過構造器注入構成的循環依賴,此依賴是無法解決的,只能拋出BeanCurrentlyInCreationException異常表示循環依賴。

如在創建CircleA類時,構造器需要CircleB類,那將去創建CircleB,在創建CircleB類時又發現需要CircleC類,則又去創建CircleC,最終在創建CircleC時發現又需要CircleA;從而形成一個環,沒辦法創建。

Spring容器將每一個正在創建的Bean 標識符放在一個“當前創建Bean池”中,Bean標識符在創建過程中將一直保持在這個池中,因此如果在創建Bean過程中發現自己已經在“當前創建Bean池”裏時將拋出BeanCurrentlyInCreationException異常表示循環依賴;而對於創建完畢的Bean將從“當前創建Bean池”中清除掉。

circleInjectByConstructor.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans 
       http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="circleA" class="com.spring.framework.carl.ioc.CircleA">
        <constructor-arg index="0" ref="circleB" />
    </bean>

    <bean id="circleB" class="com.spring.framework.carl.ioc.CircleB">
        <constructor-arg index="0" ref="circleC" />
    </bean>

    <bean id="circleC" class="com.spring.framework.carl.ioc.CircleC">
        <constructor-arg index="0" ref="circleA" />
    </bean>

</beans>

測試方法:

@Test(expected = BeanCurrentlyInCreationException.class)
public void testCircleByConstructor() throws Throwable {
    try{
        new ClassPathXmlApplicationContext("circleInjectByConstructor.xml");
    } catch (Exception e){
        // 因爲要在創建circle3時拋出;
        Throwable e1 = e.getCause().getCause().getCause();
        throw e1;
    }
}

讓我們分析一下吧:

  1. Spring容器創建“circleA” Bean,首先去“當前創建Bean池”查找是否當前Bean正在創建,如果沒發現,則繼續準備其需要的構造器參數“circleB”,並將“circleA” 標識符放到“當前創建Bean池”;
  2. Spring容器創建“circleB” Bean,首先去“當前創建Bean池”查找是否當前Bean正在創建,如果沒發現,則繼續準備其需要的構造器參數“circleC”,並將“circleB” 標識符放到“當前創建Bean池”;
  3. Spring容器創建“circleC” Bean,首先去“當前創建Bean池”查找是否當前Bean正在創建,如果沒發現,則繼續準備其需要的構造器參數“circleA”,並將“circleC” 標識符放到“當前創建Bean池”;
  4. 到此爲止Spring容器要去創建“circleA”Bean,發現該Bean 標識符在“當前創建Bean池”中,因爲表示循環依賴,拋出BeanCurrentlyInCreationException。

2、setter循環依賴(singleton)(“提前曝光”機制剛完成構造器注入但未完成其他步驟(如setter注入)的Bean來完成,只能解決單例作用域的Bean循環依賴)

setter循環依賴:表示通過setter注入方式構成的循環依賴。

對於setter注入造成的依賴是通過Spring容器提前暴露剛完成構造器注入但未完成其他步驟(如setter注入)的Bean來完成的,而且只能解決單例作用域的Bean循環依賴。

如下代碼所示,通過提前暴露一個單例工廠方法,從而使其他Bean能引用到該Bean。

addSingletonFactory(beanName, new ObjectFactory() {  
    public Object getObject() throws BeansException {  
        return getEarlyBeanReference(beanName, mbd, bean);  
    }  
}); 

具體步驟如下:

  1. Spring容器創建單例“circleA” Bean,首先根據無參構造器創建Bean,並暴露一個“ObjectFactory ”用於返回一個提前暴露一個創建中的Bean,並將“circleA” 標識符放到“當前創建Bean池”;然後進行setter注入“circleB”;
  2. Spring容器創建單例“circleB” Bean,首先根據無參構造器創建Bean,並暴露一個“ObjectFactory”用於返回一個提前暴露一個創建中的Bean,並將“circleB” 標識符放到“當前創建Bean池”,然後進行setter注入“circleC”;
  3. Spring容器創建單例“circleC” Bean,首先根據無參構造器創建Bean,並暴露一個“ObjectFactory ”用於返回一個提前暴露一個創建中的Bean,並將“circleC” 標識符放到“當前創建Bean池”,然後進行setter注入“circleA”;進行注入“circleA”時由於提前暴露了“ObjectFactory”工廠從而使用它返回提前暴露一個創建中的Bean;
  4. 最後在依賴注入“circleB”和“circleA”,完成setter注入。

circleInjectBySetterSingleton.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans 
       http://www.springframework.org/schema/beans/spring-beans.xsd">


    <bean id="circleA" class="com.spring.framework.carl.ioc.CircleA">
        <property name="circleB" ref="circleB" />
    </bean>

    <bean id="circleB" class="com.spring.framework.carl.ioc.CircleB">
        <property name="circleC" ref="circleC" />
    </bean>

    <bean id="circleC" class="com.spring.framework.carl.ioc.CircleC">
        <property name="circleA" ref="circleA" />
    </bean>

</beans>

測試方法:

@Test
public void testCircleBySetterSingleton(){
    new ClassPathXmlApplicationContext("circleInjectBySetterSingleton.xml");
}

3、setter循環依賴(非singleton)(此依賴是無法解決)

對於prototype作用域Bean,Spring容器無法完成依賴注入,因爲prototype作用域的Bean,Spring容器不進行緩存,因此無法提前暴露一個創建中的Bean。

4、setter循環依賴(singleton設置不允許依賴注入)【這裏無法解決循環依賴,只是如何禁用循環引用】

參考:https://www.cnblogs.com/duanxz/p/3491820.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章