Spring 源碼學習(五)循環依賴

填坑,分析循環依賴

還記得上一篇筆記,在 bean 加載流程,在建立過程當中,出現了依賴循環的監測,若是出現了這個循環依賴,而沒有解決的話,代碼中將會報錯,而後 Spring 容器初始化失敗。java

因爲感受循環依賴是個比較獨立的知識點,因此我將它的分析單獨寫一篇筆記,來看下什麼是循環依賴和如何解決它。git


循環依賴

循環依賴就是循環引用,就是兩個或者多個 bean 相互之間的持有對方,最後造成一個環。例如 A 引用了 BB 引用了 CC 引用了 Agithub

能夠參照下圖理解(圖中展現的類的互相依賴,但循環調用指的是方法之間的環調用,下面代碼例子會展現方法環調用):spring

circle_use

若是學過數據庫的同窗,能夠將循環依賴簡單的理解爲死鎖,互相持有對方的資源,造成一個環,而後不釋放資源,致使死鎖發生。數據庫

在循環調用中,除非出現終結條件,不然將會無限循環,最後致使內存溢出錯誤。(我也遇到過一次 OOM,也是無限循環致使的segmentfault


書中的例子是用了三個類進行環調用,我爲了簡單理解和演示,使用了兩個類進行環調用:緩存

Spring 中,循環依賴分爲如下三種狀況:多線程

構造器循環依賴

circle_method

經過上圖的配置方法,在初始化的時候就會拋出 BeanCurrentlyInCreationException 異常併發

public static void main(String[] args) {
	// 報錯緣由: Requested bean is currently in creation: Is there an unresolvable circular reference?
	ApplicationContext context = new ClassPathXmlApplicationContext("circle/circle.xml");
}
複製代碼

從上一篇筆記中知道,Spring 容器將每個正在建立的 bean 標識符放入一個 「當前建立 bean 池(prototypesCurrentlyInCreation)」 中,bean 標識符在建立過程當中將一直保持在這個池中。mvc

檢測循環依賴的方法:

分析上面的例子,在實例化 circleA 時,將本身 A 放入池中,因爲依賴了 circleB,因而去實例化 circleBB 也放入池中,因爲依賴了 A,接着想要實例化 A,發如今建立 bean 過程當中發現本身已經在 「當前建立 bean」 裏時,因而就會拋出 BeanCurrentlyInCreationException 異常。

如圖中展現,這種經過構造器注入的循環依賴,是沒法解決的


property 範圍的依賴處理

property 原型屬於一種做用域,因此首先來了解一下做用域 scope 的概念:

Spring 容器中,在Spring容器中是指其建立的 Bean 對象相對於其餘 Bean 對象的請求可見範圍

咱們最經常使用到的是單例 singleton 做用域的 beanSpring 容器中只會存在一個共享的 Bean 實例,因此咱們每次獲取一樣 id 時,只會返回bean的同一實例。

使用單例的好處有兩個:

  1. 提早實例化 bean,將有問題的配置問題提早暴露
  2. bean 實例放入單例緩存 singletonFactories 中,當須要再次使用時,直接從緩存中取,加快了運行效率。

單一實例會被存儲在單例緩存 singletonFactories 中,爲Spring的缺省做用域.

看完了單例做用域,來看下 property 做用域的概念:在 Spring 調用原型 bean 時,每次返回的都是一個新對象,至關於 new Object()

由於 Spring 容器對原型做用域的 bean 是不進行緩存,所以沒法提早暴露一個建立中的 bean,因此也是沒法解決這種狀況的循環依賴。


setter 循環依賴

對於 setter 注入形成的依賴能夠經過 Spring 容器提早暴露剛完成構造器注入但未完成其餘步驟(如 setter 注入)的 bean 來完成,並且只能解決單例做用域的 bean 依賴。

在類的加載中,核心方法 org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#doCreateBean,在這一步中有對循環依賴的校驗和處理。

跟進去方法可以發現,若是 bean 是單例,而且容許循環依賴,那麼能夠經過提早暴露一個單例工廠方法,從而使其餘 bean 能引用到,最終解決循環依賴的問題。

仍是按照上面新建的兩個類, CircleACircleB,來說下 setter 解決方法:

配置:

<!--註釋 5.3 setter 方法注入-->
<bean id="circleA" class="base.circle.CircleA">
	<property name="circleB" ref="circleB"/>
</bean>

<bean id="circleB" class="base.circle.CircleB">
	<property name="circleA" ref="circleA"/>
</bean>
複製代碼

執行 Demo 和輸出:

public static void main(String[] args) {
	ApplicationContext context = new ClassPathXmlApplicationContext("circle/circle.xml");
	CircleA circleA = (CircleA) context.getBean("circleA");
	circleA.a();
}

在 a 方法中,輸出 A,在 b 方法中,輸出B,下面是執行 demo 輸出的結果:
錯誤提示是由於兩個方法互相調用進行輸出,而後打印到必定行數提示 main 函數棧溢出了=-=

A
B
A
B
*** java.lang.instrument ASSERTION FAILED ***: "!errorOutstanding" with message transform method call failed at JPLISAgent.c line: 844
*** java.lang.instrument ASSERTION FAILED ***: "!errorOutstanding" with message transform method call failed at JPLISAgent.c line: 844
*** java.lang.instrument ASSERTION FAILED ***: "!errorOutstanding" with message transform method call failed at JPLISAgent.c line: 844
Exception in thread "main" java.lang.StackOverflowError
複製代碼

能夠看到經過 setter 注入,成功解決了循環依賴的問題,那解決的具體代碼是如何實現的呢,下面來分析一下:


代碼分析

爲了更好的理解循環依賴,首先來看下這三個變量(也叫緩存,能夠全局調用的)的含義和用途:

/** Cache of singleton objects: bean name to bean instance. */
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

/** Cache of singleton factories: bean name to ObjectFactory. */
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);

/** Cache of early singleton objects: bean name to bean instance. */
private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);
複製代碼
變量 用途
singletonObjects 用於保存 BeanName 和建立 bean 實例之間的關係,bean-name --> instanct
singletonFactories 用於保存 BeanName 和建立 bean工廠 之間的關係,bean-name --> objectFactory
earlySingletonObjects 也是保存 beanName 和建立 bean 實例之間的關係,與 singletonObjects不一樣之處在於,當一個單例 bean 被放入到這裏以後,那麼其餘 bean 在建立過程當中,就能經過 getBean 方法獲取到,目的是用來檢測循環引用

以前講過類加載的機制了,下面定位到建立 bean 時,解決循環依賴的地方:

org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#doCreateBean

// 是否須要提早曝光,用來解決循環依賴時使用
boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
		isSingletonCurrentlyInCreation(beanName));
if (earlySingletonExposure) {
	if (logger.isTraceEnabled()) {
		logger.trace("Eagerly caching bean '" + beanName +
				"' to allow for resolving potential circular references");
	}
	// 註釋 5.2 解決循環依賴 第二個參數是回調接口,實現的功能是將切面動態織入 bean
	addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
}

protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
Assert.notNull(singletonFactory, "Singleton factory must not be null");
	synchronized (this.singletonObjects) {
		// 判斷 singletonObjects 不存在 beanName
		if (!this.singletonObjects.containsKey(beanName)) {
		// 註釋 5.4 放入 beanName -> beanFactory,到時在 getSingleton() 獲取單例時,可直接獲取建立對應 bean 的工廠,解決循環依賴
		this.singletonFactories.put(beanName, singletonFactory);
		// 從提早曝光的緩存中移除,以前在 getSingleton() 放入的
		this.earlySingletonObjects.remove(beanName);
		// 往註冊緩存中添加 beanName
		this.registeredSingletons.add(beanName);
	}
}
}
複製代碼

先來看 earlySingletonExposure 這個變量: 從字面意思理解就是須要提早曝光的單例

有如下三個判斷條件:

  • mbd 是不是單例
  • 該容器是否容許循環依賴
  • 判斷該 bean 是否在建立中。

若是這三個條件都知足的話,就會執行 addSingletonFactory 操做。要想着,寫的代碼都有用處,因此接下來看下這個操做解決的什麼問題和在哪裏使用到吧


解決場景

用一開始建立的 CircleACircleB 這兩個循環引用的類做爲例子:

deal_with_circle_depend

A 類中含有屬性 BB 類中含有屬性 A,這兩個類在初始化的時候經歷瞭如下的步驟:

  1. 建立 beanA,先記錄對應的 beanName 而後將 beanA建立工廠 beanFactoryA 放入緩存中
  2. beanA 的屬性填充方法 populateBean,檢查到依賴 beanB,緩存中沒有 beanB 的實例或者單例緩存,因而要去實例化 beanB
  3. 開始實例化 beanB,經歷建立 beanA 的過程,到了屬性填充方法,檢查到依賴了 beanA
  4. 調用 getBean(A) 方法,在這個函數中,不是真正去實例化 beanA,而是先去檢測緩存中是否有已經建立好的對應的 bean,或者已經建立好的 beanFactory
  5. 檢測到 beanFactoryA 已經建立好了,而是直接調用 ObjectFactory 去建立 beanA

結合關鍵代碼梳理流程

建立原始 bean

BeanWrapper instanceWrapper = createBeanInstance(beanName, mbd, args);
// 原始 bean
final Object bean = instanceWrapper.getWrappedInstance();
複製代碼

在這一步中,建立的是原始 bean,由於還沒到最後一步屬性解析,因此這個類裏面沒有屬性值,能夠將它想象成 new ClassA,同時沒有構造函數等賦值的操做,這個原始 bean 信息將會在下一步使用到。


addSingleFactory

// 註釋 5.2 解決循環依賴 第二個參數是回調接口,實現的功能是將切面動態織入 bean
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
複製代碼

前面也提到過這個方法,它會將須要提早曝光的單例加入到緩存中,將單例的 beanNamebeanFactory 加入到緩存,在以後須要用到的時候,直接從緩存中取出來。


populateBean 填充屬性

剛纔第一步時也說過了,一開始建立的只是初始 bean,沒有屬性值,因此在這一步會解析類的屬性。在屬性解析時,會判斷屬性的類型,若是判斷到是 RuntimeBeanReference 類型,將會解析引用。

就像咱們寫的例子,CircleA 引用了 CircleB,在加載 CircleA時,發現 CircleB 依賴,因而乎就要去加載 CircleB

咱們來看下代碼中的具體流程吧:

protected void populateBean(String beanName, RootBeanDefinition mbd, @Nullable BeanWrapper bw) {
    ...
    if (pvs != null) {
		// 將屬性應用到 bean 中,使用深拷貝,將子類的屬性一併拷貝
		applyPropertyValues(beanName, mbd, bw, pvs);
	}
}

protected void applyPropertyValues(String beanName, BeanDefinition mbd, BeanWrapper bw, PropertyValues pvs) {
    ...
    String propertyName = pv.getName();
	Object originalValue = pv.getValue();
	// 註釋 5.5 解析參數,若是是引用對象,將會進行提早加載
	Object resolvedValue = valueResolver.resolveValueIfNecessary(pv, originalValue);
	...
}

public Object resolveValueIfNecessary(Object argName, @Nullable Object value) {
	// 咱們必須檢查每一個值,看看它是否須要一個運行時引用,而後來解析另外一個 bean
	if (value instanceof RuntimeBeanReference) {
		// 註釋 5.6 在這一步中,若是判斷是引用類型,須要解析引用,加載另外一個 bean
		RuntimeBeanReference ref = (RuntimeBeanReference) value;
		return resolveReference(argName, ref);
	}
	...
}
複製代碼

跟蹤到這裏,加載引用的流程比較清晰了,發現是引用類的話,最終會委派 org.springframework.beans.factory.support.BeanDefinitionValueResolver#resolveReference 進行引用處理,核心的兩行代碼以下:

// 註釋 5.7 在這裏加載引用的 bean
bean = this.beanFactory.getBean(refName);
this.beanFactory.registerDependentBean(refName, this.beanName);
複製代碼

在這一步進行 CircleB 的加載,可是咱們寫的例子中,CircleB 依賴了 CircleA,那它是如何處理的呢,因此這時,咱們剛纔將 CircleA 放入到緩存中的信息就起到了做用。


getSingleton

還記得以前在類加載時學到的只是麼,單例模式每次加載都是取同一個對象,若是在緩存中有,能夠直接取出來,在緩存中沒有的話才進行加載,因此再來熟悉一下取單例的方法:

protected Object getSingleton(String beanName, boolean allowEarlyReference) {
	Object singletonObject = this.singletonObjects.get(beanName);
	// 檢查緩存中是否存在實例
	if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
		// 記住,公共變量都須要加鎖操做,避免多線程併發修改
		synchronized (this.singletonObjects) {
			// 若是此 bean 正在加載則不處理
			singletonObject = this.earlySingletonObjects.get(beanName);
			if (singletonObject == null && allowEarlyReference) {
				// 當某些方法須要提早初始化,調用 addSingletonFactory 方法將對應的
				// objectFactory 初始化策略存儲在 earlySingletonObjects,而且從 singletonFactories 移除
				ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
				if (singletonFactory != null) {
					singletonObject = singletonFactory.getObject();
					this.earlySingletonObjects.put(beanName, singletonObject);
					this.singletonFactories.remove(beanName);
				}
			}
		}
	}
	return singletonObject;
}
複製代碼

雖然 CircleB 引用了 CircleA,但在以前的方法 addSingletonFactory 時,CircleAbeanFactory 就提早暴露。

因此 CircleB 在獲取單例 getSingleton() 時,可以拿到 CircleA 的信息,因此 CircleB 順利加載完成,同時將本身的信息加入到緩存和註冊表中,接着返回去繼續加載 CircleA,因爲它的依賴已經加載到緩存中,因此 CircleA 也可以順利完成加載,最終整個加載操做完成~

結合解決場景的流程圖和關鍵代碼流程,比較完善的介紹了循環依賴處理方法,下面還有一個 debug 流程圖,但願能加深你的理解~


總結

寫這篇總結的目的是爲了填坑,由於以前在解析類加載的文章中只是簡單的過了一下循環依賴的概念,想要將在類加載中留下的坑填掉。

在分析循環依賴的過程當中,發現以前對做用域 scope 的不瞭解,因而補充了一下這個知識點,接着又發現對循環依賴中使用到的緩存和詳細處理不熟悉,因而查閱了相關資料,跟蹤源碼,一步一步進行分析,因此發現越寫越多,解決了一個困惑,增長了幾個疑問,因此在不斷排查和了解中,加深了對 Spring 的理解。

一樣,在工做中,常常會遇到與其它團隊的合做,也會遇到同時須要對方的新接口支持,例如在 RPC 中遇到循環調用,那我建議仍是換一種方案,例如經過消息解耦,避免循環調用,實在沒辦法要循環調用,要記得在方法中加上退出條件,避免無限循環(>_<)


因爲我的技術有限,若是有理解不到位或者錯誤的地方,請留下評論,我會根據朋友們的建議進行修正

spring-analysis-note 碼雲 Gitee 地址

spring-analysis-note Github 地址


參考資料

  1. Spring學習(十五)Spring Bean 的5種做用域介紹
  2. Spring IOC 容器源碼分析 - 循環依賴的解決辦法
  3. Spring 源碼深度解析》- 郝佳

傳送門:

相關文章
相關標籤/搜索