Spring框架是怎么解決Bean之間的循環依賴的 (轉)


問題:

  循環依賴其實就是循環引用,也就是兩個或則兩個以上的bean互相持有對方,最終形成閉環。比如A依賴於B,B依賴於C,C又依賴於A。如下圖:

 

如何理解“依賴”呢,在Spring中有:

  • 構造器循環依賴
  • field屬性注入循環依賴

直接上代碼:

構造器循環依賴

@Service
public class A {  
    public A(B b) {  }
}

@Service
public class B {  
    public B(C c) {  
    }
}

@Service
public class C {  
    public C(A a) {  }
}

結果:項目啟動失敗,發現了一個cycle

 

 2.field屬性注入循環依賴

@Service
public class A1 {  
    @Autowired  
    private B1 b1;
}

@Service
public class B1 {  
    @Autowired  
    public C1 c1;
}

@Service
public class C1 {  
    @Autowired  public A1 a1;
}

結果:項目啟動成功

 

3.field屬性注入循環依賴(prototype)

@Service
@Scope("prototype")
public class A1 {  
    @Autowired  
    private B1 b1;
}

@Service
@Scope("prototype")
public class B1 {  
    @Autowired  
    public C1 c1;
}

@Service
@Scope("prototype")
public class C1 {  
    @Autowired  public A1 a1;
}

結果:項目啟動失敗,發現了一個cycle。

 
 

現象總結:同樣對於循環依賴的場景,構造器注入和prototype類型的屬性注入都會初始化Bean失敗。因為@Service默認是單例的,所以單例的屬性注入是可以成功的。

 

在分析Spring如何解決循環依的問題前我們先回顧一下 Spring的 set注入和構造器注入
 
  Spring種提供了2種常用的注入方式,set方法注入和構造函數注入。由於這2種注入方式很相似,都可以滿足我們的需求,所以在大多數情況下我們忽視了這2種注入方式的區別。下面讓我們看看這2種注入方式的特點。
  我們先看看Spring在使用set方法注入時,是怎樣實例化一個Bean和Bean的合作者的:
 

 

 在A中有一個setB方法用來接收B對象的實例。那么Spring實例化A對象的過程如下:

 

  在不考慮Bean的初始化方法和一些Spring回調的情況下,Spring首先去調用A對象的構造函數實例化A,然后查找A依賴的對象本例子中是B(合作者)。一但找到合作者,Spring就會調用合作者(B)的構造函數實例化B。如果B還有依賴的對象Spring會把B上依賴的所有對象都按照相同的機制實例化然后調用A對象的setB(B b)把b對象注入給A。
因為Spring調用一個對象的set方法注入前,這個對象必須先被實例化。所以在"使用set方法注入"的情況下Spring會首先調用對象的構造函數。

我們在來看通過構造函數注入的過程: 

 

   如果發現配置了對象的構造注入,那么Spring會在調用構造函數前把構造函數需要的依賴對象都實例化好,然后再把這些實例化后的對象作為參數去調用構造函數。

 
  在使用構造函數和set方法依賴注入時,Spring處理對象和對象依賴的對象的順序時不一樣的。一般把一個Bean設計為構造函數接收依賴對象時,其實是表達了這樣一種關系:他們(依賴對象)不存在時我也不存在,即“沒有他們就沒有我”。

  通過 構造函數的注入方式其實表達了2個對象間的一種 強的聚合關系:組合關系。就比如一輛車如果沒有輪子、引擎等部件那么車也就不存在了。而且車是由若干重 要部件組成的,在這些部件沒有的情況下車也不可能存在。這里車和他的重要部件就時組合的關系。如果你的應用中有這樣類似的場景那么你應該使用“構造函數注 入”的方式管理他們的關系。“構造函數注入”可以保證合作者先創建,在后在創建自己。
  通過 set方法注入的方式表達了2個對象間較 弱的 依賴關系:聚合關系。就像一輛車,如果沒有車內音像車也時可以工作的。當你不要求合作者於自己被創建 時,“set方法注入”注入比較合適。
  
  雖然在理論上“構造函數注入”和“set方法注入”代表2種不同的依賴強度,但是在spring中,spring並不會把無效的合作者傳遞給一個 bean。如果合作者無效或不存在spring會拋出異常,這樣spring保證一個對象的合作者都是可用的。所以在spring中,“構造函數注入”和 “set方法注入”唯一的區別在於2種方式創建合作者的順序不同。
  使用構造函數依賴注入時,Spring保證所有一個對象所有依賴的對象先實例化后,才實例化這個對象。(沒有他們就沒有我原則)
  使用set方法依賴注入時,Spring首先實例化對象,然后才實例化所有依賴的對象。
 
 
 

Spring如何解決循環依賴

spring中循環依賴有三種情況:

  1、構造器注入形成的循環依賴。也就是beanB需要在beanA的構造函數中完成初始化,beanA也需要在beanB的構造函數中完成舒適化,這種情況的結果就是兩個bean都不能完成初始化,循環依賴難以解決。

  2、setter注入構成的循環依賴。beanA需要在beanB的setter方法中完成初始化,beanB也需要在beanA的setter方法中完成初始化,spring設計的機制主要就是解決這種循環依賴,也是今天下文討論的重點。

  3、prototype作用域bean的循環依賴。這種循環依賴同樣無法解決,因為spring不會緩存‘prototype’作用域的bean,而spring中循環依賴的解決正是通過緩存來實現的。

下面主要說明第二種情況中循環依賴的解決方案

  步驟一:beanA進行初始化,並且將自己進行初始化的狀態記錄下來,並提前向外暴露一個單例工程方法,從而使其他bean能引用到該bean(可能讀完這一句,您仍然心存疑惑,沒關系,繼續往下讀)

  步驟二:beanA中有beanB的依賴,於是開始初始化beanB。

  步驟三:初始化beanB的過程中又發現beanB依賴了beanA,於是又進行beanA的初始化,這時發現beanA已經在進行初始化了,程序發現了存在的循環依賴,然后通過步驟一中暴露的單例工程方法拿到beanA的引用(注意,此時的beanA只是完成了構造函數的注入但為完成其他步驟),從而beanB拿到beanA的引用,完成注入,完成了初始化,如此beanB的引用也就可以被beanA拿到,從而beanA也就完成了初始化。

  spring進行bean的加載的時候,首先進行bean的初始化(調用構造函數),然后進行屬性填充。在這兩步中間,spring對bean進行了一次狀態的記錄,也就是說spring會把指向只完成了構造函數初始化的bean的引用通過一個變量記錄下來,明白這一點對之后的源碼理解至關重要。

源碼角度觀看循環依賴的解決步驟

步驟一中首先進行beanA的創建

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;
    }
});

進入getSingleton中,spirng會記錄當前beanA正在創建中

if (!this.inCreationCheckExclusions.contains(beanName) && !this.singletonsCurrentlyInCreation.add(beanName)) {
  throw new BeanCurrentlyInCreationException(beanName);
}

並且將注冊一個工廠方法來解決循環依賴

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");
            }
     這里:    addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
        }

主要就是addSingletonFactory,這句就完成了工廠方法的注冊,這個方法可以返回一個只完成了構造函數初始化的beanA,也許大家想知道他是如何返回的,我們進入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;
    }

  可以看到這個方法中,除了對后處理器的調用,沒有進行任何動作,而是直接返回了我們參數傳入的bean,那么這個bean是哪來的呢?其實在這之前,spring先調用了beanA的構造函數,並拿到了只完成了構造函數初始化的一個實例,並把他記錄了下來。

if (mbd.isSingleton()) {
            instanceWrapper = this.factoryBeanInstanceCache.remove(beanName);
        }
        if (instanceWrapper == null) {
            instanceWrapper = createBeanInstance(beanName, mbd, args);
        }
        final Object bean = instanceWrapper.getWrappedInstance();

  這就是這個bean的由來。然后當我們進行到步驟三的時候,就會檢查是否允許循環依賴(即使是Singleton類型的bean也可以通過參數設置,禁止循環依賴),如果允許的話,就會通過這個工廠方法拿到beanA的引用。從而完成beanA和beanB的加載。

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) {
                    ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
                    if (singletonFactory != null) {
              這里:     singletonObject = singletonFactory.getObject();
                        this.earlySingletonObjects.put(beanName, singletonObject);
                        this.singletonFactories.remove(beanName);
                    }
                }
            }
        }
        return singletonObject;
    }

 

也可以看這篇 : Spring如何解決循環依賴的問題

 

  在關於Spring的面試中,我們經常會被問到一個問題,就是Spring是如何解決循環依賴的問題的。這個問題算是關於Spring的一個高頻面試題,因為如果不刻意研讀,相信即使讀過源碼,面試者也不一定能夠一下子思考出個中奧秘。本文主要針對這個問題,從源碼的角度對其實現原理進行講解。

1. 過程演示

        關於Spring bean的創建,其本質上還是一個對象的創建,既然是對象,讀者朋友一定要明白一點就是,一個完整的對象包含兩部分:當前對象實例化和對象屬性的實例化。在Spring中,對象的實例化是通過反射實現的,而對象的屬性則是在對象實例化之后通過一定的方式設置的。這個過程可以按照如下方式進行理解:

        理解這一個點之后,對於循環依賴的理解就已經幫助一大步了,我們這里以兩個類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屬性設置了值。下面我們通過一個流程圖來對這個過程進行講解:

        圖中getBean()表示調用Spring的ApplicationContext.getBean()方法,而該方法中的參數,則表示我們要嘗試獲取的目標對象。圖中的黑色箭頭表示一開始的方法調用走向,走到最后,返回了Spring中緩存的A對象之后,表示遞歸調用返回了,此時使用綠色的箭頭表示。從圖中我們可以很清楚的看到,B對象的a屬性是在第三步中注入的半成品A對象,而A對象的b屬性是在第二步中注入的成品B對象,此時半成品的A對象也就變成了成品的A對象,因為其屬性已經設置完成了。

2. 源碼講解

        對於Spring處理循環依賴問題的方式,我們這里通過上面的流程圖其實很容易就可以理解,需要注意的一個點就是,Spring是如何標記開始生成的A對象是一個半成品,並且是如何保存A對象的。這里的標記工作Spring是使用ApplicationContext的屬性Set<String> singletonsCurrentlyInCreation來保存的,而半成品的A對象則是通過Map<String, ObjectFactory<?>> singletonFactories來保存的,這里的ObjectFactory是一個工廠對象,可通過調用其getObject()方法來獲取目標對象。在AbstractBeanFactory.doGetBean()方法中獲取對象的方法如下:

protected <T> T doGetBean(final String name, @Nullable final Class<T> 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的屬性的。

3. 小結

        本文首先通過圖文的方式對Spring是如何解決循環依賴的問題進行了講解,然后從源碼的角度詳細講解了Spring是如何實現各個bean的裝配工作的。

 

 

出處:

  https://cloud.tencent.com/developer/article/1455953

  https://www.jianshu.com/p/8bb67ca11831


 

另一篇文章關於Spring的循環依賴分析:  圖解Spring解決循環依賴

前言

Spring如何解決的循環依賴,是近兩年流行起來的一道Java面試題。

其實筆者本人對這類框架源碼題還是持一定的懷疑態度的。

如果筆者作為面試官,可能會問一些諸如“如果注入的屬性為null,你會從哪幾個方向去排查”這些場景題。

那么既然寫了這篇文章,閑話少說,發車看看Spring是如何解決的循環依賴,以及帶大家看清循環依賴的本質是什么。

正文

通常來說,如果問Spring內部如何解決循環依賴,一定是單默認的單例Bean中,屬性互相引用的場景。

比如幾個Bean之間的互相引用:

               

甚至自己“循環”依賴自己:

                     

先說明前提:原型(Prototype)的場景是不支持循環依賴的,通常會走到AbstractBeanFactory類中下面的判斷,拋出異常。

if (isPrototypeCurrentlyInCreation(beanName)) {
  throw new BeanCurrentlyInCreationException(beanName);
}

原因很好理解,創建新的A時,發現要注入原型字段B,又創建新的B發現要注入原型字段A...

這就套娃了, 你猜是先StackOverflow還是OutOfMemory?

Spring怕你不好猜,就先拋出了BeanCurrentlyInCreationException


基於構造器的循環依賴,就更不用說了,官方文檔都攤牌了,你想讓構造器注入支持循環依賴,是不存在的,不如把代碼改了。

那么默認單例的屬性注入場景,Spring是如何支持循環依賴的?

Spring解決循環依賴

首先,Spring內部維護了三個Map,也就是我們通常說的三級緩存。

筆者翻閱Spring文檔倒是沒有找到三級緩存的概念,可能也是本土為了方便理解的詞匯。

在Spring的DefaultSingletonBeanRegistry類中,你會赫然發現類上方掛着這三個Map:

  • singletonObjects 它是我們最熟悉的朋友,俗稱“單例池”“容器”,緩存創建完成單例Bean的地方。

  • singletonFactories 映射創建Bean的原始工廠

  • earlySingletonObjects 映射Bean的早期引用,也就是說在這個Map里的Bean不是完整的,甚至還不能稱之為“Bean”,只是一個Instance.

后兩個Map其實是“墊腳石”級別的,只是創建Bean的時候,用來借助了一下,創建完成就清掉了。

所以筆者前文對“三級緩存”這個詞有些迷惑,可能是因為注釋都是以Cache of開頭吧。

為什么成為后兩個Map為墊腳石,假設最終放在singletonObjects的Bean是你想要的一杯“涼白開”。

那么Spring准備了兩個杯子,即singletonFactoriesearlySingletonObjects來回“倒騰”幾番,把熱水晾成“涼白開”放到singletonObjects中。

閑話不說,都濃縮在圖里。

上面的是一張GIF,如果你沒看到可能還沒加載出來。三秒一幀,不是你電腦卡

筆者畫了17張圖簡化表述了Spring的主要步驟,GIF上方即是剛才提到的三級緩存,下方展示是主要的幾個方法。

當然了,這個地步你肯定要結合Spring源碼來看,要不肯定看不懂。

如果你只是想大概了解,或者面試,可以先記住筆者上文提到的“三級緩存”,以及下文即將要說的本質。

循環依賴的本質

上文了解完Spring如何處理循環依賴之后,讓我們跳出“閱讀源碼”的思維,假設讓你實現一個有以下特點的功能,你會怎么做?

  • 將指定的一些類實例為單例
  • 類中的字段也都實例為單例
  • 支持循環依賴

舉個例子,假設有類A:

public class A {
    private B b;
}

類B:

public class B {
    private A a;
}

說白了讓你模仿Spring:假裝A和B是被@Component修飾,
並且類中的字段假裝是@Autowired修飾的,處理完放到Map中。

其實非常簡單,筆者寫了一份粗糙的代碼,可供參考:

import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;

public class SpringTest {
    
    private static Map<String, Object> cacheMap = new HashMap<String, Object>(2);
    
    public static void main(String[] args) throws Exception {
        // 假裝掃描出來的對象
        Class[] classes = {A.class, B.class};
        
        for(Class aClass : classes) {
            getBean(aClass);
        }
        
        // check
        System.out.println(getBean(B.class).getA() == getBean(A.class));
        System.out.println(getBean(A.class).getB() == getBean(B.class));
    }
    
    private static <T> T getBean(Class<T> beanClass) throws Exception{
        // 本文用類名小寫 簡單代替bean的命名規則
        String beanName = beanClass.getSimpleName().toLowerCase();
        
        // 如果已經是一個bean,則直接返回
        if (cacheMap.containsKey(beanName)) {
            return (T) cacheMap.get(beanName);
        }
        
        // 將對象本身實例化
        Object object = beanClass.getDeclaredConstructor().newInstance();
        
        // 放入緩存
        cacheMap.put(beanName, object);
        
        // 把所有字段當成需要注入的bean,創建並注入到當前bean中
        Field[] fields = object.getClass().getDeclaredFields();
        for (Field field : fields) {
            field.setAccessible(true);
            // 獲取需要注入字段的class
            Class<?> fieldClass = field.getType();
            String fieldBeanName = fieldClass.getSimpleName().toLowerCase();
            
            // 如果需要注入的bean,已經在緩存Map中,那么把緩存Map中的值注入到該field即可
            // 如果緩存沒有 繼續創建
            field.set(object, cacheMap.containsKey(fieldBeanName)
                                ? cacheMap.get(fieldBeanName) : getBean(fieldClass));
        }
        // 屬性填充完成,返回
        return (T) object;
    }

}

class A {
    private B b;

    public B getB() {
        return b;
    }

    public void setB(B b) {
        this.b = b;
    }
    
}

class B {
    private A a;

    public A getA() {
        return a;
    }

    public void setA(A a) {
        this.a = a;
    }
}

這段代碼的效果,其實就是處理了循環依賴,並且處理完成后,cacheMap中放的就是完整的“Bean”了

 

這就是“循環依賴”的本質,而不是“Spring如何解決循環依賴”。

之所以要舉這個例子,是發現一小部分盆友陷入了“閱讀源碼的泥潭”,而忘記了問題的本質。

為了看源碼而看源碼,結果一直看不懂,卻忘了本質是什么。

如果真看不懂,不如先寫出基礎版本,逆推Spring為什么要這么實現,可能效果會更好。

what?問題的本質居然是two sum!

看完筆者剛才的代碼有沒有似曾相識?沒錯,和two sum的解題是類似的。

不知道two sum是什么梗的,筆者和你介紹一下:

two sum是刷題網站leetcode序號為1的題,也就是大多人的算法入門的第一題。

常常被人調侃,有算法面的公司,被面試官欽定了,合的來。那就來一道two sum走走過場。

問題內容是:給定一個數組,給定一個數字。返回數組中可以相加得到指定數字的兩個索引。

比如:給定nums = [2, 7, 11, 15], target = 9
那么要返回 [0, 1],因為2 + 7 = 9

這道題的優解是,一次遍歷+HashMap:

class Solution {    
    public int[] twoSum(int[] nums, int target) {        
        Map<Integer, Integer> map = new HashMap<>();        
        for (int i = 0; i < nums.length; i++) {            
            int complement = target - nums[i];           
             if (map.containsKey(complement)) {                
                return new int[] { map.get(complement), i };   
            }            
            map.put(nums[i], i);        
        }        
        throw new IllegalArgumentException("No two sum solution");    
    }
}

先去Map中找需要的數字,沒有就將當前的數字保存在Map中,如果找到需要的數字,則一起返回。

和筆者上面的代碼是不是一樣?

先去緩存里找Bean,沒有則實例化當前的Bean放到Map,如果有需要依賴當前Bean的,就能從Map取到。

結尾

如果你是上文筆者提到的“陷入閱讀源碼的泥潭”的讀者,上文應該可以幫助到你。

可能還有盆友有疑問,為什么一道“two-sum”,Spring處理的如此復雜?
這個想想Spring支持多少功能就知道了,各種實例方式..各種注入方式..各種Bean的加載,校驗..各種callback,aop處理等等..

Spring可不只有依賴注入,同樣Java也不僅是Spring。如果我們陷入了某個“牛角尖”,不妨跳出來看看,可能會更佳清晰哦。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM