天天看點

Spring 如何解決循環依賴?

在Spring實際的開發過程中,可能會出現一種情況:BeanA 依賴BeanB ,BeanB依賴BeanA,代碼如下:

@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為例,如ApplicationContext.getBean(A),這個時候由于還沒有A的執行個體,則會去建立A對象。然後發現其依賴了B對象,因而會嘗試遞歸的通過ApplicationContext.getBean()方法擷取B對象的執行個體。此時A對象和B對象都已經建立了,并且儲存在Spring容器中了,隻不過A對象的屬性b和B對象的屬性a都還沒有設定進去。

在建立完B對象執行個體的時候,這個時候B對象發現有依賴A對象的執行個體,因而還是會嘗試遞歸的調用ApplicationContext.getBean()方法擷取A對象的執行個體。但是在調用ApplicationContext.getBean()方法擷取A對象的執行個體時候發現已經存在了A執行個體,雖然這個A執行個體還沒有注入屬性,這裡需要注意:此時會将半成品的A執行個體注入到B執行個體中,這裡注入的其實就是一個B執行個體對象引用,也就是位址。

然後還是ApplicationContext.getBean()方法遞歸的傳回,也就是将B對象的執行個體傳回,此時就會将該執行個體設定到A對象的屬性b中。這裡的A對象其實和前面設定到執行個體B中的半成品A對象是同一個對象,其引用位址是同一個,這裡為A對象的b屬性設定了值,其實也就是為那個半成品的a屬性設定了值。

這裡可以通過流程圖幫助了解:

Spring 如何解決循環依賴?

源碼解析:

對于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,然後為其注入屬性。