Spring的循環依賴和三級緩存

1. 循環依賴

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

注意,這裏不是函數的循環調用,是對象的相互依賴關係。循環調用其實就是一個死循環,除非有終結條件。

循環依賴就是N個類中循環嵌套引用,如果在日常開發中我們用new 對象的方式發生這種循環依賴的話程序會在運行時一直循環調用,直至內存溢出報錯。

2. Spring循環依賴的場景

常規Java的循環依賴有兩個場景:

  1. 構造器的循環依賴。
  2. field屬性的循環依賴。

如果是Spring的依賴注入場景的話,field屬性的循環依賴還可以分爲

  • 單例bean的循環依賴(scope=singleton)
  • 非單例bean的循環依賴(scope=prototype)

2.1 構造器的循環依賴

構造器的循環依賴代碼如下:

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

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


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

我們如果啓動Spring的初始化流程,最後執行得到的報錯是:

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? 

2.2 屬性的循環依賴

屬性的依賴,我們都知道是Spring利用反射,調用了對應屬性的setter方式進行注入的。

2.2.1 單例屬性的循環依賴

我們知道,Spring中@Service和@Autowired註解都是默認的單例模式,即scope=singleton。

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

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

結果:項目啓動成功

2.2.1 原型屬性的循環依賴

ok,單例屬性的循環依賴是被允許的,那麼原型模式呢?我們添加一個scope=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:

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? 

3 原因分析

3.1 bean的初始化

在Spring中,同樣對於循環依賴的場景,構造器注入和prototype類型的屬性注入都會初始化Bean失敗。只有單例的屬性注入是可以成功的,這是爲什麼呢?

原因就藏在Spring IOC的源碼中。

我們知道Bean的初始化流程如下圖所示:

這裏面最重要的是如下三步:

分別對應Spring源碼中的這三個方法:

  1. createBeanInstance()方法:實例化,其實也就是調用對象的構造方法實例化對象,或者說Spring利用反射,new了一個對象。

  2. populateBean()方法:填充屬性,這一步主要是多bean的依賴屬性進行填充。

  3. initializeBean()方法:調用spring xml中的init方法,不過這個和循環依賴無關,我們不多解釋。

可以看到,Spring是先將Bean對象實例化之後再設置對象屬性的

3.2 三級緩存

我們知道了在Spring中對象實例化和對象屬性填充是分成兩步來操作的,爲了解決循環依賴,Spring內部維護了三個Map,也就是我們通常說的三級緩存。

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

三級緩存在DefaultListableBeanFactory類中(繼承自其父類DefaultSingletonBeanRegistry):

 /** Cache of singleton objects: bean name --> bean instance(緩存單例實例化對象的Map集合) 一級緩存*/
 private final Map<String, Object> singletonObjects = new ConcurrentHashMap<String, Object>(64); 
  
 /** Cache of singleton factories: bean name --> ObjectFactory(單例的工廠Bean緩存集合) 三級緩存*/
 private final Map<String, ObjectFactory> singletonFactories = new HashMap<String, ObjectFactory>(16); 
  
 /** Cache of early singleton objects: bean name --> bean instance(早期的單身對象緩存集合) 二級緩存*/
 private final Map<String, Object> earlySingletonObjects = new HashMap<String, Object>(16); 

因爲在Spring中,對象實例化和對象屬性填充是分成兩步來操作的,那麼很顯然,一個bean可以被分成兩個階段,名字是我自己取的:

  1. 胚胎階段:即已經new出了一個對象,但是還沒完成populateBean()方法,依賴的屬性還未填充完畢的階段。
  2. 成熟階段:即完成populateBean()方法,依賴的屬性已經填充完畢的階段,此時對象就已經是一個成熟的對象了。

所以對應的,不同階段的bean,會被存放在不同級別的緩存中,三級緩存因此而來:

  1. singletonFactories:三級緩存,保存胚胎階段對象的工廠類
  2. earlySingletonObjects:二級緩存,保存胚胎階段的對象。
  3. singletonObjects:一級緩存,俗稱單例池或者容器。構造完成的成熟階段的單例對象都在裏面。

這三個緩存中,三級緩存和循環依賴息息相關,那麼Spring如何利用三級緩存來解決循環依賴呢?我們來理一下整個初始化bean的全過程。

Spring初始化容器對象的代碼在org.springframework.context.support.AbstractApplicationContext#refresh()中方法,它調用finishBeanFactoryInitialization(beanFactory)方法,進而調用了org.springframework.beans.factory.support.DefaultListableBeanFactory#preInstantiateSingletons()方法,該方法顧名思義,負責遍歷註冊的beanName,依次初始化所有非懶加載的單例bean。

public void preInstantiateSingletons() throws BeansException {
    ...
    ...
    List<String> beanNames = new ArrayList<String>(this.beanDefinitionNames);
    // 將獲取到的所有bean依次初始化
    for (String beanName : beanNames) {
        RootBeanDefinition bd = getMergedLocalBeanDefinition(beanName);
        if (!bd.isAbstract() && bd.isSingleton() && !bd.isLazyInit()) {
            if (isFactoryBean(beanName)) {
                // 初始化的關鍵方法,getBean()方法
                final FactoryBean factory = (FactoryBean) getBean(FACTORY_BEAN_PREFIX + beanName);
                ...
            }
            else {
                getBean(beanName);
            }
        }
    }
    ...
    ...
}

**AbstractBeanFactory.getBean(beanName)**是核心方法,它調用的是AbstractBeanFactory.doGetBean(beanName)方法,初始化bean的邏輯就在其中。

@SuppressWarnings("unchecked")
protected <T> T doGetBean(
        final String name, final Class<T> requiredType, final Object[] args, boolean typeCheckOnly)
        throws BeansException {

    // 從三級緩存中取出bean
    Object sharedInstance = getSingleton(beanName);
    if (sharedInstance != null && args == null) {
        if (logger.isDebugEnabled()) {
            // 檢查當前 Bean是否正處於正在創建的狀態(新生階段)中,當Bean創建時會將Bean名稱存放到singletonsCurrentlyInCreation集合中
            if (isSingletonCurrentlyInCreation(beanName)) {
                ...
            }
            ...
        }
        bean = getObjectForBeanInstance(sharedInstance, name, beanName, null);
    }else{
        ...
        ...
        // Create bean instance.
        // 如果該bean是單例
        if (mbd.isSingleton()) {
            // 處理
            sharedInstance = getSingleton(beanName, new ObjectFactory() {
                public Object getObject() throws BeansException {
                    try {
                        return createBean(beanName, mbd, args);
                    }
                    catch (BeansException ex) {
                        ...
                    }
                }
            });
            bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
        }
        ...
    }
    ...
    return (T) bean;
}

其中,重點的方法DefaultSingletonBeanRegistry.getSingleton(beanName),我們可以看下邏輯:

protected Object getSingleton(String beanName, boolean allowEarlyReference) {
    // 首先先嚐試從singletonObjects緩存裏面獲取單例bean對象
    Object singletonObject = this.singletonObjects.get(beanName);
    // 如果緩存中沒獲取到單例bean對象
    if (singletonObject == null) {
        synchronized (this.singletonObjects) {
            // 嘗試從earlySingletonObjects緩存裏面獲取早期單例bean對象
            singletonObject = this.earlySingletonObjects.get(beanName);
            if (singletonObject == null && allowEarlyReference) {
                // 如果earlySingletonObjects緩存裏面也沒有早期bean對象,那麼從factory緩存裏獲取工廠類。
                ObjectFactory singletonFactory = this.singletonFactories.get(beanName);
                if (singletonFactory != null) {
                    // 利用工廠類生成一個單例bean對象
                    singletonObject = singletonFactory.getObject();
                    // 將剛創建的早起bean對象,放進earlySingletonObjects緩存
                    this.earlySingletonObjects.put(beanName, singletonObject);
                    // 從singletonFactories中移除,結合放入earlySingletonObjects中的操作。其實也就是從三級緩存移動到了二級緩存。
                    this.singletonFactories.remove(beanName);
                }
            }
        }
    }
    return (singletonObject != NULL_OBJECT ? singletonObject : null);
}

doGetBean()方法裏面邏輯很多,我們簡單描述其調用堆棧(只列出和循環依賴相關的堆棧):

  • 1 DefaultSingletonBeanRegistry.getSingleton(beanName)
    • 1.1【嘗試從一級緩存取中bean,如果取到就返回】
    • 1.2【如果取不到,就加鎖,從二級緩存取,如果取到就返回】
    • 1.3【如果取不到,再從三級緩存中取到ObjectFactory對象,如果取到了】
      • 1.3.1 ObjectFactory.getObject() // 通過factory類獲取bean對象
      • 1.3.2【從三級緩存中刪除該bean的工廠類,並將得到的bean對象加入二級緩存】
    • 1.4【如果取不到ObjectFactory對象,返回null】
  • 2 【如果getSingleton(beanName)返回的null】
    • 【如果該bean是單例】
      • DefaultSingletonBeanRegistry.getSingleton(beanName,factory) // 該方法負責實例化bean。factory的getObject()調用createBean
      • ①【將當前bean放入singletonsCurrentlyInCreation這個Set中,表示該bean正在創建】
      • ② ObjectFactory.getObject() // 調用工廠方法,獲取bean對象
        • AbstractAutowireCapableBeanFactory.createBean() // 創建對象
          • AbstractAutowireCapableBeanFactory.doCreateBean() // 實際方法
            • ⑴ AbstractAutowireCapableBeanFactory.createBeanInstance()
              • AbstractAutowireCapableBeanFactory.instantiateBean() // 實例化bean
            • ⑵ 【如果當前bean是創建中的(當前bean是否在singletonsCurrentlyInCreation中來判斷)單例bean,且Spring配置支持循環依賴】
              • DefaultSingletonBeanRegistry.addSingletonFactory() // 將bean加入三級緩存。factory的getObject()調用getEarlyBeanReference
            • AbstractAutowireCapableBeanFactory.populateBean() // 填充依賴的屬性
              • AutowiredAnnotationBeanPostProcessor.postProcessPropertyValues() // 對屬性進行賦值
                • AutowiredAnnotationBeanPostProcessor.AutowiredFieldElement.inject() // 依賴注入
                  • AbstractBeanFactory.getBean(B)
                    • ...
                    • ...
                    • ...
      • ③【將當前bean從singletonsCurrentlyInCreation這個Set中刪除,表示該bean完成創建】
      • ④【將當前bean加入一級緩存中,並且在二級三級緩存中刪除該bean】

如果populateBean()方法中A bean依賴了B bean,那麼就會進入AbstractBeanFactory.getBean(B)的邏輯,於是,整個流程如下圖:

3.3 三種循環依賴的總結

所以我們可以看到,單例的屬性注入流程中有兩個重點,就是這兩個點,解決了循環依賴:

  1. 提前曝光,如果用c語言的說法就是將指針曝光出去,用java就是將引用對象曝光出去。也就是說即便a對象還未創建完成,但是在實例化過程中new A()動作完成後,A bean就已經被放進了緩存之中,接下來B bean就可以引用的到。

  2. 已經瞭解了提前曝光的作用,而相比而言曝光的時機也非常的重要,該時機發生在實例化之後,填充屬性初始化之前。

正是因爲屬性注入(或者說set方法注入)時,實例化和初始化是分開的兩步,所以才能讓Spring有可乘之機,在這兩個步驟之間做提前曝光,這纔有了Spring能夠支持set方法注入時循環依賴的結論。


而構造器的循環依賴Spring之所以不支持,也正是因爲此時實例化和初始化是原子的一個步驟,沒有辦法在中間插入提前曝光的機會。

構造器注入的報錯如下圖:


至於原型模式下的循環依賴,其實很好理解,因爲原型模式每次都是重新生成一個全新的bean,根本沒有緩存一說。這將導致實例化A完,填充發現需要B,實例化B完又發現需要A,而每次的A又都要不一樣,所以死循環的依賴下去。

唯一的做法就是利用循環依賴檢測,發現原型模式下存在循環依賴並拋出異常。

AbstractBeanFactory工廠類有個Set,叫做prototypesCurrentlyInCreation,它和前文中描述的singletonsCurrentlyInCreation一樣,用來存放正在創建中的bean對象,只不過前者存的是原型模式的bean,後者存的是單例模式的bean。

Spring會在實例化prototype bean後將其放入prototypesCurrentlyInCreation中,如果有循環依賴,就會檢查被依賴的bean是否也在prototypesCurrentlyInCreation中,如果是,那就表示依賴的bean和被依賴的bean同時在創建中,那就發生了循環依賴,這是不允許的。

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

3.4 爲什麼要有三級緩存

經過前文的敘述,我們貌似發現,三級緩存和二級緩存,貌似作用有點重複,兩級緩存不夠嗎,一級緩存不夠嗎?爲什麼要用三級緩存?

只用一級緩存肯定不行,這很好理解,一級緩存的問題在於,就一個map,裏面既有完整的bean,也有不完整的,尚未設置屬性的bean。如果這時候,有其他線程獲取到了不完整的bean,並且對還是null的屬性做操作,那就直接空指針了。

那麼兩級緩存夠嗎?其實是夠的,IoC循環依賴,兩級緩存就夠用了

但是,如果參與循環依賴的A和B中,至少有一個對象有AOP切面呢?(AOP切面會動態生成一個代理對象,依賴注入的實際上得是代理對象纔行)

在考慮有AOP動態代理對象存在的情況下,兩級緩存就不夠用了,假設我們給A加了個切面,Spring給A生成了一個動態代理對象A_Proxy。

如果只有兩級緩存,一級緩存放完成初始化的bean,二級緩存放提前曝光的早期bean。那麼

  1. A完成實例化之後將引用提前曝光至二級緩存,並開始初始化B,
  2. B發現要依賴A,就會從二級緩存中取出A對象,注入屬性。此時B就會錯誤的引用了A,而不是Spring希望的引用A_Proxy。

那三級緩存就能解決這個問題麼?可以的,還記得我們的第三級緩存存放的是工廠類ObjectFactory。當三級緩存命中的時候,我們是調用ObjectFactory.getObject()來獲取對象的,而getObject()實際調用的又是各個beanPostProcessor的getEarlyBeanReference()方法:

addSingletonFactory(beanName, new ObjectFactory() {
    public Object getObject() throws BeansException {
        return getEarlyBeanReference(beanName, mbd, bean);
    }
});

其中,主要就是AOP的主力beanPostProcessor,AbstractAutoProxyCreator#getEarlyBeanReference:

protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
    Object exposedObject = bean;
    if (bean != null && !mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
        for (BeanPostProcessor bp : getBeanPostProcessors()) {
            if (bp instanceof SmartInstantiationAwareBeanPostProcessor) {
                SmartInstantiationAwareBeanPostProcessor ibp = (SmartInstantiationAwareBeanPostProcessor) bp;
                // AbstractAutoProxyCreator的getEarlyBeanReference
                // 調用了SmartInstantiationAwareBeanPostProcessor的getEarlyBeanReference
                exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName);
                if (exposedObject == null) {
                    return exposedObject;
                }
            }
        }
    }
    return exposedObject;
}

在看SmartInstantiationAwareBeanPostProcessor的getEarlyBeanReference():

public Object getEarlyBeanReference(Object bean, String beanName) throws BeansException {
    Object cacheKey = getCacheKey(bean.getClass(), beanName);
    this.earlyProxyReferences.add(cacheKey);
    // 此處,會創建代理,於是返回的對象,就是個代理對象了
    return wrapIfNecessary(bean, beanName, cacheKey);
}

這就能保證如果有動態代理的情況,那麼從三級緩存取出來的對象,就會是代理對象A_Proxy。

我們把doCreateBean的流程串起來走一下,只列出相關的代碼,並假設A和B循環依賴,且A有AOP切面,我們稱原始的A爲A_Origin,A的代理對象爲A_Proxy:

protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final Object[] args) {
    ...

    if (instanceWrapper == null) {
        // 創建A_Origin對象,此時,屬性什麼的全是null,可以理解爲,只是new了,field還沒設置
        instanceWrapper = createBeanInstance(beanName, mbd, args);
    }
    final Object bean = (instanceWrapper != null ? instanceWrapper.getWrappedInstance() : null);
    Class beanType = (instanceWrapper != null ? instanceWrapper.getWrappedClass() : null);

    ...
    
    // 如果該bean是單例且允許Spring配置允許循環依賴,且當前bean確實在創建中
    boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
            isSingletonCurrentlyInCreation(beanName));
    if (earlySingletonExposure) {
        ...
        // 添加到第三級緩存;加進去的,只是個factory,只有循環依賴的時候,纔會發揮作用
        addSingletonFactory(beanName, new ObjectFactory() {
            public Object getObject() throws BeansException {
                return getEarlyBeanReference(beanName, mbd, bean);
            }
        });
    }

    // 把A_Origin,存到exposedObject
    Object exposedObject = bean;
    try {
        // 填充屬性;循環依賴情況下,A/B循環依賴。當前爲A_Origin,那麼此時填充A的屬性的時候,會去:new B;
        // 並且因爲三級緩存機制,B中引用的A是A_Proxy。然後B初始化完成,邏輯返回,繼續A_Origin的初始化,A_Origin引用了B。
        populateBean(beanName, mbd, instanceWrapper);
        if (exposedObject != null) {
            // 對A_Origin進行後置處理,此時調用aop後置處理器的postProcessAfterInitialization;
            // 這裏底層是有可能調用wrapIfNecessary獲取到代理對象。
            // 不過Spring爲了防止wrapIfNecessary被重複調用,從而生成不同的A_Proxy對象,
            // 所以對於一個bean而言,Spring會記下已經調用了wrapIfNecessary的beanName,保證wrapIfNecessary只會被調用一次。
            // 因爲在B的初始化中,A的wrapIfNecessary已經被調用了,所以在這裏,wrapIfNecessary不會被調用
            // 所以此處返回的是A_Origin
            exposedObject = initializeBean(beanName, exposedObject, mbd);
        }
    }

    ...

    if (earlySingletonExposure) {
        // 去三級緩存裏獲取A,拿到的是A_Proxy
        Object earlySingletonReference = getSingleton(beanName, false);
        if (earlySingletonReference != null) {
            // 此時exposedObject=A_Origin, bean=A_Origin, earlySingletonReference=A_Proxy
            // 所以下面這個條件是滿足的,所以,exposedObject,最終被替換爲A_Proxy:
            if (exposedObject == bean) {
                exposedObject = earlySingletonReference;
            }
            ...
        }
    }

    ...

    return exposedObject;
}

讀到這裏,也許有人會問,就算只使用兩級緩存,我如果在A實例化後,緊接着就調用getEarlyBeanReference()方法去創建切面,然後將生成的A_Proxy放入二級緩存行不行?這不是又可以避免代理對象的問題,又只需要兩級緩存嗎?

答案是:理論上,是的,可以,但性能不好。

因爲Spring中循環依賴出現場景很少,我們沒有必要爲了解決系統中那1%可能出現的循環依賴問題,而讓99%的bean在創建時都去調用getEarlyBeanReference()走上這麼一圈。大部分bean調用getEarlyBeanReference(),只會徒增判斷邏輯,而沒有實質的作用,他們既沒有切面,也沒有配置相關的BeanPostProcessor類。

使用三級緩存,就可以讓確實有循環依賴場景的bean纔會去調用getEarlyBeanReference()。因爲只有有循環依賴場景的bean,纔會用到二三級緩存。

而正常的bean都是

實例化——加入三級緩存——注入屬性——執行init方法——執行BeanPostProcessor的方法——加入一級緩存——刪除三級緩存——完成初始化

這樣的流程。三級緩存的增刪,只是一個以防萬一而已。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章