還記得上一篇筆記,在 bean
加載流程,在建立過程當中,出現了依賴循環的監測,若是出現了這個循環依賴,而沒有解決的話,代碼中將會報錯,而後 Spring
容器初始化失敗。java
因爲感受循環依賴是個比較獨立的知識點,因此我將它的分析單獨寫一篇筆記,來看下什麼是循環依賴和如何解決它。git
循環依賴就是循環引用,就是兩個或者多個 bean
相互之間的持有對方,最後造成一個環。例如 A
引用了 B
,B
引用了 C
,C
引用了 A
。github
能夠參照下圖理解(圖中展現的類的互相依賴,但循環調用指的是方法之間的環調用,下面代碼例子會展現方法環調用):spring
若是學過數據庫的同窗,能夠將循環依賴簡單的理解爲死鎖,互相持有對方的資源,造成一個環,而後不釋放資源,致使死鎖發生。數據庫
在循環調用中,除非出現終結條件,不然將會無限循環,最後致使內存溢出錯誤。(我也遇到過一次 OOM,也是無限循環致使的)segmentfault
書中的例子是用了三個類進行環調用,我爲了簡單理解和演示,使用了兩個類進行環調用:緩存
在 Spring
中,循環依賴分爲如下三種狀況:多線程
經過上圖的配置方法,在初始化的時候就會拋出 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
,因而去實例化 circleB
,B
也放入池中,因爲依賴了 A
,接着想要實例化 A
,發如今建立 bean
過程當中發現本身已經在 「當前建立 bean
」 裏時,因而就會拋出 BeanCurrentlyInCreationException
異常。
如圖中展現,這種經過構造器注入的循環依賴,是沒法解決的。
property
原型屬於一種做用域,因此首先來了解一下做用域 scope
的概念:
在 Spring
容器中,在Spring容器中是指其建立的 Bean
對象相對於其餘 Bean
對象的請求可見範圍
咱們最經常使用到的是單例 singleton
做用域的 bean
,Spring
容器中只會存在一個共享的 Bean
實例,因此咱們每次獲取一樣 id
時,只會返回bean的同一實例。
使用單例的好處有兩個:
bean
,將有問題的配置問題提早暴露bean
實例放入單例緩存 singletonFactories
中,當須要再次使用時,直接從緩存中取,加快了運行效率。單一實例會被存儲在單例緩存 singletonFactories
中,爲Spring的缺省做用域.
看完了單例做用域,來看下 property
做用域的概念:在 Spring
調用原型 bean
時,每次返回的都是一個新對象,至關於 new Object()
。
由於 Spring
容器對原型做用域的 bean
是不進行緩存,所以沒法提早暴露一個建立中的 bean
,因此也是沒法解決這種狀況的循環依賴。
對於 setter
注入形成的依賴能夠經過 Spring
容器提早暴露剛完成構造器注入但未完成其餘步驟(如 setter
注入)的 bean
來完成,並且只能解決單例做用域的 bean
依賴。
在類的加載中,核心方法 org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#doCreateBean
,在這一步中有對循環依賴的校驗和處理。
跟進去方法可以發現,若是 bean
是單例,而且容許循環依賴,那麼能夠經過提早暴露一個單例工廠方法,從而使其餘 bean
能引用到,最終解決循環依賴的問題。
仍是按照上面新建的兩個類, CircleA
和 CircleB
,來說下 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
操做。要想着,寫的代碼都有用處,因此接下來看下這個操做解決的什麼問題和在哪裏使用到吧
用一開始建立的 CircleA
和 CircleB
這兩個循環引用的類做爲例子:
A
類中含有屬性 B
,B
類中含有屬性 A
,這兩個類在初始化的時候經歷瞭如下的步驟:
beanA
,先記錄對應的 beanName
而後將 beanA
的建立工廠 beanFactoryA 放入緩存中beanA
的屬性填充方法 populateBean
,檢查到依賴 beanB
,緩存中沒有 beanB
的實例或者單例緩存,因而要去實例化 beanB
。beanB
,經歷建立 beanA
的過程,到了屬性填充方法,檢查到依賴了 beanA
。getBean(A)
方法,在這個函數中,不是真正去實例化 beanA
,而是先去檢測緩存中是否有已經建立好的對應的 bean
,或者已經建立好的 beanFactory
beanFactoryA
已經建立好了,而是直接調用 ObjectFactory
去建立 beanA
BeanWrapper instanceWrapper = createBeanInstance(beanName, mbd, args);
// 原始 bean
final Object bean = instanceWrapper.getWrappedInstance();
複製代碼
在這一步中,建立的是原始 bean
,由於還沒到最後一步屬性解析,因此這個類裏面沒有屬性值,能夠將它想象成 new ClassA
,同時沒有構造函數等賦值的操做,這個原始 bean
信息將會在下一步使用到。
// 註釋 5.2 解決循環依賴 第二個參數是回調接口,實現的功能是將切面動態織入 bean
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
複製代碼
前面也提到過這個方法,它會將須要提早曝光的單例加入到緩存中,將單例的 beanName
和 beanFactory
加入到緩存,在以後須要用到的時候,直接從緩存中取出來。
剛纔第一步時也說過了,一開始建立的只是初始 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
放入到緩存中的信息就起到了做用。
還記得以前在類加載時學到的只是麼,單例模式每次加載都是取同一個對象,若是在緩存中有,能夠直接取出來,在緩存中沒有的話才進行加載,因此再來熟悉一下取單例的方法:
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
時,CircleA
的 beanFactory
就提早暴露。
因此 CircleB
在獲取單例 getSingleton()
時,可以拿到 CircleA
的信息,因此 CircleB
順利加載完成,同時將本身的信息加入到緩存和註冊表中,接着返回去繼續加載 CircleA
,因爲它的依賴已經加載到緩存中,因此 CircleA
也可以順利完成加載,最終整個加載操做完成~
結合解決場景的流程圖和關鍵代碼流程,比較完善的介紹了循環依賴處理方法,下面還有一個 debug
流程圖,但願能加深你的理解~
寫這篇總結的目的是爲了填坑,由於以前在解析類加載的文章中只是簡單的過了一下循環依賴的概念,想要將在類加載中留下的坑填掉。
在分析循環依賴的過程當中,發現以前對做用域 scope
的不瞭解,因而補充了一下這個知識點,接着又發現對循環依賴中使用到的緩存和詳細處理不熟悉,因而查閱了相關資料,跟蹤源碼,一步一步進行分析,因此發現越寫越多,解決了一個困惑,增長了幾個疑問,因此在不斷排查和了解中,加深了對 Spring
的理解。
一樣,在工做中,常常會遇到與其它團隊的合做,也會遇到同時須要對方的新接口支持,例如在 RPC
中遇到循環調用,那我建議仍是換一種方案,例如經過消息解耦,避免循環調用,實在沒辦法要循環調用,要記得在方法中加上退出條件,避免無限循環(>_<)
因爲我的技術有限,若是有理解不到位或者錯誤的地方,請留下評論,我會根據朋友們的建議進行修正
spring-analysis-note 碼雲 Gitee 地址
spring-analysis-note Github 地址