文章已收錄在 GitHub JavaKeeper ,N 線互聯網開發、面試必備技能兵器譜,筆記自取。微信搜「 JavaKeeper 」程序員成長充電站,互聯網技術武道場。無套路領取 500+ 本電子書和 30+ 視頻教學和源碼。java
循環依賴問題,算是一道爛大街的面試題了,解毒以前,咱們先來回顧兩個知識點:git
初學 Spring 的時候,咱們就知道 IOC,控制反轉麼,它將本來在程序中手動建立對象的控制權,交由 Spring 框架來管理,不須要咱們手動去各類 new XXX
。程序員
儘管是 Spring 管理,不也得建立對象嗎, Java 對象的建立步驟不少,能夠 new XXX
、序列化、clone()
等等, 只是 Spring 是經過反射 + 工廠的方式建立對象並放在容器的,建立好的對象咱們通常還會對對象屬性進行賦值,纔去使用,能夠理解是分了兩個步驟。github
好了,對這兩個步驟有個印象就行,接着咱們進入循環依賴,先說下循環依賴的概念面試
所謂的循環依賴是指,A 依賴 B,B 又依賴 A,它們之間造成了循環依賴。或者是 A 依賴 B,B 依賴 C,C 又依賴 A,造成了循環依賴。更或者是本身依賴本身。它們之間的依賴關係以下:spring
這裏以兩個類直接相互依賴爲例,他們的實現代碼可能以下:緩存
public class BeanB { private BeanA beanA; public void setBeanA(BeanA beanA) { this.beanA = beanA; } } public class BeanA { private BeanB beanB; public void setBeanB(BeanB beanB) { this.beanB = beanB; } }
配置信息以下(用註解方式注入同理,只是爲了方便理解,用了配置文件):微信
<bean id="beanA" class="priv.starfish.BeanA"> <property name="beanB" ref="beanB"/> </bean> <bean id="beanB" class="priv.starfish.BeanB"> <property name="beanA" ref="beanA"/> </bean>
Spring 啓動後,讀取如上的配置文件,會按順序先實例化 A,可是建立的時候又發現它依賴了 B,接着就去實例化 B ,一樣又發現它依賴了 A ,這尼瑪咋整?無限循環呀架構
Spring 「確定」不會讓這種事情發生的,如前言咱們說的 Spring 實例化對象分兩步,第一步會先建立一個原始對象,只是沒有設置屬性,能夠理解爲"半成品"—— 官方叫 A 對象的早期引用(EarlyBeanReference),因此當實例化 B 的時候發現依賴了 A, B 就會把這個「半成品」設置進去先完成實例化,既然 B 完成了實例化,因此 A 就能夠得到 B 的引用,也完成實例化了,這其實就是 Spring 解決循環依賴的思想。app
不理解不要緊,先有個大概的印象,而後咱們從源碼來看下 Spring 具體是怎麼解決的。
代碼版本:5.0.16.RELEASE
在 Spring IOC 容器讀取 Bean 配置建立 Bean 實例以前, 必須對它進行實例化。只有在容器實例化後,才能夠從 IOC 容器裏獲取 Bean 實例並使用,循環依賴問題也就是發生在實例化 Bean 的過程當中的,因此咱們先回顧下獲取 Bean 的過程。
Spring IOC 容器中獲取 bean 實例的簡化版流程以下(排除了各類包裝和檢查的過程)
大概的流程順序(能夠結合着源碼看下,我就不貼了,貼太多的話,嘔~嘔嘔,想吐):
getBean
方法開始,getBean
是個空殼方法,全部邏輯直接到 doGetBean
方法中transformedBeanName
將 name 轉換爲真正的 beanName(name 多是 FactoryBean 以 & 字符開頭或者有別名的狀況,因此須要轉化下)getSingleton(beanName)
方法嘗試從緩存中查找是否是有該實例 sharedInstance(單例在 Spring 的同一容器只會被建立一次,後續再獲取 bean,就直接從緩存獲取便可)getObjectForBeanInstance
處理便可返回第三步的時候咱們提到了一個緩存的概念,這個就是 Spring 爲了解決單例的循環依賴問題而設計的 三級緩存
/** Cache of singleton objects: bean name --> bean instance */ private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256); /** Cache of singleton factories: bean name --> ObjectFactory */ private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16); /** Cache of early singleton objects: bean name --> bean instance */ private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);
這三級緩存的做用分別是:
singletonObjects
:完成初始化的單例對象的 cache,這裏的 bean 經歷過 實例化->屬性填充->初始化
以及各類後置處理(一級緩存)earlySingletonObjects
:存放原始的 bean 對象(完成實例化可是還沒有填充屬性和初始化),僅僅能做爲指針提早曝光,被其餘 bean 所引用,用於解決循環依賴的 (二級緩存)singletonFactories
:在 bean 實例化完以後,屬性填充以及初始化以前,若是容許提早曝光,Spring 會將實例化後的 bean 提早曝光,也就是把該 bean 轉換成 beanFactory
並加入到 singletonFactories
(三級緩存)咱們首先從緩存中試着獲取 bean,就是從這三級緩存中查找
protected Object getSingleton(String beanName, boolean allowEarlyReference) { // 從 singletonObjects 獲取實例,singletonObjects 中的實例都是準備好的 bean 實例,能夠直接使用 Object singletonObject = this.singletonObjects.get(beanName); //isSingletonCurrentlyInCreation() 判斷當前單例bean是否正在建立中 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) { // 三級緩存有的話,就把他移動到二級緩存,.getObject() 後續會講到 singletonObject = singletonFactory.getObject(); this.earlySingletonObjects.put(beanName, singletonObject); this.singletonFactories.remove(beanName); } } } } return singletonObject; }
若是緩存沒有的話,咱們就要建立了,接着咱們以單例對象爲例,再看下建立 bean 的邏輯(大括號表示內部類調用方法):
建立 bean 從如下代碼開始,一個匿名內部類方法參數(總以爲 Lambda 的方式可讀性不如內部類好理解)
if (mbd.isSingleton()) { sharedInstance = getSingleton(beanName, () -> { try { return createBean(beanName, mbd, args); } catch (BeansException ex) { destroySingleton(beanName); throw ex; } }); bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd); }
getSingleton()
方法內部主要有兩個方法
public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) { // 建立 singletonObject singletonObject = singletonFactory.getObject(); // 將 singletonObject 放入緩存 addSingleton(beanName, singletonObject); }
getObject()
匿名內部類的實現真正調用的又是 createBean(beanName, mbd, args)
doCreateBean
方法,先經過 createBeanInstance
建立一個原始 bean 對象addSingletonFactory
添加 bean 工廠對象到 singletonFactories 緩存(三級緩存)populateBean
方法向原始 bean 對象中填充屬性,並解析依賴,假設這時候建立 A 以後填充屬性時發現依賴 B,而後建立依賴對象 B 的時候又發現依賴 A,仍是一樣的流程,又去 getBean(A)
,這個時候三級緩存已經有了 beanA 的「半成品」,這時就能夠把 A 對象的原始引用注入 B 對象(並將其移動到二級緩存)來解決循環依賴問題。這時候 getObject()
方法就算執行結束了,返回徹底實例化的 beanaddSingleton
把徹底實例化好的 bean 對象放入 singletonObjects 緩存(一級緩存)中,打完收工建議搭配着「源碼」看下邊的邏輯圖,更好下飯
流程其實上邊都已經說過了,結合着上圖咱們再看下具體細節,用大白話再捋一捋:
這就是單例模式下 Spring 解決循環依賴的流程了。
可是這個地方,不論是誰看源碼都會有個小疑惑,爲何須要三級緩存呢,我趕腳二級他也夠了呀
革命還沒有成功,同志仍需努力
跟源碼的時候,發如今建立 beanB 須要引用 beanA 這個「半成品」的時候,就會觸發"前期引用",即以下代碼:
ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName); if (singletonFactory != null) { // 三級緩存有的話,就把他移動到二級緩存 singletonObject = singletonFactory.getObject(); this.earlySingletonObjects.put(beanName, singletonObject); this.singletonFactories.remove(beanName); }
singletonFactory.getObject()
是一個接口方法,這裏具體的實現方法在
protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) { Object exposedObject = bean; if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) { for (BeanPostProcessor bp : getBeanPostProcessors()) { if (bp instanceof SmartInstantiationAwareBeanPostProcessor) { SmartInstantiationAwareBeanPostProcessor ibp = (SmartInstantiationAwareBeanPostProcessor) bp; // 這麼一大段就這句話是核心,也就是當bean要進行提早曝光時, // 給一個機會,經過重寫後置處理器的getEarlyBeanReference方法,來自定義操做bean // 值得注意的是,若是提早曝光了,可是沒有被提早引用,則該後置處理器並不生效!!! // 這也正式三級緩存存在的意義,不然二級緩存就能夠解決循環依賴的問題 exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName); } } } return exposedObject; }
這個方法就是 Spring 爲何使用三級緩存,而不是二級緩存的緣由,它的目的是爲了後置處理,若是沒有 AOP 後置處理,就不會走進 if 語句,直接返回了 exposedObject ,至關於啥都沒幹,二級緩存就夠用了。
因此又得出結論,這個三級緩存應該和 AOP 有關係,繼續。
在 Spring 的源碼中getEarlyBeanReference
是 SmartInstantiationAwareBeanPostProcessor
接口的默認方法,真正實現這個方法的只有AbstractAutoProxyCreator
這個類,用於提早曝光的 AOP 代理。
@Override public Object getEarlyBeanReference(Object bean, String beanName) throws BeansException { Object cacheKey = getCacheKey(bean.getClass(), beanName); this.earlyProxyReferences.put(cacheKey, bean); // 對bean進行提早Spring AOP代理 return wrapIfNecessary(bean, beanName, cacheKey); }
這麼說有點幹,來個小 demo 吧,咱們都知道 Spring AOP、事務等都是經過代理對象來實現的,而事務的代理對象是由自動代理建立器來自動完成的。也就是說 Spring 最終給咱們放進容器裏面的是一個代理對象,而非原始對象,假設咱們有以下一段業務代碼:
@Service public class HelloServiceImpl implements HelloService { @Autowired private HelloService helloService; @Override @Transactional public Object hello() { return "Hello JavaKeeper"; } }
此 Service
類使用到了事務,因此最終會生成一個 JDK 動態代理對象 Proxy
。恰好它又存在本身引用本身的循環依賴,完美符合咱們的場景需求。
咱們再自定義一個後置處理,來看下效果:
@Component public class HelloProcessor implements SmartInstantiationAwareBeanPostProcessor { @Override public Object getEarlyBeanReference(Object bean, String beanName) throws BeansException { System.out.println("提早曝光了:"+beanName); return bean; } }
能夠看到,調用方法棧中有咱們本身實現的 HelloProcessor
,說明這個 bean 會經過 AOP 代理處理。
再從源碼看下這個本身循環本身的 bean 的建立流程:
protected Object doCreateBean( ... ){ ... boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences && isSingletonCurrentlyInCreation(beanName)); // 須要提早暴露(支持循環依賴),就註冊一個ObjectFactory到三級緩存 if (earlySingletonExposure) { // 添加 bean 工廠對象到 singletonFactories 緩存中,並獲取原始對象的早期引用 //匿名內部方法 getEarlyBeanReference 就是後置處理器 // SmartInstantiationAwareBeanPostProcessor 的一個方法, // 它的功效爲:保證本身被循環依賴的時候,即便被別的Bean @Autowire進去的也是代理對象 addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean)); } // 此處注意:若是此處本身被循環依賴了 那它會走上面的getEarlyBeanReference,從而建立一個代理對象從 三級緩存轉移到二級緩存裏 // 注意此時候對象還在二級緩存裏,並無在一級緩存。而且此時後續的這兩步操做仍是用的 exposedObject,它仍舊是原始對象~~~ populateBean(beanName, mbd, instanceWrapper); exposedObject = initializeBean(beanName, exposedObject, mbd); // 由於事務的AOP自動代理建立器在getEarlyBeanReference 建立代理後,initializeBean 就不會再重複建立了,二選一的) // 因此通過這兩大步後,exposedObject 仍是原始對象,經過 getEarlyBeanReference 建立的代理對象還在三級緩存呢 ... // 循環依賴校驗 if (earlySingletonExposure) { // 注意此處第二個參數傳的false,表示不去三級緩存裏再去調用一次getObject()方法了~~~,此時代理對象還在二級緩存,因此這裏拿出來的就是個 代理對象 // 最後賦值給exposedObject 而後return出去,進而最終被addSingleton()添加進一級緩存裏面去 // 這樣就保證了咱們容器裏 最終其實是代理對象,而非原始對象~~~~~ Object earlySingletonReference = getSingleton(beanName, false); if (earlySingletonReference != null) { if (exposedObject == bean) { exposedObject = earlySingletonReference; } } ... } }
咱們再來看下相關代碼,假設咱們如今是二級緩存架構,建立 A 的時候,咱們不知道有沒有循環依賴,因此放入二級緩存提早暴露,接着建立 B,也是放入二級緩存,這時候發現又循環依賴了 A,就去二級緩存找,是有,可是若是此時還有 AOP 代理呢,咱們要的是代理對象可不是原始對象,這怎麼辦,只能改邏輯,在第一步的時候,無論3721,全部 Bean 通通去完成 AOP 代理,若是是這樣的話,就不須要三級緩存了,可是這樣不只沒有必要,並且違背了 Spring 在結合 AOP
跟 Bean 的生命週期的設計。
因此 Spring 「畫蛇添足」的將實例先封裝到 ObjectFactory 中(三級緩存),主要關鍵點在 getObject()
方法並不是直接返回實例,而是對實例又使用 SmartInstantiationAwareBeanPostProcessor
的 getEarlyBeanReference
方法對 bean 進行處理,也就是說,當 Spring 中存在該後置處理器,全部的單例 bean 在實例化後都會被進行提早曝光到三級緩存中,可是並非全部的 bean 都存在循環依賴,也就是三級緩存到二級緩存的步驟不必定都會被執行,有可能曝光後直接建立完成,沒被提早引用過,就直接被加入到一級緩存中。所以能夠確保只有提早曝光且被引用的 bean 纔會進行該後置處理。
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) { // 三級緩存獲取,key=beanName value=objectFactory,objectFactory中存儲 //getObject()方法用於獲取提早曝光的實例 ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName); if (singletonFactory != null) { // 三級緩存有的話,就把他移動到二級緩存 singletonObject = singletonFactory.getObject(); this.earlySingletonObjects.put(beanName, singletonObject); this.singletonFactories.remove(beanName); } } } } return singletonObject; } 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"); } // 添加 bean 工廠對象到 singletonFactories 緩存中,並獲取原始對象的早期引用 //匿名內部方法 getEarlyBeanReference 就是後置處理器 // SmartInstantiationAwareBeanPostProcessor 的一個方法, // 它的功效爲:保證本身被循環依賴的時候,即便被別的Bean @Autowire進去的也是代理對象~~~~ AOP自動代理建立器此方法裏會建立的代理對象~~~ addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean)); }
這個又涉及到了 Spring 中動態代理的實現,不論是cglib
代理仍是jdk
動態代理生成的代理類,代理時,會將目標對象 target 保存在最後生成的代理 $proxy
中,當調用 $proxy
方法時會回調 h.invoke
,而 h.invoke
又會回調目標對象 target 的原始方法。全部,其實在 AOP 動態代理時,原始 bean 已經被保存在 提早曝光代理中了,以後 原始 bean
繼續完成屬性填充
和初始化
操做。由於 AOP 代理$proxy
中保存着 traget
也就是是 原始bean
的引用,所以後續 原始bean
的完善,也就至關於Spring AOP中的 target
的完善,這樣就保證了 AOP 的屬性填充
與初始化
了!
看完了單例模式的循環依賴,咱們再看下非單例的狀況,假設咱們的配置文件是這樣的:
<bean id="beanA" class="priv.starfish.BeanA" scope="prototype"> <property name="beanB" ref="beanB"/> </bean> <bean id="beanB" class="priv.starfish.BeanB" scope="prototype"> <property name="beanA" ref="beanA"/> </bean>
啓動 Spring,結果以下:
Error creating bean with name 'beanA' defined in class path resource [applicationContext.xml]: Cannot resolve reference to bean 'beanB' while setting bean property 'beanB'; Error creating bean with name 'beanB' defined in class path resource [applicationContext.xml]: Cannot resolve reference to bean 'beanA' while setting bean property 'beanA'; Caused by: org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'beanA': Requested bean is currently in creation: Is there an unresolvable circular reference?
對於 prototype
做用域的 bean,Spring 容器沒法完成依賴注入,由於 Spring 容器不進行緩存 prototype
做用域的 bean ,所以沒法提早暴露一個建立中的bean 。
緣由也挺好理解的,原型模式每次請求都會建立一個實例對象,即便加了緩存,循環引用太多的話,就比較麻煩了就,因此 Spring 不支持這種方式,直接拋出異常:
if (isPrototypeCurrentlyInCreation(beanName)) { throw new BeanCurrentlyInCreationException(beanName); }
上文咱們講的是經過 Setter 方法注入的單例 bean 的循環依賴問題,用 Spring 的小夥伴也都知道,依賴注入的方式還有構造器注入、工廠方法注入的方式(不多使用),那若是構造器注入方式也有循環依賴,能夠搞不?
咱們再改下代碼和配置文件
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; } }
<bean id="beanA" class="priv.starfish.BeanA"> <constructor-arg ref="beanB"/> </bean> <bean id="beanB" class="priv.starfish.BeanB"> <constructor-arg ref="beanA"/> </bean>
執行結果,又是異常
看看官方給出的說法
Circular dependenciesIf you use predominantly constructor injection, it is possible to create an unresolvable circular dependency scenario.
For example: Class A requires an instance of class B through constructor injection, and class B requires an instance of class A through constructor injection. If you configure beans for classes A and B to be injected into each other, the Spring IoC container detects this circular reference at runtime, and throws a
BeanCurrentlyInCreationException
.One possible solution is to edit the source code of some classes to be configured by setters rather than constructors. Alternatively, avoid constructor injection and use setter injection only. In other words, although it is not recommended, you can configure circular dependencies with setter injection.
Unlike the typical case (with no circular dependencies), a circular dependency between bean A and bean B forces one of the beans to be injected into the other prior to being fully initialized itself (a classic chicken-and-egg scenario).
大概意思是:
若是您主要使用構造器注入,循環依賴場景是沒法解決的。建議你用 setter 注入方式代替構造器注入
其實也不是說只要是構造器注入就會有循環依賴問題,Spring 在建立 Bean 的時候默認是按照天然排序來進行建立的,咱們暫且把先建立的 bean 叫主 bean,上文的 A 即主 bean,只要主 bean 注入依賴 bean 的方式是 setter 方式,依賴 bean 的注入方式無所謂,均可以解決,反之亦然
因此上文咱們 AB 循環依賴問題,只要 A 的注入方式是 setter ,就不會有循環依賴問題。
面試官問:爲何呢?
Spring 解決循環依賴依靠的是 Bean 的「中間態」這個概念,而這個中間態指的是已經實例化,但還沒初始化的狀態。實例化的過程又是經過構造器建立的,若是 A 還沒建立好出來,怎麼可能提早曝光,因此構造器的循環依賴沒法解決,我一直認爲應該先有雞纔能有蛋。
雖然在建立 B 時會提早給 B 注入了一個還未初始化的 A 對象,可是在建立 A 的流程中一直使用的是注入到 B 中的 A 對象的引用,以後會根據這個引用對 A 進行初始化,因此這是沒有問題的。
Spring 爲了解決單例的循環依賴問題,使用了三級緩存。其中一級緩存爲單例池(singletonObjects
),二級緩存爲提早曝光對象(earlySingletonObjects
),三級緩存爲提早曝光對象工廠(singletonFactories
)。
假設A、B循環引用,實例化 A 的時候就將其放入三級緩存中,接着填充屬性的時候,發現依賴了 B,一樣的流程也是實例化後放入三級緩存,接着去填充屬性時又發現本身依賴 A,這時候從緩存中查找到早期暴露的 A,沒有 AOP 代理的話,直接將 A 的原始對象注入 B,完成 B 的初始化後,進行屬性填充和初始化,這時候 B 完成後,就去完成剩下的 A 的步驟,若是有 AOP 代理,就進行 AOP 處理獲取代理後的對象 A,注入 B,走剩下的流程。
若是沒有 AOP 代理,二級緩存能夠解決問題,可是有 AOP 代理的狀況下,只用二級緩存就意味着全部 Bean 在實例化後就要完成 AOP 代理,這樣違背了 Spring 設計的原則,Spring 在設計之初就是經過 AnnotationAwareAspectJAutoProxyCreator
這個後置處理器來在 Bean 生命週期的最後一步來完成 AOP 代理,而不是在實例化後就立馬進行 AOP 代理。
https://developer.aliyun.com/...
http://www.tianxiaobo.com/201...