被問麻了,Spring 如何處理迴圈依賴?
前言
Spring如何處理迴圈依賴?這是最近較為頻繁被問到的一個面試題,在前面Bean例項化流程中,對屬性注入一文多多少少對迴圈依賴有過介紹,這篇文章詳細講一下Spring中的迴圈依賴的處理方案。
什麼是迴圈依賴
依賴指的是Bean與Bean之間的依賴關係,迴圈依賴指的是兩個或者多個Bean相互依賴,如:
構造器迴圈依賴
程式碼示例:
``` public class BeanA {
private BeanB beanB;
public BeanA(BeanB beanB){
this.beanB = beanB;
}
}
public class BeanB {
private BeanA beanA;
public BeanB(BeanA beanA){
this.beanA = beanA;
}
} ```
配置檔案
```
Setter迴圈依賴
程式碼示例
``` public class BeanA {
private BeanB beanB;
public void setBeanB(BeanB beanB){
this.beanB = beanB;
}
}
@Data public class BeanB {
private BeanA beanA;
public void setBeanA(BeanA beanA){
this.beanA = beanA;
}
} ```
配置檔案
```
迴圈依賴包括: 構造器注入迴圈依賴 set , 注入迴圈依賴 和 prototype模式Bean的迴圈依賴。Spring只解決了單例Bean的 setter 注入迴圈依賴,對於構造器迴圈依賴,和 prototype模式的迴圈依賴是無法解決的,在建立Bean的時候就會丟擲異常 :“BeanCurrentlyInCreationException” ,
迴圈依賴控制開關在 AbstractRefreshableApplicationContext 容器工廠類中有定義:
``` public abstract class AbstractRefreshableApplicationContext extends AbstractApplicationContext {
@Nullable private Boolean allowBeanDefinitionOverriding; //是否允許迴圈依賴 @Nullable private Boolean allowCircularReferences;
//設定迴圈依賴 public void setAllowCircularReferences(boolean allowCircularReferences) { this.allowCircularReferences = allowCircularReferences; } ```
預設情況下是允許Bean之間的迴圈依賴的,在依賴注入時Spring會嘗試處理迴圈依賴。如果將該屬性配置為“false”則關閉迴圈依賴,當在Bean依賴注入的時遇到迴圈依賴時丟擲異常。可以通過如下方式關閉,但是一般都不這麼做
ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("bean.xml");
//禁用迴圈依賴
applicationContext.setAllowCircularReferences(false);
//重新整理容器
applicationContext.refresh();
...
構造器迴圈依賴處理
構造器是不允許迴圈依賴的,動動你的小腦瓜想一想,比如:A 依賴 B ,B依賴C,C依賴A,在例項化A的時候,構造器需要注入B,然後Spirng會例項化B,此時的A屬於“正在建立”的狀態。當例項化B的時候,發現構造器需要注入C,然後去例項化C,然而例項化C的時候又需要注入A的例項,這樣就造成了一個死迴圈,永遠無法先例項化出某一個Bean,所以Spring遇到這裡構造器迴圈依賴會直接丟擲異常。
那麼Spring到底是如何做的呢?
- 首先Spring會走Bean的例項化流程嘗試建立 A 的例項 ,在建立例項之間先從 “正在建立Bean池” (一個快取Map而已)中去查詢A 是否正在建立,如果沒找到,則將 A 放入 “正在建立Bean池”中,然後準備例項化構造器引數 B。
- Spring會走Bean的例項化流程嘗試建立 B 的例項 ,在建立例項之間先從 “正在建立Bean池” (一個快取Map而已)中去查詢B 是否正在建立,如果沒找到,則將 B 放入 “正在建立Bean池”中,然後準備例項化構造器引數 A。
- Spring會走Bean的例項化流程嘗試建立 A 的例項 ,在建立例項之間先從 “正在建立Bean池” (一個快取Map而已)中去查詢A 是否正在建立。
- 此時:Spring發現 A 正處於“正在建立Bean池”,表示出現構造器迴圈依賴,丟擲異常:“BeanCurrentlyInCreationException”
DefaultSingletonBeanRegistry#getSingleton
下面我們以 BeanA 構造引數依賴BeanB, BeanB 構造引數依賴BeanA 為例來分析。
當Spring的IOC容器啟動,嘗試對單利的BeanA進行初始化,根據之前的分析我們知道,單利Bean的建立入口是 AbstractBeanFactory#doGetBean 在該方法中會先從單利Bean快取中獲取,如果沒有程式碼會走到:DefaultSingletonBeanRegistry#getSingleton(jString beanName, ObjectFactory<?> singletonFactory) 方法中 ,在該方法中會先對把建立的Bean加入 一個名字為 singletonsCurrentlyInCreation 的 ConcurrentHashMap中,意思是該Bean正在建立中,然後呼叫 ObjectFactory.getObject() 例項化Bean , 假設 BeanA 進入了該方法進行例項化:
```
//正在建立中的Bean
private final Set
public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) { ...省略... //把該Bean的名字加入 singletonsCurrentlyInCreation 正在建立池 中 beforeSingletonCreation(beanName); boolean newSingleton = false; boolean recordSuppressedExceptions = (this.suppressedExceptions == null); if (recordSuppressedExceptions) { this.suppressedExceptions = new LinkedHashSet<>(); } try { //呼叫ObjectFactory建立Bean的例項 singletonObject = singletonFactory.getObject(); newSingleton = true; } ...省略...
//如果singletonsCurrentlyInCreation中沒該Bean,就把該Bean儲存到singletonsCurrentlyInCreation中, //如果 singletonsCurrentlyInCreation 中有 該Bean,就報錯迴圈依賴異常BeanCurrentlyInCreationException //也就意味著同一個beanName進入該方法2次就會拋異常 protected void beforeSingletonCreation(String beanName) { if (!this.inCreationCheckExclusions.contains(beanName) && !this.singletonsCurrentlyInCreation.add(beanName)) { throw new BeanCurrentlyInCreationException(beanName); } } ```
beforeSingletonCreation 方法非常關鍵 ,它會把beanName加入 singletonsCurrentlyInCreation,一個代表“正在建立中的Bean”的ConcurrentHashMap中。
如果singletonsCurrentlyInCreation中沒該beanName,就把該Bean儲存到singletonsCurrentlyInCreation中, 如果 singletonsCurrentlyInCreation 中有 該Bean,就報錯迴圈依賴異常BeanCurrentlyInCreationException
【注意】也就意味著同一個beanName進入該方法2次就會拋異常 , 現在BeanA已經加入了singletonsCurrentlyInCreation
AbstractAutowireCapableBeanFactory#autowireConstructor
我們前面分析過 ObjectFactory.getObject例項化Bean的詳細流程,這裡我只是大概在覆盤一下就行了。因為我們的BeanA的構造器注入了一個BeanB,所以 程式碼最終會走到AbstractAutowireCapableBeanFactory#autowireConstructor ,通過構造器來例項化BeanA(在屬性注入那一章有講到 ) 。
在autowireConstructor 方法中會通過 ConstructorResolver#resolveConstructorArguments來解析構造引數,呼叫 BeanDefinitionValueResolver 去把 ref="beanB" 這種字串的引用變成一個實實在在的Bean,即BeanB,所以在 BeanDefinitionValueResolver 屬性值解析器中又會去例項化BeanB,同樣會走到 DefaultSingletonBeanRegistry#getSingleton 中把BeanB加入 singletonsCurrentlyInCreation “正在建立Bean池”中,然後呼叫ObjectFactory.getObject例項化BeanB。
低於BeanB而已同樣需要通過構造器建立,BeanB構造器引數依賴了BeanA,也就意味著又會呼叫 BeanDefinitionValueResolver 去把 ref=“beanA” 這種字串引用變成容器中的BeanA的Bean例項,然後程式碼又會走到 DefaultSingletonBeanRegistry#getSingleton。然後再一次的嘗試把BeanA加入singletonsCurrentlyInCreation “正在建立Bean池”。
此時問題就來了,在最開始建立BeanA的時候它已經加入過一次“正在建立Bean” 池,這會兒例項化BeanB的時候,由於構造器引數依賴了BeanA,導致BeanA又想進入“正在建立Bean” 池 ,此時 Spring丟擲迴圈依賴異常:
Error creating bean with name ‘beanA’: Requested bean is currently in creation: Is there an unresolvable circular reference?
到這,Spring處理構造器迴圈依賴的原始碼分析完畢。
setter迴圈依賴處理
setter迴圈依賴是可以允許的。Spring是通過提前暴露未例項化完成的Bean的 ObjectFactory來實現迴圈依賴的,這樣做的目的是其他的Bean可以通過 ObjectFactory 引用到該Bean。
實現流程如下:
- Spring建立BeanA,通過無參構造例項化,把BeanA新增到“正在建立Bean池”中,並暴露當前例項的ObjectFactory,即把ObjectFactory新增到singletonFactories(三級快取)中,該ObjectFactory用來獲取建立中的BeanA,然後,然後通過setter注入BeanB
- Spring建立BeanB,通過無參構造例項化,把BeanB新增到“正在建立Bean池”中,並暴露一個ObjectFactory,然後,然後通過setter注入BeanA
- 在BeanB通過setter注入BeanA時,由於BeanA 提前暴露了ObjectFactory ,通過它返回一個提前暴露一個建立中的BeanA。
- 然後完成BeanB的依賴注入
這裡補張圖:
獲取Bean的時候走三級快取
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
//一級快取,儲存例項化好的Bean
Object singletonObject = this.singletonObjects.get(beanName);
//如果單利快取池中沒有,但是beanName正在建立
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
synchronized (this.singletonObjects) {
//獲取二級快取,這個裡面儲存的是正在建立的Bean,半成品
singletonObject = this.earlySingletonObjects.get(beanName);
//如果也為空,但是允許迴圈依賴
if (singletonObject == null && allowEarlyReference) {
//從三級快取獲取Bean的建立工廠,
ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
if (singletonFactory != null) {
//建立Bean的例項
singletonObject = singletonFactory.getObject();
//把Bean儲存到二級快取
this.earlySingletonObjects.put(beanName, singletonObject);
//移除三級快取中的建立工廠
this.singletonFactories.remove(beanName);
}
}
}
}
return (singletonObject != NULL_OBJECT ? singletonObject : null);
}
AbstractAutowireCapableBeanFactory#doCreateBean
我們以BeanA 通過settter依賴BeanB,BeanB通過setter 依賴BeanA為例來分析一下原始碼,在之前的Bean例項化流程分析過程中我們瞭解到,Bean的例項化會走AbstractBeanFactory#doGetBean,然後查詢單利快取中是否有該Bean ,如果沒有就呼叫 DefaultSingletonBeanRegistry#getSingleton,方法會把BeanA加入 singletonsCurrentlyInCreation “建立中的Bean池”,然後呼叫ObjectFactory.getObject建立Bean.
org.springframework.beans.factory.support.AbstractBeanFactory#doGetBean 原始碼:
```
protected
final String beanName = transformedBeanName(name); Object bean;
// Eagerly check singleton cache for manually registered singletons. //快取中獲取Bean,解決了迴圈依賴問題 Object sharedInstance = getSingleton(beanName); ...快取中沒有走下面... if (mbd.isSingleton()) { //走 DefaultSingletonBeanRegistry#getSingleton ,方法會把bean加入“正在建立bean池” //然後呼叫ObjectFactory例項化Bean sharedInstance = getSingleton(beanName, () -> { try { return createBean(beanName, mbd, args); } catch (BeansException ex) { // Explicitly remove instance from singleton cache: It might have been put there // eagerly by the creation process, to allow for circular reference resolution. // Also remove any beans that received a temporary reference to the bean. destroySingleton(beanName); throw ex; } }); bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd); } ```
第一次進來,快取中是沒有BeanA的,所有會走 getSingleton 方法,然後程式碼最終會走到AbstractAutowireCapableBeanFactory#doCreateBean 方法中 。
AbstractAutowireCapableBeanFactory#doCreateBean原始碼:
``` protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args) throws BeanCreationException {
// Instantiate the bean. BeanWrapper instanceWrapper = null; if (mbd.isSingleton()) { instanceWrapper = this.factoryBeanInstanceCache.remove(beanName); } if (instanceWrapper == null) { //例項化Bean instanceWrapper = createBeanInstance(beanName, mbd, args); } ...省略... //如果是單利 ,如果是允許迴圈依賴,如果 beanName 出於建立中,已經被新增到“建立中的bean池” boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences && isSingletonCurrentlyInCreation(beanName)); if (earlySingletonExposure) { if (logger.isDebugEnabled()) { logger.debug("Eagerly caching bean '" + beanName + "' to allow for resolving potential circular references"); } //把ObjectFactory 新增到 singletonFactories 中。 addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean)); }
try { //走依賴注入流程 populateBean(beanName, mbd, instanceWrapper); exposedObject = initializeBean(beanName, exposedObject, mbd); }
//快取單利Bean的建立工廠,用於解決迴圈依賴 protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) { Assert.notNull(singletonFactory, "Singleton factory must not be null"); synchronized (this.singletonObjects) { //singletonObjects單利快取中是否包含Bean if (!this.singletonObjects.containsKey(beanName)) { //提前暴露ObjectFactory,把ObjectFactory放到singletonFactories中, //後面解決迴圈依賴,獲取Bean例項的時候會用到 this.singletonFactories.put(beanName, singletonFactory); //早期單利bean快取中移除Bean this.earlySingletonObjects.remove(beanName); //把註冊的Bean加入registeredSingletons中 this.registeredSingletons.add(beanName); } } } ```
該方法中把BeanA例項化好之後,會把ObjectFactory儲存到一個 singletonFactories(HashMap)中來提前暴露Bean的建立工廠,用於解決迴圈依賴【重要】,然後呼叫 populateBean 走屬性注入流程。
屬性注入會通過BeanDefinition得到bean的依賴屬性,然後呼叫 AbstractAutowireCapableBeanFactory#applyPropertyValues ,把屬性應用到物件上。在applyPropertyValues 方法中最終呼叫 BeanDefinitionValueResolver#resolveValueIfNecessary 解析屬性值,比如:ref=“beanB” 這種字串引用變成 物件例項的引用。
在BeanDefinitionValueResolver解析依賴的屬性值即:BeanB的時候,同樣會觸發BeanB的例項化,程式碼會走到AbstractBeanFactory#doGetBean ,然後走方法 DefaultSingletonBeanRegistry#getSingleton 中把BeanB加入 singletonsCurrentlyInCreation “建立中的Bean池”,然後程式碼會走到AbstractAutowireCapableBeanFactory#doCreateBean 方法中建立BeanB,
該方法中會先例項化BeanB,接著會把BeanB的ObjectFactory儲存到 singletonFactories (HashMap)中來提前暴露Bean的建立工廠,用於解決迴圈依賴,然後呼叫 populateBean 走屬性注入流程。
同樣因為BeanB通過Setter 注入了 A,所以在 populateBean 屬性注入流程中會解析 ref=“beanA” 為容器中的 BeanA 的例項。
然後會走到 AbstractBeanFactory#doGetBean 中獲取BeanA的例項。這個時候流程就不一樣了,我們先看一下 AbstractBeanFactory#doGetBean 中的程式碼
```
protected
final String beanName = transformedBeanName(name); Object bean;
// Eagerly check singleton cache for manually registered singletons. //從快取中獲取Bean Object sharedInstance = getSingleton(beanName);
...省略...
//如果快取中沒有Bean,就建立Bean if (mbd.isSingleton()) { sharedInstance = getSingleton(beanName, () -> { try { return createBean(beanName, mbd, args); } catch (BeansException ex) { // Explicitly remove instance from singleton cache: It might have been put there // eagerly by the creation process, to allow for circular reference resolution. // Also remove any beans that received a temporary reference to the bean. destroySingleton(beanName); throw ex; } }); bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd); } ```
在獲取單利Bean的例項的時候是會先去單利Bean的快取中去檢視Bean是否已經存在,如果不存在,才會走DefaultSingletonBeanRegistry#getSingleton方法建立Bean。
問題是:此刻單利Bean快取中已經有BeanA了,因為在最開始BeanA已經出於“正在建立Bean池”中了。我們先來看一下是如何從快取獲取Bean的。
DefaultSingletonBeanRegistry#getSingleton(java.lang.String, boolean)原始碼如下:
``` //allowEarlyReference :是否建立早期應用,主要用來解決迴圈依賴 @Nullable protected Object getSingleton(String beanName, boolean allowEarlyReference) { // Quick check for existing instance without full singleton lock //從Map中 singletonObjects = new ConcurrentHashMap<>(256); 獲取單利Bean
//【一級快取】singletonObject快取中是否有Bean , 它儲存的是已經例項化好的Bean Object singletonObject = this.singletonObjects.get(beanName);
//如果singletonObjects中沒有Bean,但是Bean出於正在建立池中,即:Set
//【二級快取】從早期單例物件的快取 earlySingletonObjects 中獲取 singletonObject = this.earlySingletonObjects.get(beanName);
//早期單利物件快取中也沒有,但是允許迴圈依賴 if (singletonObject == null && allowEarlyReference) {
synchronized (this.singletonObjects) {
// Consistent creation of early reference within full singleton lock
singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null) {
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null) {
//【三級快取】獲取ObjectFactory , 物件建立工廠,得到Bean建立過程中提前暴露的工廠。
ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
if (singletonFactory != null) {
//通過工廠ObjectFactory 獲取物件例項
singletonObject = singletonFactory.getObject();
//把物件儲存到早期快取中
this.earlySingletonObjects.put(beanName, singletonObject);
//把ObjectFactory移除
this.singletonFactories.remove(beanName);
}
}
}
}
} } return singletonObject; } ```
這裡就是經典的三級快取解決Spring迴圈依賴。你看到了,這裡會先從 singletonObjects 單利Bean快取集合中獲取Bean(該快取是例項化完成了的Bean),如果沒有,就從earlySingletonObjects早期物件快取中獲取Bean(該快取中存放的是還未例項化完成的早期Bean),如果還是沒有,就從singletonFactories中得到暴露的ObjectFactory來獲取依賴的Bean。然後放入早期快取中。並把ObjectFactory從singletonFactories中移除。最後返回Bean的例項。
由於在例項化BeanA的時候已經把BeanA的ObjectFactory新增到了 singletonFactories 快取中,那麼這裡就會走到 singletonFactory.getObject(); 方法得到BeanA的例項,並且會把BeanA儲存到 earlySingletonObjects早期單利Bean快取中。
BeanA的例項成功返回,那麼BeanB的 setter注入成功,代表BeanB例項化完成,那麼BeanA的setter方法注入成功,BeanA例項化完成。
prototype模式的迴圈依賴
對於prototype模式下的Bean不允許迴圈依賴,因為 這種模式下Bean是不做快取的,所以就沒法暴露ObjectFactory,也就沒辦法實現迴圈依賴。
總結
不知道你有沒有看暈,反正我但是在原始碼時的過程是比較辛苦的,這裡需要你對前面Bean的例項化流程和屬性注入流程比較熟悉,否則就會暈菜。
這裡總結一下:
構造器迴圈依賴是不允許的,主要通過 singletonsCurrentlyInCreation “正在建立Bean池” 把建立中的Bean快取起來,如果迴圈依賴,同一個Bean勢必會嘗試進入該快取2次,丟擲迴圈依賴異常。
setter迴圈依賴是可以允許的。Spring是通過提前暴露未例項化完成的Bean的 ObjectFactory來實現迴圈依賴的,這樣做的目的是其他的Bean可以通過 ObjectFactory 引用到該Bean 。在獲取依賴的Bean的時候使用到了三級快取。
下面的面試題你會答了嗎?
- Spirng支援那種模式下的迴圈依賴(構造器?,setter?, prototype?)
- Spring是如何處理構造器注入迴圈依賴的?
- Spring是如何處理Setter注入迴圈依賴的?
- 【開悟篇】Java多執行緒之JUC從入門到精通
- 想學設計模式、想搞架構設計,先學學UML系統建模吧您
- 被問麻了,Spring 如何處理迴圈依賴?
- java零基礎影片,Java架構師必備框架技能核心筆記
- 10年阿里開發架構師經驗分享,我在華為做Java外包的真實經歷
- 如何從Java工程師成長為架構師?
- 咕泡P6:Java網際網路高階架構師4期
- 矽谷碼農35歲危機:Java之父也找不到工作!程式設計師整容成風!
- 送你九年經驗,我的Java學習之路你也可以複製
- 好日子,宣佈一件事情!
- Java軟體架構師應該掌握的十個開源工具!!
- Java和C長期霸權結束?
- Python十年隱藏無人知,一朝影象識別天下聞
- 開玩笑,數學沒用論?程式設計師的需要掌握的數學知識
- 壓箱底 | Java 程式設計學習必讀的十本書
- 阿里限流神器Sentinel奪命連環 17 問?
- 雲原生時代,Java還有優勢麼?
- 中秋節無休,梳理了Java程式設計必讀的10本經典書
- 我摸索了一條架構師成長路線( 文末有驚喜)
- 我是有證的男人