Spring 如何解決循環依賴?

在關于Spring的面試中,我們經常會被問到一個問題:Spring是如何解決循環依賴的問題的。

這個問題算是關于Spring的一個高頻面試題,因為如果不刻意研讀,相信即使讀過源碼,面試者也不一定能夠一下子思考出個中奧秘。

本文主要針對這個問題,從源碼的角度對其實現原理進行講解。

1. 過程演示

關于Spring bean的創建,其本質上還是一個對象的創建,既然是對象,讀者朋友一定要明白一點就是,一個完整的對象包含兩部分:當前對象實例化和對象屬性的實例化。

在Spring中,對象的實例化是通過反射實現的,而對象的屬性則是在對象實例化之后通過一定的方式設置的。

這個過程可以按照如下方式進行理解:

image

理解這一個點之后,對于循環依賴的理解就已經幫助一大步了,我們這里以兩個類A和B為例進行講解,如下是A和B的聲明:

@Component
public class A {
  private B b;
  public void setB(B b) {
    this.b = b;
  }
}
@Component
public class B {
  private A a;
  public void setA(A a) {
    this.a = a;
  }
}

可以看到,這里A和B中各自都以對方為自己的全局屬性。這里首先需要說明的一點,Spring實例化bean是通過ApplicationContext.getBean()方法來進行的。

如果要獲取的對象依賴了另一個對象,那么其首先會創建當前對象,然后通過遞歸的調用ApplicationContext.getBean()方法來獲取所依賴的對象,最后將獲取到的對象注入到當前對象中。

這里我們以上面的首先初始化A對象實例為例進行講解。

首先Spring嘗試通過ApplicationContext.getBean()方法獲取A對象的實例,由于Spring容器中還沒有A對象實例,因而其會創建一個A對象

然后發現其依賴了B對象,因而會嘗試遞歸的通過ApplicationContext.getBean()方法獲取B對象的實例

但是Spring容器中此時也沒有B對象的實例,因而其還是會先創建一個B對象的實例。

讀者需要注意這個時間點,此時A對象和B對象都已經創建了,并且保存在Spring容器中了,只不過A對象的屬性b和B對象的屬性a都還沒有設置進去。

在前面Spring創建B對象之后,Spring發現B對象依賴了屬性A,因而還是會嘗試遞歸的調用ApplicationContext.getBean()方法獲取A對象的實例

因為Spring中已經有一個A對象的實例,雖然只是半成品(其屬性b還未初始化),但其也還是目標bean,因而會將該A對象的實例返回。

此時,B對象的屬性a就設置進去了,然后還是ApplicationContext.getBean()方法遞歸的返回,也就是將B對象的實例返回,此時就會將該實例設置到A對象的屬性b中。

這個時候,注意A對象的屬性b和B對象的屬性a都已經設置了目標對象的實例了

讀者朋友可能會比較疑惑的是,前面在為對象B設置屬性a的時候,這個A類型屬性還是個半成品。但是需要注意的是,這個A是一個引用,其本質上還是最開始就實例化的A對象。

而在上面這個遞歸過程的最后,Spring將獲取到的B對象實例設置到了A對象的屬性b中了

這里的A對象其實和前面設置到實例B中的半成品A對象是同一個對象,其引用地址是同一個,這里為A對象的b屬性設置了值,其實也就是為那個半成品的a屬性設置了值。

下面我們通過一個流程圖來對這個過程進行講解:

image

圖中getBean()表示調用Spring的ApplicationContext.getBean()方法,而該方法中的參數,則表示我們要嘗試獲取的目標對象。

圖中的黑色箭頭表示一開始的方法調用走向,走到最后,返回了Spring中緩存的A對象之后,表示遞歸調用返回了,此時使用綠色的箭頭表示。

從圖中我們可以很清楚的看到,B對象的a屬性是在第三步中注入的半成品A對象,而A對象的b屬性是在第二步中注入的成品B對象,此時半成品的A對象也就變成了成品的A對象,因為其屬性已經設置完成了。

2. 源碼講解

對于Spring處理循環依賴問題的方式,我們這里通過上面的流程圖其實很容易就可以理解

需要注意的一個點,Spring是如何標記開始生成的A對象是一個半成品,并且是如何保存A對象的。

這里的標記工作Spring是使用ApplicationContext的屬性SetsingletonsCurrentlyInCreation來保存的,而半成品的A對象則是通過MapsingletonFactories來保存的

這里的ObjectFactory是一個工廠對象,可通過調用其getObject()方法來獲取目標對象。在AbstractBeanFactory.doGetBean()方法中獲取對象的方法如下:

protected  T doGetBean(final String name, @Nullable final Class requiredType,
    @Nullable final Object[] args, boolean typeCheckOnly) throws BeansException {

  // 嘗試通過bean名稱獲取目標bean對象,比如這里的A對象
  Object sharedInstance = getSingleton(beanName);
  // 我們這里的目標對象都是單例的
  if (mbd.isSingleton()) {

    // 這里就嘗試創建目標對象,第二個參數傳的就是一個ObjectFactory類型的對象,這里是使用Java8的lamada
    // 表達式書寫的,只要上面的getSingleton()方法返回值為空,則會調用這里的getSingleton()方法來創建
    // 目標對象
    sharedInstance = getSingleton(beanName, () -> {
      try {
        // 嘗試創建目標對象
        return createBean(beanName, mbd, args);
      } catch (BeansException ex) {
        throw ex;
      }
    });
  }
  return (T) bean;
}

這里的doGetBean()方法是非常關鍵的一個方法(中間省略了其他代碼),上面也主要有兩個步驟

第一個步驟的getSingleton()方法的作用是嘗試從緩存中獲取目標對象,如果沒有獲取到,則嘗試獲取半成品的目標對象;如果第一個步驟沒有獲取到目標對象的實例,那么就進入第二個步驟

第二個步驟的getSingleton()方法的作用是嘗試創建目標對象,并且為該對象注入其所依賴的屬性。

這里其實就是主干邏輯,我們前面圖中已經標明,在整個過程中會調用三次doGetBean()方法

第一次調用的時候會嘗試獲取A對象實例,此時走的是第一個getSingleton()方法,由于沒有已經創建的A對象的成品或半成品,因而這里得到的是null

然后就會調用第二個getSingleton()方法,創建A對象的實例,然后遞歸的調用doGetBean()方法,嘗試獲取B對象的實例以注入到A對象中

此時由于Spring容器中也沒有B對象的成品或半成品,因而還是會走到第二個getSingleton()方法,在該方法中創建B對象的實例

創建完成之后,嘗試獲取其所依賴的A的實例作為其屬性,因而還是會遞歸的調用doGetBean()方法

此時需要注意的是,在前面由于已經有了一個半成品的A對象的實例,因而這個時候,再嘗試獲取A對象的實例的時候,會走第一個getSingleton()方法

在該方法中會得到一個半成品的A對象的實例,然后將該實例返回,并且將其注入到B對象的屬性a中,此時B對象實例化完成。

然后,將實例化完成的B對象遞歸的返回,此時就會將該實例注入到A對象中,這樣就得到了一個成品的A對象。

我們這里可以閱讀上面的第一個getSingleton()方法:

@Nullable
protected Object getSingleton(String beanName, boolean allowEarlyReference) {

  // 嘗試從緩存中獲取成品的目標對象,如果存在,則直接返回
  Object singletonObject = this.singletonObjects.get(beanName);

  // 如果緩存中不存在目標對象,則判斷當前對象是否已經處于創建過程中,在前面的講解中,第一次嘗試獲取A對象
  // 的實例之后,就會將A對象標記為正在創建中,因而最后再嘗試獲取A對象的時候,這里的if判斷就會為true
  if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {

    synchronized (this.singletonObjects) {
      singletonObject = this.earlySingletonObjects.get(beanName);
      if (singletonObject == null && allowEarlyReference) {

        // 這里的singletonFactories是一個Map,其key是bean的名稱,而值是一個ObjectFactory類型的
        // 對象,這里對于A和B而言,調用圖其getObject()方法返回的就是A和B對象的實例,無論是否是半成品
        ObjectFactory singletonFactory = this.singletonFactories.get(beanName);
        if (singletonFactory != null) {

          // 獲取目標對象的實例
          singletonObject = singletonFactory.getObject();
          this.earlySingletonObjects.put(beanName, singletonObject);
          this.singletonFactories.remove(beanName);
        }
      }
    }
  }
  return singletonObject;
}

這里我們會存在一個問題就是A的半成品實例是如何實例化的,然后是如何將其封裝為一個ObjectFactory類型的對象,并且將其放到上面的singletonFactories屬性中的。

這主要是在前面的第二個getSingleton()方法中,其最終會通過其傳入的第二個參數,從而調用createBean()方法,該方法的最終調用是委托給了另一個doCreateBean()方法進行的

這里面有如下一段代碼:

protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args)
  throws BeanCreationException {
  // 實例化當前嘗試獲取的bean對象,比如A對象和B對象都是在這里實例化的
  BeanWrapper instanceWrapper = null;
  if (mbd.isSingleton()) {
    instanceWrapper = this.factoryBeanInstanceCache.remove(beanName);
  }
  if (instanceWrapper == null) {
    instanceWrapper = createBeanInstance(beanName, mbd, args);
  }
  // 判斷Spring是否配置了支持提前暴露目標bean,也就是是否支持提前暴露半成品的bean
  boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences 
    && isSingletonCurrentlyInCreation(beanName));
  if (earlySingletonExposure) {

    // 如果支持,這里就會將當前生成的半成品的bean放到singletonFactories中,這個singletonFactories
    // 就是前面第一個getSingleton()方法中所使用到的singletonFactories屬性,也就是說,這里就是
    // 封裝半成品的bean的地方。而這里的getEarlyBeanReference()本質上是直接將放入的第三個參數,也就是
    // 目標bean直接返回
    addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
  }
  try {
    // 在初始化實例之后,這里就是判斷當前bean是否依賴了其他的bean,如果依賴了,
    // 就會遞歸的調用getBean()方法嘗試獲取目標bean
    populateBean(beanName, mbd, instanceWrapper);
  } catch (Throwable ex) {
    // 省略...
  }
  return exposedObject;
}

到這里,Spring整個解決循環依賴問題的實現思路已經比較清楚了。對于整體過程,讀者朋友只要理解兩點:

  • Spring是通過遞歸的方式獲取目標bean及其所依賴的bean的;
  • Spring實例化一個bean的時候,是分兩步進行的,首先實例化目標bean,然后為其注入屬性。

結合這兩點,也就是說,Spring在實例化一個bean的時候,是首先遞歸的實例化其所依賴的所有bean,直到某個bean沒有依賴其他bean,此時就會將該實例返回,然后反遞歸的將獲取到的bean設置為各個上層bean的屬性的。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,517評論 6 539
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,087評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,521評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,493評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,207評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,603評論 1 325
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,624評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,813評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,364評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,110評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,305評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,874評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,532評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,953評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,209評論 1 291
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,033評論 3 396
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,268評論 2 375

推薦閱讀更多精彩內容