面試必殺技,講一講Spring中的循環依賴


本系列文章:html


據說你還沒學Spring就被源碼編譯勸退了?30+張圖帶你玩轉Spring編譯
程序員

讀源碼,咱們能夠從第一行讀起
web

你知道Spring是怎麼解析配置類的嗎?
面試

配置類爲何要添加@Configuration註解?
spring

談談Spring中的對象跟Bean,你知道Spring怎麼建立對象的嗎?
緩存

這篇文章,咱們來談一談Spring中的屬性注入
微信

Spring中AOP相關的API及源碼解析,原來AOP是這樣子的
編輯器

你知道Spring是怎麼將AOP應用到Bean的生命週期中的嗎?
ide

推薦閱讀:源碼分析


Spring官網閱讀 | 總結篇


Spring雜談


本系列文章將會帶你一行行的將Spring的源碼吃透,推薦閱讀的文章是閱讀源碼的基礎!

前言

Spring中的循環依賴一直是Spring中一個很重要的話題,一方面是由於源碼中爲了解決循環依賴作了不少處理,另一方面是由於面試的時候,若是問到Spring中比較高階的問題,那麼循環依賴一定逃不掉。若是你回答得好,那麼這就是你的必殺技,反正,那就是面試官的必殺技,這也是取這個標題的緣由,固然,本文的目的是爲了讓你在以後的全部面試中能多一個必殺技,專門用來絕殺面試官!

本文的核心思想就是,

當面試官問:

「請講一講Spring中的循環依賴。」的時候,

咱們到底該怎麼回答?

主要分下面幾點

  1. 什麼是循環依賴?
  2. 什麼狀況下循環依賴能夠被處理?
  3. Spring是如何解決的循環依賴?

同時本文但願糾正幾個目前業界內常常出現的幾個關於循環依賴的錯誤的說法

  1. 只有在setter方式注入的狀況下,循環依賴才能解決(
  2. 三級緩存的目的是爲了提升效率(

OK,鋪墊已經作完了,接下來咱們開始正文

什麼是循環依賴?

從字面上來理解就是A依賴B的同時B也依賴了A,就像下面這樣

image-20200705175322521
image-20200705175322521

體現到代碼層次就是這個樣子

@Component
public class A {  // A中注入了B  @Autowired  private B b; }  @Component public class B {  // B中也注入了A  @Autowired  private A a; } 複製代碼

固然,這是最多見的一種循環依賴,比較特殊的還有

// 本身依賴本身
@Component public class A {  // A中注入了A  @Autowired  private A a; } 複製代碼

雖然體現形式不同,可是實際上都是同一個問題----->循環依賴

什麼狀況下循環依賴能夠被處理?

在回答這個問題以前首先要明確一點,Spring解決循環依賴是有前置條件的

  1. 出現循環依賴的Bean必需要是單例
  2. 依賴注入的方式不能全是構造器注入的方式(不少博客上說,只能解決setter方法的循環依賴,這是錯誤的)

其中第一點應該很好理解,第二點:不能全是構造器注入是什麼意思呢?咱們仍是用代碼說話

@Component
public class A { // @Autowired // private B b;  public A(B b) {   } }   @Component public class B {  // @Autowired // private A a;   public B(A a){   } } 複製代碼

在上面的例子中,A中注入B的方式是經過構造器,B中注入A的方式也是經過構造器,這個時候循環依賴是沒法被解決,若是你的項目中有兩個這樣相互依賴的Bean,在啓動時就會報出如下錯誤:

Caused by: org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'a': Requested bean is currently in creation: Is there an unresolvable circular reference?
複製代碼

爲了測試循環依賴的解決狀況跟注入方式的關係,咱們作以下四種狀況的測試

依賴狀況 依賴注入方式 循環依賴是否被解決
AB相互依賴(循環依賴) 均採用setter方法注入
AB相互依賴(循環依賴) 均採用構造器注入
AB相互依賴(循環依賴) A中注入B的方式爲setter方法,B中注入A的方式爲構造器
AB相互依賴(循環依賴) B中注入A的方式爲setter方法,A中注入B的方式爲構造器

具體的測試代碼跟簡單,我就不放了。從上面的測試結果咱們能夠看到,不是隻有在setter方法注入的狀況下循環依賴才能被解決,即便存在構造器注入的場景下,循環依賴依然被能夠被正常處理掉。

那麼究竟是爲何呢?Spring究竟是怎麼處理的循環依賴呢?不要急,咱們接着往下看

Spring是如何解決的循環依賴?

關於循環依賴的解決方式應該要分兩種狀況來討論

  1. 簡單的循環依賴(沒有AOP)
  2. 結合了AOP的循環依賴

簡單的循環依賴(沒有AOP)

咱們先來分析一個最簡單的例子,就是上面提到的那個demo

@Component
public class A {  // A中注入了B  @Autowired  private B b; }  @Component public class B {  // B中也注入了A  @Autowired  private A a; } 複製代碼

經過上文咱們已經知道了這種狀況下的循環依賴是可以被解決的,那麼具體的流程是什麼呢?咱們一步步分析

首先,咱們要知道Spring在建立Bean的時候默認是按照天然排序來進行建立的,因此第一步Spring會去建立A

與此同時,咱們應該知道,Spring在建立Bean的過程當中分爲三步

  1. 實例化,對應方法:AbstractAutowireCapableBeanFactory中的createBeanInstance方法

  2. 屬性注入,對應方法:AbstractAutowireCapableBeanFactorypopulateBean方法

  3. 初始化,對應方法:AbstractAutowireCapableBeanFactoryinitializeBean

這些方法在以前源碼分析的文章中都作過詳細的解讀了,若是你以前沒看過個人文章,那麼你只須要知道

  1. 實例化,簡單理解就是new了一個對象
  2. 屬性注入,爲實例化中new出來的對象填充屬性
  3. 初始化,執行aware接口中的方法,初始化方法,完成 AOP代理

基於上面的知識,咱們開始解讀整個循環依賴處理的過程,整個流程應該是以A的建立爲起點,前文也說了,第一步就是建立A嘛!

image-20200706092738559
image-20200706092738559

建立A的過程實際上就是調用getBean方法,這個方法有兩層含義

  1. 建立一個新的Bean
  2. 從緩存中獲取到已經被建立的對象

咱們如今分析的是第一層含義,由於這個時候緩存中尚未A嘛!

調用getSingleton(beanName)

首先調用getSingleton(a)方法,這個方法又會調用getSingleton(beanName, true),在上圖中我省略了這一步

public Object getSingleton(String beanName) {
 return getSingleton(beanName, true); } 複製代碼

getSingleton(beanName, true)這個方法實際上就是到緩存中嘗試去獲取Bean,整個緩存分爲三級

  1. singletonObjects,一級緩存,存儲的是全部建立好了的單例Bean
  2. earlySingletonObjects,完成實例化,可是還未進行屬性注入及初始化的對象
  3. singletonFactories,提早暴露的一個單例工廠,二級緩存中存儲的就是從這個工廠中獲取到的對象

由於A是第一次被建立,因此無論哪一個緩存中必然都是沒有的,所以會進入getSingleton的另一個重載方法getSingleton(beanName, singletonFactory)

調用getSingleton(beanName, singletonFactory)

這個方法就是用來建立Bean的,其源碼以下:

public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
 Assert.notNull(beanName, "Bean name must not be null");  synchronized (this.singletonObjects) {  Object singletonObject = this.singletonObjects.get(beanName);  if (singletonObject == null) {   // ....  // 省略異常處理及日誌  // ....   // 在單例對象建立前先作一個標記  // 將beanName放入到singletonsCurrentlyInCreation這個集合中  // 標誌着這個單例Bean正在建立  // 若是同一個單例Bean屢次被建立,這裏會拋出異常  beforeSingletonCreation(beanName);  boolean newSingleton = false;  boolean recordSuppressedExceptions = (this.suppressedExceptions == null);  if (recordSuppressedExceptions) {  this.suppressedExceptions = new LinkedHashSet<>();  }  try {  // 上游傳入的lambda在這裏會被執行,調用createBean方法建立一個Bean後返回  singletonObject = singletonFactory.getObject();  newSingleton = true;  }  // ...  // 省略catch異常處理  // ...  finally {  if (recordSuppressedExceptions) {  this.suppressedExceptions = null;  }  // 建立完成後將對應的beanName從singletonsCurrentlyInCreation移除  afterSingletonCreation(beanName);  }  if (newSingleton) {  // 添加到一級緩存singletonObjects中  addSingleton(beanName, singletonObject);  }  }  return singletonObject;  } } 複製代碼

上面的代碼咱們主要抓住一點,經過createBean方法返回的Bean最終被放到了一級緩存,也就是單例池中。

那麼到這裏咱們能夠得出一個結論:一級緩存中存儲的是已經徹底建立好了的單例Bean

調用addSingletonFactory方法

以下圖所示:

image-20200706105535307
image-20200706105535307

在完成Bean的實例化後,屬性注入以前Spring將Bean包裝成一個工廠添加進了三級緩存中,對應源碼以下:

// 這裏傳入的參數也是一個lambda表達式,() -> getEarlyBeanReference(beanName, mbd, bean)
protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {  Assert.notNull(singletonFactory, "Singleton factory must not be null");  synchronized (this.singletonObjects) {  if (!this.singletonObjects.containsKey(beanName)) {  // 添加到三級緩存中  this.singletonFactories.put(beanName, singletonFactory);  this.earlySingletonObjects.remove(beanName);  this.registeredSingletons.add(beanName);  }  } } 複製代碼

這裏只是添加了一個工廠,經過這個工廠(ObjectFactory)的getObject方法能夠獲得一個對象,而這個對象實際上就是經過getEarlyBeanReference這個方法建立的。那麼,何時會去調用這個工廠的getObject方法呢?這個時候就要到建立B的流程了。

當A完成了實例化並添加進了三級緩存後,就要開始爲A進行屬性注入了,在注入時發現A依賴了B,那麼這個時候Spring又會去getBean(b),而後反射調用setter方法完成屬性注入。

image-20200706114501300
image-20200706114501300

由於B須要注入A,因此在建立B的時候,又會去調用getBean(a),這個時候就又回到以前的流程了,可是不一樣的是,以前的getBean是爲了建立Bean,而此時再調用getBean不是爲了建立了,而是要從緩存中獲取,由於以前A在實例化後已經將其放入了三級緩存singletonFactories中,因此此時getBean(a)的流程就是這樣子了

image-20200706115959250
image-20200706115959250

從這裏咱們能夠看出,注入到B中的A是經過getEarlyBeanReference方法提早暴露出去的一個對象,還不是一個完整的Bean,那麼getEarlyBeanReference到底幹了啥了,咱們看下它的源碼

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;  exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName);  }  }  }  return exposedObject; } 複製代碼

它實際上就是調用了後置處理器的getEarlyBeanReference,而真正實現了這個方法的後置處理器只有一個,就是經過@EnableAspectJAutoProxy註解導入的AnnotationAwareAspectJAutoProxyCreator也就是說若是在不考慮AOP的狀況下,上面的代碼等價於:

protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
 Object exposedObject = bean;  return exposedObject; } 複製代碼

也就是說這個工廠啥都沒幹,直接將實例化階段建立的對象返回了!因此說在不考慮AOP的狀況下三級緩存有用嘛?講道理,真的沒什麼用,我直接將這個對象放到二級緩存中不是一點問題都沒有嗎?若是你說它提升了效率,那你告訴我提升的效率在哪?

image-20200706124118108

那麼三級緩存到底有什麼做用呢?不要急,咱們先把整個流程走完,在下文結合AOP分析循環依賴的時候你就能體會到三級緩存的做用!

到這裏不知道小夥伴們會不會有疑問,B中提早注入了一個沒有通過初始化的A類型對象不會有問題嗎?

答:不會

這個時候咱們須要將整個建立A這個Bean的流程走完,以下圖:

image-20200706133018669
image-20200706133018669

從上圖中咱們能夠看到,雖然在建立B時會提早給B注入了一個還未初始化的A對象,可是在建立A的流程中一直使用的是注入到B中的A對象的引用,以後會根據這個引用對A進行初始化,因此這是沒有問題的。

結合了AOP的循環依賴

以前咱們已經說過了,在普通的循環依賴的狀況下,三級緩存沒有任何做用。三級緩存實際上跟Spring中的AOP相關,咱們再來看一看getEarlyBeanReference的代碼:

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;  exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName);  }  }  }  return exposedObject; } 複製代碼

若是在開啓AOP的狀況下,那麼就是調用到AnnotationAwareAspectJAutoProxyCreatorgetEarlyBeanReference方法,對應的源碼以下:

public Object getEarlyBeanReference(Object bean, String beanName) {
 Object cacheKey = getCacheKey(bean.getClass(), beanName);  this.earlyProxyReferences.put(cacheKey, bean);  // 若是須要代理,返回一個代理對象,不須要代理,直接返回當前傳入的這個bean對象  return wrapIfNecessary(bean, beanName, cacheKey); } 複製代碼

回到上面的例子,咱們對A進行了AOP代理的話,那麼此時getEarlyBeanReference將返回一個代理後的對象,而不是實例化階段建立的對象,這樣就意味着B中注入的A將是一個代理對象而不是A的實例化階段建立後的對象。image-20200706161709829

看到這個圖你可能會產生下面這些疑問

  1. 在給B注入的時候爲何要注入一個代理對象?

答:當咱們對A進行了AOP代理時,說明咱們但願從容器中獲取到的就是A代理後的對象而不是A自己,所以把A看成依賴進行注入時也要注入它的代理對象

  1. 明明初始化的時候是A對象,那麼Spring是在哪裏將代理對象放入到容器中的呢?
image-20200706160542584
image-20200706160542584

在完成初始化後,Spring又調用了一次getSingleton方法,這一次傳入的參數又不同了,false能夠理解爲禁用三級緩存,前面圖中已經提到過了,在爲B中注入A時已經將三級緩存中的工廠取出,並從工廠中獲取到了一個對象放入到了二級緩存中,因此這裏的這個getSingleton方法作的時間就是從二級緩存中獲取到這個代理後的A對象。exposedObject == bean能夠認爲是一定成立的,除非你非要在初始化階段的後置處理器中替換掉正常流程中的Bean,例如增長一個後置處理器:

@Component
public class MyPostProcessor implements BeanPostProcessor {  @Override  public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {  if (beanName.equals("a")) {  return new A();  }  return bean;  } } 複製代碼

不過,請不要作這種騷操做,徒增煩惱!

  1. 初始化的時候是對A對象自己進行初始化,而容器中以及注入到B中的都是代理對象,這樣不會有問題嗎?

答:不會,這是由於不論是cglib代理仍是jdk動態代理生成的代理類,內部都持有一個目標類的引用,當調用代理對象的方法時,實際會去調用目標對象的方法,A完成初始化至關於代理對象自身也完成了初始化

  1. 三級緩存爲何要使用工廠而不是直接使用引用?換而言之,爲何須要這個三級緩存,直接經過二級緩存暴露一個引用不行嗎?

答:這個工廠的目的在於延遲對實例化階段生成的對象的代理,只有真正發生循環依賴的時候,纔去提早生成代理對象,不然只會建立一個工廠並將其放入到三級緩存中,可是不會去經過這個工廠去真正建立對象

咱們思考一種簡單的狀況,就以單首創建A爲例,假設AB之間如今沒有依賴關係,可是A被代理了,這個時候當A完成實例化後仍是會進入下面這段代碼:

// A是單例的,mbd.isSingleton()條件知足
// allowCircularReferences:這個變量表明是否容許循環依賴,默認是開啓的,條件也知足 // isSingletonCurrentlyInCreation:正在在建立A,也知足 // 因此earlySingletonExposure=true boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&  isSingletonCurrentlyInCreation(beanName)); // 仍是會進入到這段代碼中 if (earlySingletonExposure) {  // 仍是會經過三級緩存提早暴露一個工廠對象  addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean)); } 複製代碼

看到了吧,即便沒有循環依賴,也會將其添加到三級緩存中,並且是不得不添加到三級緩存中,由於到目前爲止Spring也不能肯定這個Bean有沒有跟別的Bean出現循環依賴。

假設咱們在這裏直接使用二級緩存的話,那麼意味着全部的Bean在這一步都要完成AOP代理。這樣作有必要嗎?

不只沒有必要,並且違背了Spring在結合AOP跟Bean的生命週期的設計!Spring結合AOP跟Bean的生命週期自己就是經過AnnotationAwareAspectJAutoProxyCreator這個後置處理器來完成的,在這個後置處理的postProcessAfterInitialization方法中對初始化後的Bean完成AOP代理。若是出現了循環依賴,那沒有辦法,只有給Bean先建立代理,可是沒有出現循環依賴的狀況下,設計之初就是讓Bean在生命週期的最後一步完成代理而不是在實例化後就立馬完成代理。

三級緩存真的提升了效率了嗎?

如今咱們已經知道了三級緩存的真正做用,可是這個答案可能還沒法說服你,因此咱們再最後總結分析一波,三級緩存真的提升了效率了嗎?分爲兩點討論:

  1. 沒有進行 AOP的Bean間的循環依賴

從上文分析能夠看出,這種狀況下三級緩存根本沒用!因此不會存在什麼提升了效率的說法

  1. 進行了 AOP的Bean間的循環依賴

就以咱們上的A、B爲例,其中A被AOP代理,咱們先分析下使用了三級緩存的狀況下,A、B的建立流程

image-20200706171514327
image-20200706171514327

假設不使用三級緩存,直接在二級緩存中

image-20200706172523258
image-20200706172523258

上面兩個流程的惟一區別在於爲A對象建立代理的時機不一樣,在使用了三級緩存的狀況下爲A建立代理的時機是在B中須要注入A的時候,而不使用三級緩存的話在A實例化後就須要立刻爲A建立代理而後放入到二級緩存中去。對於整個A、B的建立過程而言,消耗的時間是同樣的

綜上,不論是哪一種狀況,三級緩存提升了效率這種說法都是錯誤的!

總結

面試官:」Spring是如何解決的循環依賴?「

答:Spring經過三級緩存解決了循環依賴,其中一級緩存爲單例池(singletonObjects),二級緩存爲早期曝光對象earlySingletonObjects,三級緩存爲早期曝光對象工廠(singletonFactories)。當A、B兩個類發生循環引用時,在A完成實例化後,就使用實例化後的對象去建立一個對象工廠,並添加到三級緩存中,若是A被AOP代理,那麼經過這個工廠獲取到的就是A代理後的對象,若是A沒有被AOP代理,那麼這個工廠獲取到的就是A實例化的對象。當A進行屬性注入時,會去建立B,同時B又依賴了A,因此建立B的同時又會去調用getBean(a)來獲取須要的依賴,此時的getBean(a)會從緩存中獲取,第一步,先獲取到三級緩存中的工廠;第二步,調用對象工工廠的getObject方法來獲取到對應的對象,獲得這個對象後將其注入到B中。緊接着B會走完它的生命週期流程,包括初始化、後置處理器等。當B建立完後,會將B再注入到A中,此時A再完成它的整個生命週期。至此,循環依賴結束!

面試官:」爲何要使用三級緩存呢?二級緩存能解決循環依賴嗎?「

答:若是要使用二級緩存解決循環依賴,意味着全部Bean在實例化後就要完成AOP代理,這樣違背了Spring設計的原則,Spring在設計之初就是經過AnnotationAwareAspectJAutoProxyCreator這個後置處理器來在Bean生命週期的最後一步來完成AOP代理,而不是在實例化後就立馬進行AOP代理。

一道思考題

爲何在下表中的第三種狀況的循環依賴能被解決,而第四種狀況不能被解決呢?

提示:Spring在建立Bean時默認會根據天然排序進行建立,因此A會先於B進行建立

依賴狀況 依賴注入方式 循環依賴是否被解決
AB相互依賴(循環依賴) 均採用setter方法注入
AB相互依賴(循環依賴) 均採用構造器注入
AB相互依賴(循環依賴) A中注入B的方式爲setter方法,B中注入A的方式爲構造器
AB相互依賴(循環依賴) B中注入A的方式爲setter方法,A中注入B的方式爲構造器

若是本文對你由幫助的話,記得點個贊吧!也歡迎關注個人公衆號,微信搜索:程序員DMZ,或者掃描下方二維碼,跟着我一塊兒認認真真學Java,踏踏實實作一個coder。

公衆號

我叫DMZ,一個在學習路上匍匐前行的小菜鳥! 碼字不易,本文要是對你有幫助的話,記得點個贊吧!

相關文章
相關標籤/搜索