爛了大街的 Spring 循環(huán)依賴(lài)問(wèn)題拙毫,你以為自己就真會(huì)了嗎

前言

循環(huán)依賴(lài)問(wèn)題,算是一道爛大街的面試題了棺禾,解毒之前缀蹄,我們先來(lái)回顧兩個(gè)知識(shí)點(diǎn):

初學(xué) Spring 的時(shí)候,我們就知道 IOC膘婶,控制反轉(zhuǎn)碼缺前,它將原本在程序中手動(dòng)創(chuàng)建對(duì)象的控制權(quán),交由 Spring 框架來(lái)管理悬襟,不需要我們手動(dòng)去各種 new XXX衅码。

盡管是 Spring 管理,不也得創(chuàng)建對(duì)象嗎脊岳, Java 對(duì)象的創(chuàng)建步驟很多逝段,可以 new XXX、序列化割捅、clone() 等等奶躯, 只是 Spring 是通過(guò)反射 + 工廠的方式創(chuàng)建對(duì)象并放在容器的,創(chuàng)建好的對(duì)象我們一般還會(huì)對(duì)對(duì)象屬性進(jìn)行賦值亿驾,才去使用嘹黔,可以理解是分了兩個(gè)步驟。

好了莫瞬,對(duì)這兩個(gè)步驟有個(gè)印象就行儡蔓,接著我們進(jìn)入循環(huán)依賴(lài),先說(shuō)下循環(huán)依賴(lài)的概念

什么是循環(huán)依賴(lài)

所謂的循環(huán)依賴(lài)是指疼邀,A 依賴(lài) B浙值,B 又依賴(lài) A,它們之間形成了循環(huán)依賴(lài)檩小。或者是 A 依賴(lài) B烟勋,B 依賴(lài) C规求,C 又依賴(lài) A,形成了循環(huán)依賴(lài)卵惦。更或者是自己依賴(lài)自己阻肿。它們之間的依賴(lài)關(guān)系如下:

爛了大街的 Spring 循環(huán)依賴(lài)問(wèn)題,你以為自己就真會(huì)了嗎

這里以?xún)蓚€(gè)類(lèi)直接相互依賴(lài)為例沮尿,它們的實(shí)現(xiàn)代碼可能如下:

public class BeanB {
    private BeanA beanA;
    public void setBeanA(BeanA beanA) {
  this.beanA = beanA;
 }
}

public class BeanA {
    private BeanB beanB;
    public void setBeanB(BeanB beanB) {
        this.beanB = beanB;
 }
}

配置信息如下(用注解方式注入同理丛塌,只是為了方便理解较解,用了配置文件):

<bean id="beanA" class="priv.starfish.BeanA">
  <property name="beanB" ref="beanB"/>
</bean>

<bean id="beanB" class="priv.starfish.BeanB">
  <property name="beanA" ref="beanA"/>
</bean>

Spring 啟動(dòng)后,讀取如上的配置文件赴邻,會(huì)按順序先實(shí)例化 A印衔,但是創(chuàng)建的時(shí)候又發(fā)現(xiàn)它依賴(lài)了 B,接著就去實(shí)例化 B 姥敛,同樣又發(fā)現(xiàn)它依賴(lài)了 A 奸焙,這尼瑪咋整?無(wú)限循環(huán)呀

Spring “肯定”不會(huì)讓這種事情發(fā)生的彤敛,如前言我們說(shuō)的 Spring 實(shí)例化對(duì)象分兩步与帆,第一步會(huì)先創(chuàng)建一個(gè)原始對(duì)象,只是沒(méi)有設(shè)置屬性墨榄,可以理解為"半成品"—— 官方叫 A 對(duì)象的早期引用(EarlyBeanReference)玄糟,所以當(dāng)實(shí)例化 B 的時(shí)候發(fā)現(xiàn)依賴(lài)了 A, B 就會(huì)把這個(gè)“半成品”設(shè)置進(jìn)去先完成實(shí)例化袄秩,既然 B 完成了實(shí)例化阵翎,所以 A 就可以獲得 B 的引用,也完成實(shí)例化了播揪,這其實(shí)就是 Spring 解決循環(huán)依賴(lài)的思想贮喧。
image

<figcaption style="margin: 5px 0px 0px; padding: 0px; max-width: 100%; box-sizing: border-box !important; word-wrap: break-word !important; text-align: center; color: rgb(136, 136, 136); font-size: 14px;">有點(diǎn)懵逼</figcaption>

不理解沒(méi)關(guān)系,先有個(gè)大概的印象猪狈,然后我們從源碼來(lái)看下 Spring 具體是怎么解決的箱沦。

源碼解毒

代碼版本:5.0.16.RELEASE

在 Spring IOC 容器讀取 Bean 配置創(chuàng)建 Bean 實(shí)例之前, 必須對(duì)它進(jìn)行實(shí)例化。只有在容器實(shí)例化后雇庙,才可以從 IOC 容器里獲取 Bean 實(shí)例并使用谓形,循環(huán)依賴(lài)問(wèn)題也就是發(fā)生在實(shí)例化 Bean 的過(guò)程中的,所以我們先回顧下獲取 Bean 的過(guò)程疆前。

獲取 Bean 流程

Spring IOC 容器中獲取 bean 實(shí)例的簡(jiǎn)化版流程如下(排除了各種包裝和檢查的過(guò)程)

爛了大街的 Spring 循環(huán)依賴(lài)問(wèn)題寒跳,你以為自己就真會(huì)了嗎

大概的流程順序(可以結(jié)合著源碼看下,我就不貼了竹椒,貼太多的話童太,嘔~嘔嘔,想吐):

  1. 流程從 getBean 方法開(kāi)始胸完,getBean 是個(gè)空殼方法书释,所有邏輯直接到doGetBean 方法中
  2. transformedBeanNamename 轉(zhuǎn)換為真正的 beanNamename 可能是 FactoryBean& 字符開(kāi)頭或者有別名的情況,所以需要轉(zhuǎn)化下)
  3. 然后通過(guò) getSingleton(beanName) 方法嘗試從緩存中查找是不是有該實(shí)例 sharedInstance(單例在 Spring 的同一容器只會(huì)被創(chuàng)建一次赊窥,后續(xù)再獲取 bean爆惧,就直接從緩存獲取即可)
  4. 如果有的話,sharedInstance 可能是完全實(shí)例化好的 bean锨能,也可能是一個(gè)原始的 bean扯再,所以再經(jīng) getObjectForBeanInstance 處理即可返回
  5. 當(dāng)然 sharedInstance 也可能是 null芍耘,這時(shí)候就會(huì)執(zhí)行創(chuàng)建 bean 的邏輯,將結(jié)果返回

第三步的時(shí)候我們提到了一個(gè)緩存的概念熄阻,這個(gè)就是 Spring 為了解決單例的循環(huán)依賴(lài)問(wèn)題而設(shè)計(jì)的 三級(jí)緩存

/** Cache of singleton objects: bean name --> bean instance */
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

/** Cache of singleton factories: bean name --> ObjectFactory */
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);

/** Cache of early singleton objects: bean name --> bean instance */
private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);

這三級(jí)緩存的作用分別是:

  • singletonObjects:完成初始化的單例對(duì)象的 cache斋竞,這里的 bean 經(jīng)歷過(guò) 實(shí)例化->屬性填充->初始化 以及各種后置處理(一級(jí)緩存)

  • earlySingletonObjects:存放原始的 bean 對(duì)象(完成實(shí)例化但是尚未填充屬性和初始化),僅僅能作為指針提前曝光饺律,被其他 bean 所引用窃页,用于解決循環(huán)依賴(lài)的 (二級(jí)緩存)

  • singletonFactories:在 bean 實(shí)例化完之后,屬性填充以及初始化之前复濒,如果允許提前曝光脖卖,Spring 會(huì)將實(shí)例化后的 bean 提前曝光,也就是把該 bean 轉(zhuǎn)換成 beanFactory 并加入到 singletonFactories(三級(jí)緩存)

我們首先從緩存中試著獲取 bean巧颈,就是從這三級(jí)緩存中查找

protected Object getSingleton(String beanName, boolean allowEarlyReference) {
    // 從 singletonObjects 獲取實(shí)例畦木,singletonObjects 中的實(shí)例都是準(zhǔn)備好的 bean 實(shí)例,可以直接使用
    Object singletonObject = this.singletonObjects.get(beanName);
    //isSingletonCurrentlyInCreation() 判斷當(dāng)前單例bean是否正在創(chuàng)建中
    if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
        synchronized (this.singletonObjects) {
            // 一級(jí)緩存沒(méi)有砸泛,就去二級(jí)緩存找
            singletonObject = this.earlySingletonObjects.get(beanName);
            if (singletonObject == null && allowEarlyReference) {
                // 二級(jí)緩存也沒(méi)有十籍,就去三級(jí)緩存找
                ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
                if (singletonFactory != null) {
                    // 三級(jí)緩存有的話,就把他移動(dòng)到二級(jí)緩存,.getObject() 后續(xù)會(huì)講到
                    singletonObject = singletonFactory.getObject();
                    this.earlySingletonObjects.put(beanName, singletonObject);
                    this.singletonFactories.remove(beanName);
                }
            }
        }
    }
    return singletonObject;
}

如果緩存沒(méi)有的話唇礁,我們就要?jiǎng)?chuàng)建了勾栗,接著我們以單例對(duì)象為例,再看下創(chuàng)建 bean 的邏輯(大括號(hào)表示內(nèi)部類(lèi)調(diào)用方法):

爛了大街的 Spring 循環(huán)依賴(lài)問(wèn)題盏筐,你以為自己就真會(huì)了嗎
  1. 創(chuàng)建 bean 從以下代碼開(kāi)始围俘,一個(gè)匿名內(nèi)部類(lèi)方法參數(shù)(總覺(jué)得 Lambda 的方式可讀性不如內(nèi)部類(lèi)好理解)
if (mbd.isSingleton()) {
    sharedInstance = getSingleton(beanName, () -> {
        try {
            return createBean(beanName, mbd, args);
        }
        catch (BeansException ex) {
            destroySingleton(beanName);
            throw ex;
        }
    });
    bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
}

getSingleton() 方法內(nèi)部主要有兩個(gè)方法

public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
    // 創(chuàng)建 singletonObject
 singletonObject = singletonFactory.getObject();
    // 將 singletonObject 放入緩存
    addSingleton(beanName, singletonObject);
}
  1. getObject() 匿名內(nèi)部類(lèi)的實(shí)現(xiàn)真正調(diào)用的又是 createBean(beanName, mbd, args)

  2. 往里走,主要的實(shí)現(xiàn)邏輯在 doCreateBean方法琢融,先通過(guò) createBeanInstance 創(chuàng)建一個(gè)原始 bean 對(duì)象

  3. 接著 addSingletonFactory 添加 bean 工廠對(duì)象到 singletonFactories 緩存(三級(jí)緩存)

  4. 通過(guò) populateBean 方法向原始 bean 對(duì)象中填充屬性界牡,并解析依賴(lài),假設(shè)這時(shí)候創(chuàng)建 A 之后填充屬性時(shí)發(fā)現(xiàn)依賴(lài) B漾抬,然后創(chuàng)建依賴(lài)對(duì)象 B 的時(shí)候又發(fā)現(xiàn)依賴(lài) A宿亡,還是同樣的流程,又去 getBean(A)纳令,這個(gè)時(shí)候三級(jí)緩存已經(jīng)有了 beanA 的“半成品”挽荠,這時(shí)就可以把 A 對(duì)象的原始引用注入 B 對(duì)象(并將其移動(dòng)到二級(jí)緩存)來(lái)解決循環(huán)依賴(lài)問(wèn)題。這時(shí)候 getObject() 方法就算執(zhí)行結(jié)束了平绩,返回完全實(shí)例化的 bean

  5. 最后調(diào)用 addSingleton 把完全實(shí)例化好的 bean 對(duì)象放入 singletonObjects 緩存(一級(jí)緩存)中坤按,打完收工

Spring 解決循環(huán)依賴(lài)

建議搭配著“源碼”看下邊的邏輯圖,更好下飯

爛了大街的 Spring 循環(huán)依賴(lài)問(wèn)題馒过,你以為自己就真會(huì)了嗎

流程其實(shí)上邊都已經(jīng)說(shuō)過(guò)了,結(jié)合著上圖我們?cè)倏聪戮唧w細(xì)節(jié)酗钞,用大白話再捋一捋:

  1. Spring 創(chuàng)建 bean 主要分為兩個(gè)步驟腹忽,創(chuàng)建原始 bean 對(duì)象来累,接著去填充對(duì)象屬性和初始化
  2. 每次創(chuàng)建 bean 之前,我們都會(huì)從緩存中查下有沒(méi)有該 bean窘奏,因?yàn)槭菃卫谒荒苡幸粋€(gè)
  3. 當(dāng)我們創(chuàng)建 beanA 的原始對(duì)象后,并把它放到三級(jí)緩存中着裹,接下來(lái)就該填充對(duì)象屬性了领猾,這時(shí)候發(fā)現(xiàn)依賴(lài)了 beanB,接著就又去創(chuàng)建 beanB骇扇,同樣的流程摔竿,創(chuàng)建完 beanB 填充屬性時(shí)又發(fā)現(xiàn)它依賴(lài)了 beanA,又是同樣的流程少孝,不同的是继低,這時(shí)候可以在三級(jí)緩存中查到剛放進(jìn)去的原始對(duì)象 beanA,所以不需要繼續(xù)創(chuàng)建稍走,用它注入 beanB袁翁,完成 beanB 的創(chuàng)建
  4. 既然 beanB 創(chuàng)建好了,所以 beanA 就可以完成填充屬性的步驟了婿脸,接著執(zhí)行剩下的邏輯粱胜,閉環(huán)完成

這就是單例模式下 Spring 解決循環(huán)依賴(lài)的流程了。

但是這個(gè)地方狐树,不管是誰(shuí)看源碼都會(huì)有個(gè)小疑惑焙压,為什么需要三級(jí)緩存呢,我趕腳二級(jí)他也夠了呀

革命尚未成功褪迟,同志仍需努力

跟源碼的時(shí)候冗恨,發(fā)現(xiàn)在創(chuàng)建 beanB 需要引用 beanA 這個(gè)“半成品”的時(shí)候,就會(huì)觸發(fā)"前期引用"味赃,即如下代碼:

ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
if (singletonFactory != null) {
    // 三級(jí)緩存有的話掀抹,就把他移動(dòng)到二級(jí)緩存
    singletonObject = singletonFactory.getObject();
    this.earlySingletonObjects.put(beanName, singletonObject);
    this.singletonFactories.remove(beanName);
}

singletonFactory.getObject() 是一個(gè)接口方法,這里具體的實(shí)現(xiàn)方法在

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;
                // 這么一大段就這句話是核心心俗,也就是當(dāng)bean要進(jìn)行提前曝光時(shí)傲武,
                // 給一個(gè)機(jī)會(huì),通過(guò)重寫(xiě)后置處理器的getEarlyBeanReference方法城榛,來(lái)自定義操作bean
                // 值得注意的是揪利,如果提前曝光了,但是沒(méi)有被提前引用狠持,則該后置處理器并不生效!!!
                // 這也正式三級(jí)緩存存在的意義疟位,否則二級(jí)緩存就可以解決循環(huán)依賴(lài)的問(wèn)題
                exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName);
            }
        }
    }
    return exposedObject;
}

這個(gè)方法就是 Spring 為什么使用三級(jí)緩存,而不是二級(jí)緩存的原因喘垂,它的目的是為了后置處理甜刻,如果沒(méi)有 AOP 后置處理绍撞,就不會(huì)走進(jìn) if 語(yǔ)句,直接返回了 exposedObject 得院,相當(dāng)于啥都沒(méi)干傻铣,二級(jí)緩存就夠用了。

所以又得出結(jié)論祥绞,這個(gè)三級(jí)緩存應(yīng)該和 AOP 有關(guān)系非洲,繼續(xù)。

在 Spring 的源碼中g(shù)etEarlyBeanReference 是 SmartInstantiationAwareBeanPostProcessor 接口的默認(rèn)方法蜕径,真正實(shí)現(xiàn)這個(gè)方法的只有** AbstractAutoProxyCreator ** 這個(gè)類(lèi)两踏,用于提前曝光的 AOP 代理。

@Override
public Object getEarlyBeanReference(Object bean, String beanName) throws BeansException {
   Object cacheKey = getCacheKey(bean.getClass(), beanName);
   this.earlyProxyReferences.put(cacheKey, bean);
   // 對(duì)bean進(jìn)行提前Spring AOP代理
   return wrapIfNecessary(bean, beanName, cacheKey);
}

這么說(shuō)有點(diǎn)干丧荐,來(lái)個(gè)小 demo 吧缆瓣,我們都知道 Spring AOP、事務(wù)等都是通過(guò)代理對(duì)象來(lái)實(shí)現(xiàn)的虹统,而事務(wù)的代理對(duì)象是由自動(dòng)代理創(chuàng)建器來(lái)自動(dòng)完成的弓坞。也就是說(shuō) Spring 最終給我們放進(jìn)容器里面的是一個(gè)代理對(duì)象,而非原始對(duì)象车荔,假設(shè)我們有如下一段業(yè)務(wù)代碼:

@Service
public class HelloServiceImpl implements HelloService {
   @Autowired
   private HelloService helloService;

   @Override
   @Transactional
   public Object hello() {
      return "Hello JavaKeeper";
   }
}

此 Service 類(lèi)使用到了事務(wù)渡冻,所以最終會(huì)生成一個(gè) JDK 動(dòng)態(tài)代理對(duì)象 Proxy。剛好它又存在自己引用自己的循環(huán)依賴(lài)忧便,完美符合我們的場(chǎng)景需求族吻。

我們?cè)僮远x一個(gè)后置處理,來(lái)看下效果:

@Component
public class HelloProcessor implements SmartInstantiationAwareBeanPostProcessor {

 @Override
 public Object getEarlyBeanReference(Object bean, String beanName) throws BeansException {
  System.out.println("提前曝光了:"+beanName);
  return bean;
 }
}

可以看到珠增,調(diào)用方法棧中有我們自己實(shí)現(xiàn)的 HelloProcessor超歌,說(shuō)明這個(gè) bean 會(huì)通過(guò) AOP 代理處理。

image

再?gòu)脑创a看下這個(gè)自己循環(huán)自己的 bean 的創(chuàng)建流程:

protected Object doCreateBean( ... ){
 ...
 
 boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences && isSingletonCurrentlyInCreation(beanName));
    // 需要提前暴露(支持循環(huán)依賴(lài))蒂教,就注冊(cè)一個(gè)ObjectFactory到三級(jí)緩存
 if (earlySingletonExposure) { 
        // 添加 bean 工廠對(duì)象到 singletonFactories 緩存中巍举,并獲取原始對(duì)象的早期引用
  //匿名內(nèi)部方法 getEarlyBeanReference 就是后置處理器 
  // SmartInstantiationAwareBeanPostProcessor 的一個(gè)方法,
  // 它的功效為:保證自己被循環(huán)依賴(lài)的時(shí)候凝垛,即使被別的Bean @Autowire進(jìn)去的也是代理對(duì)象
  addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
 }

 // 此處注意:如果此處自己被循環(huán)依賴(lài)了  那它會(huì)走上面的getEarlyBeanReference懊悯,從而創(chuàng)建一個(gè)代理對(duì)象從  三級(jí)緩存轉(zhuǎn)移到二級(jí)緩存里
 // 注意此時(shí)候?qū)ο筮€在二級(jí)緩存里,并沒(méi)有在一級(jí)緩存梦皮。并且此時(shí)后續(xù)的這兩步操作還是用的 exposedObject炭分,它仍舊是原始對(duì)象~~~
 populateBean(beanName, mbd, instanceWrapper);
 exposedObject = initializeBean(beanName, exposedObject, mbd);

 // 因?yàn)槭聞?wù)的AOP自動(dòng)代理創(chuàng)建器在getEarlyBeanReference 創(chuàng)建代理后,initializeBean 就不會(huì)再重復(fù)創(chuàng)建了剑肯,二選一的)
     
 // 所以經(jīng)過(guò)這兩大步后捧毛,exposedObject 還是原始對(duì)象,通過(guò) getEarlyBeanReference 創(chuàng)建的代理對(duì)象還在三級(jí)緩存呢
 
 ...
 
 // 循環(huán)依賴(lài)校驗(yàn)
 if (earlySingletonExposure) {
        // 注意此處第二個(gè)參數(shù)傳的false,表示不去三級(jí)緩存里再去調(diào)用一次getObject()方法了~~~岖妄,此時(shí)代理對(duì)象還在二級(jí)緩存型将,所以這里拿出來(lái)的就是個(gè) 代理對(duì)象
  // 最后賦值給exposedObject  然后return出去,進(jìn)而最終被addSingleton()添加進(jìn)一級(jí)緩存里面去  
  // 這樣就保證了我們?nèi)萜骼?最終實(shí)際上是代理對(duì)象荐虐,而非原始對(duì)象~~~~~
  Object earlySingletonReference = getSingleton(beanName, false);
  if (earlySingletonReference != null) {
   if (exposedObject == bean) { 
    exposedObject = earlySingletonReference;
   }
  }
  ...
 }
 
}

自我解惑:

問(wèn):還是不太懂,為什么這么設(shè)計(jì)呢丸凭,即使有代理福扬,在二級(jí)緩存代理也可以吧 | 為什么要使用三級(jí)緩存呢?

我們?cè)賮?lái)看下相關(guān)代碼惜犀,假設(shè)我們現(xiàn)在是二級(jí)緩存架構(gòu)铛碑,創(chuàng)建 A 的時(shí)候,我們不知道有沒(méi)有循環(huán)依賴(lài)虽界,所以放入二級(jí)緩存提前暴露汽烦,接著創(chuàng)建 B,也是放入二級(jí)緩存莉御,這時(shí)候發(fā)現(xiàn)又循環(huán)依賴(lài)了 A撇吞,就去二級(jí)緩存找,是有礁叔,但是如果此時(shí)還有 AOP 代理呢牍颈,我們要的是代理對(duì)象可不是原始對(duì)象,這怎么辦琅关,只能改邏輯煮岁,在第一步的時(shí)候,不管3721涣易,所有 Bean 統(tǒng)統(tǒng)去完成 AOP 代理画机,如果是這樣的話,就不需要三級(jí)緩存了新症,但是這樣不僅沒(méi)有必要步氏,而且違背了 Spring 在結(jié)合 AOP 跟 Bean 的生命周期的設(shè)計(jì)。

所以 Spring “多此一舉”的將實(shí)例先封裝到 ObjectFactory 中(三級(jí)緩存)账劲,主要關(guān)鍵點(diǎn)在 getObject() 方法并非直接返回實(shí)例戳护,而是對(duì)實(shí)例又使用SmartInstantiationAwareBeanPostProcessor 的 getEarlyBeanReference 方法對(duì) bean 進(jìn)行處理,也就是說(shuō)瀑焦,當(dāng) Spring 中存在該后置處理器腌且,所有的單例 bean 在實(shí)例化后都會(huì)被進(jìn)行提前曝光到三級(jí)緩存中,但是并不是所有的 bean 都存在循環(huán)依賴(lài)榛瓮,也就是三級(jí)緩存到二級(jí)緩存的步驟不一定都會(huì)被執(zhí)行铺董,有可能曝光后直接創(chuàng)建完成,沒(méi)被提前引用過(guò),就直接被加入到一級(jí)緩存中精续。因此可以確保只有提前曝光且被引用的 bean 才會(huì)進(jìn)行該后置處理坝锰。

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) {
             // 三級(jí)緩存獲取,key=beanName value=objectFactory重付,objectFactory中存儲(chǔ)     //getObject()方法用于獲取提前曝光的實(shí)例
                ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
                if (singletonFactory != null) {
                    // 三級(jí)緩存有的話顷级,就把他移動(dòng)到二級(jí)緩存
                    singletonObject = singletonFactory.getObject();
                    this.earlySingletonObjects.put(beanName, singletonObject);
                    this.singletonFactories.remove(beanName);
                }
            }
        }
    }
    return singletonObject;
}


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");
   }
   // 添加 bean 工廠對(duì)象到 singletonFactories 緩存中,并獲取原始對(duì)象的早期引用
   //匿名內(nèi)部方法 getEarlyBeanReference 就是后置處理器
   // SmartInstantiationAwareBeanPostProcessor 的一個(gè)方法确垫,
   // 它的功效為:保證自己被循環(huán)依賴(lài)的時(shí)候弓颈,即使被別的Bean @Autowire進(jìn)去的也是代理對(duì)象~~~~  AOP自動(dòng)代理創(chuàng)建器此方法里會(huì)創(chuàng)建的代理對(duì)象~~~
   addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
}

再問(wèn):AOP 代理對(duì)象提前放入了三級(jí)緩存,沒(méi)有經(jīng)過(guò)屬性填充和初始化删掀,這個(gè)代理又是如何保證依賴(lài)屬性的注入的呢翔冀?

這個(gè)又涉及到了 Spring 中動(dòng)態(tài)代理的實(shí)現(xiàn),不管是cglib代理還是jdk動(dòng)態(tài)代理生成的代理類(lèi)披泪,代理時(shí)纤子,會(huì)將目標(biāo)對(duì)象 target 保存在最后生成的代理 proxy 中,當(dāng)調(diào)用proxy 方法時(shí)會(huì)回調(diào) h.invoke款票,而 h.invoke 又會(huì)回調(diào)目標(biāo)對(duì)象 target 的原始方法控硼。所有,其實(shí)在 AOP 動(dòng)態(tài)代理時(shí)徽职,原始 bean 已經(jīng)被保存在 提前曝光代理中了象颖,之后 原始 bean 繼續(xù)完成屬性填充和初始化操作。因?yàn)?AOP 代理$proxy中保存著 traget 也就是是 原始bean 的引用姆钉,因此后續(xù) 原始bean 的完善说订,也就相當(dāng)于Spring AOP中的 target 的完善,這樣就保證了 AOP 的屬性填充與初始化了潮瓶!

非單例循環(huán)依賴(lài)

看完了單例模式的循環(huán)依賴(lài)陶冷,我們?cè)倏聪路菃卫那闆r,假設(shè)我們的配置文件是這樣的:

<bean id="beanA" class="priv.starfish.BeanA" scope="prototype">
   <property name="beanB" ref="beanB"/>
</bean>

<bean id="beanB" class="priv.starfish.BeanB" scope="prototype">
   <property name="beanA" ref="beanA"/>
</bean>

啟動(dòng) Spring毯辅,結(jié)果如下:

Error creating bean with name 'beanA' defined in class path resource [applicationContext.xml]: Cannot resolve reference to bean 'beanB' while setting bean property 'beanB';

Error creating bean with name 'beanB' defined in class path resource [applicationContext.xml]: Cannot resolve reference to bean 'beanA' while setting bean property 'beanA';

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

對(duì)于 prototype 作用域的 bean埂伦,Spring 容器無(wú)法完成依賴(lài)注入,因?yàn)?Spring 容器不進(jìn)行緩存 prototype 作用域的 bean 思恐,因此無(wú)法提前暴露一個(gè)創(chuàng)建中的bean 沾谜。

原因也挺好理解的,原型模式每次請(qǐng)求都會(huì)創(chuàng)建一個(gè)實(shí)例對(duì)象胀莹,即使加了緩存基跑,循環(huán)引用太多的話,就比較麻煩了就描焰,所以 Spring 不支持這種方式媳否,直接拋出異常:

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

構(gòu)造器循環(huán)依賴(lài)

上文我們講的是通過(guò) Setter 方法注入的單例 bean 的循環(huán)依賴(lài)問(wèn)題,用 Spring 的小伙伴也都知道,依賴(lài)注入的方式還有構(gòu)造器注入篱竭、工廠方法注入的方式(很少使用)力图,那如果構(gòu)造器注入方式也有循環(huán)依賴(lài),可以搞不掺逼?

我們?cè)俑南麓a和配置文件

public class BeanA {
   private BeanB beanB;
   public BeanA(BeanB beanB) {
      this.beanB = beanB;
   }
}

public class BeanB {
 private BeanA beanA;
 public BeanB(BeanA beanA) {
  this.beanA = beanA;
 }
}
<bean id="beanA" class="priv.starfish.BeanA">
<constructor-arg ref="beanB"/>
</bean>

<bean id="beanB" class="priv.starfish.BeanB">
<constructor-arg ref="beanA"/>
</bean>

執(zhí)行結(jié)果吃媒,又是異常
image

看看官方給出的說(shuō)法

Circular dependencies
If you use predominantly constructor injection, it is possible to create an unresolvable circular dependency scenario.
For example: Class A requires an instance of class B through constructor injection, and class B requires an instance of class A through constructor injection. If you configure beans for classes A and B to be injected into each other, the Spring IoC container detects this circular reference at runtime, and throws a BeanCurrentlyInCreationException.
One possible solution is to edit the source code of some classes to be configured by setters rather than constructors. Alternatively, avoid constructor injection and use setter injection only. In other words, although it is not recommended, you can configure circular dependencies with setter injection.
Unlike the typical case (with no circular dependencies), a circular dependency between bean A and bean B forces one of the beans to be injected into the other prior to being fully initialized itself (a classic chicken-and-egg scenario).

大概意思是:

如果您主要使用構(gòu)造器注入,循環(huán)依賴(lài)場(chǎng)景是無(wú)法解決的吕喘。建議你用 setter 注入方式代替構(gòu)造器注入

其實(shí)也不是說(shuō)只要是構(gòu)造器注入就會(huì)有循環(huán)依賴(lài)問(wèn)題晓折,Spring 在創(chuàng)建 Bean 的時(shí)候默認(rèn)是按照自然排序來(lái)進(jìn)行創(chuàng)建的,我們暫且把先創(chuàng)建的 bean 叫主 bean兽泄,上文的 A 即主 bean,只要主 bean 注入依賴(lài) bean 的方式是 setter 方式漾月,依賴(lài) bean 的注入方式無(wú)所謂病梢,都可以解決,反之亦然

所以上文我們 AB 循環(huán)依賴(lài)問(wèn)題梁肿,只要 A 的注入方式是 setter 蜓陌,就不會(huì)有循環(huán)依賴(lài)問(wèn)題。

面試官問(wèn):為什么呢吩蔑?

Spring 解決循環(huán)依賴(lài)依靠的是 Bean 的“中間態(tài)”這個(gè)概念钮热,而這個(gè)中間態(tài)指的是已經(jīng)實(shí)例化,但還沒(méi)初始化的狀態(tài)烛芬。實(shí)例化的過(guò)程又是通過(guò)構(gòu)造器創(chuàng)建的隧期,如果 A 還沒(méi)創(chuàng)建好出來(lái),怎么可能提前曝光赘娄,所以構(gòu)造器的循環(huán)依賴(lài)無(wú)法解決仆潮,我一直認(rèn)為應(yīng)該先有雞才能有蛋。

小總結(jié) | 面試這么答

B 中提前注入了一個(gè)沒(méi)有經(jīng)過(guò)初始化的 A 類(lèi)型對(duì)象不會(huì)有問(wèn)題嗎遣臼?
雖然在創(chuàng)建 B 時(shí)會(huì)提前給 B 注入了一個(gè)還未初始化的 A 對(duì)象性置,但是在創(chuàng)建 A 的流程中一直使用的是注入到 B 中的 A 對(duì)象的引用,之后會(huì)根據(jù)這個(gè)引用對(duì) A 進(jìn)行初始化揍堰,所以這是沒(méi)有問(wèn)題的鹏浅。

Spring 是如何解決的循環(huán)依賴(lài)?
Spring 為了解決單例的循環(huán)依賴(lài)問(wèn)題屏歹,使用了三級(jí)緩存隐砸。其中一級(jí)緩存為單例池(singletonObjects),二級(jí)緩存為提前曝光對(duì)象(earlySingletonObjects)西采,三級(jí)緩存為提前曝光對(duì)象工廠(singletonFactories)凰萨。

假設(shè)A、B循環(huán)引用,實(shí)例化 A 的時(shí)候就將其放入三級(jí)緩存中胖眷,接著填充屬性的時(shí)候武通,發(fā)現(xiàn)依賴(lài)了 B,同樣的流程也是實(shí)例化后放入三級(jí)緩存珊搀,接著去填充屬性時(shí)又發(fā)現(xiàn)自己依賴(lài) A冶忱,這時(shí)候從緩存中查找到早期暴露的 A,沒(méi)有 AOP 代理的話境析,直接將 A 的原始對(duì)象注入 B囚枪,完成 B 的初始化后,進(jìn)行屬性填充和初始化劳淆,這時(shí)候 B 完成后链沼,就去完成剩下的 A 的步驟,如果有 AOP 代理沛鸵,就進(jìn)行 AOP 處理獲取代理后的對(duì)象 A括勺,注入 B,走剩下的流程曲掰。

為什么要使用三級(jí)緩存呢疾捍?二級(jí)緩存能解決循環(huán)依賴(lài)嗎?
如果沒(méi)有 AOP 代理栏妖,二級(jí)緩存可以解決問(wèn)題乱豆,但是有 AOP 代理的情況下,只用二級(jí)緩存就意味著所有 Bean 在實(shí)例化后就要完成 AOP 代理吊趾,這樣違背了 Spring 設(shè)計(jì)的原則宛裕,Spring 在設(shè)計(jì)之初就是通過(guò) AnnotationAwareAspectJAutoProxyCreator 這個(gè)后置處理器來(lái)在 Bean 生命周期的最后一步來(lái)完成 AOP 代理,而不是在實(shí)例化后就立馬進(jìn)行 AOP 代理趾徽。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末续滋,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子孵奶,更是在濱河造成了極大的恐慌疲酌,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,718評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件了袁,死亡現(xiàn)場(chǎng)離奇詭異朗恳,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)载绿,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,683評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門(mén)粥诫,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人崭庸,你說(shuō)我怎么就攤上這事怀浆∫昵簦” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 158,207評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵执赡,是天一觀的道長(zhǎng)镰踏。 經(jīng)常有香客問(wèn)我,道長(zhǎng)沙合,這世上最難降的妖魔是什么奠伪? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,755評(píng)論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮首懈,結(jié)果婚禮上绊率,老公的妹妹穿的比我還像新娘。我一直安慰自己究履,他們只是感情好滤否,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,862評(píng)論 6 386
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著最仑,像睡著了一般顽聂。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上盯仪,一...
    開(kāi)封第一講書(shū)人閱讀 50,050評(píng)論 1 291
  • 那天,我揣著相機(jī)與錄音蜜葱,去河邊找鬼全景。 笑死,一個(gè)胖子當(dāng)著我的面吹牛牵囤,可吹牛的內(nèi)容都是我干的爸黄。 我是一名探鬼主播,決...
    沈念sama閱讀 39,136評(píng)論 3 410
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼揭鳞,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼炕贵!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起野崇,我...
    開(kāi)封第一講書(shū)人閱讀 37,882評(píng)論 0 268
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤称开,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后乓梨,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體鳖轰,經(jīng)...
    沈念sama閱讀 44,330評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,651評(píng)論 2 327
  • 正文 我和宋清朗相戀三年扶镀,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了蕴侣。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,789評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡臭觉,死狀恐怖昆雀,靈堂內(nèi)的尸體忽然破棺而出辱志,到底是詐尸還是另有隱情,我是刑警寧澤狞膘,帶...
    沈念sama閱讀 34,477評(píng)論 4 333
  • 正文 年R本政府宣布揩懒,位于F島的核電站,受9級(jí)特大地震影響客冈,放射性物質(zhì)發(fā)生泄漏旭从。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,135評(píng)論 3 317
  • 文/蒙蒙 一场仲、第九天 我趴在偏房一處隱蔽的房頂上張望和悦。 院中可真熱鬧,春花似錦渠缕、人聲如沸鸽素。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,864評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)馍忽。三九已至,卻和暖如春燕差,著一層夾襖步出監(jiān)牢的瞬間遭笋,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,099評(píng)論 1 267
  • 我被黑心中介騙來(lái)泰國(guó)打工徒探, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留瓦呼,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,598評(píng)論 2 362
  • 正文 我出身青樓测暗,卻偏偏與公主長(zhǎng)得像央串,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子碗啄,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,697評(píng)論 2 351