面試必殺技,講一講Spring中的迴圈依賴

程式設計師DMZ發表於2020-07-06

本系列文章:

聽說你還沒學Spring就被原始碼編譯勸退了?30+張圖帶你玩轉Spring編譯

讀原始碼,我們可以從第一行讀起

你知道Spring是怎麼解析配置類的嗎?

配置類為什麼要新增@Configuration註解?

談談Spring中的物件跟Bean,你知道Spring怎麼建立物件的嗎?

這篇文章,我們來談一談Spring中的屬性注入

Spring中AOP相關的API及原始碼解析,原來AOP是這樣子的

你知道Spring是怎麼將AOP應用到Bean的生命週期中的嗎?

推薦閱讀:

Spring官網閱讀 | 總結篇

Spring雜談

本系列文章將會帶你一行行的將Spring的原始碼吃透,推薦閱讀的文章是閱讀原始碼的基礎!

前言

Spring中的迴圈依賴一直是Spring中一個很重要的話題,一方面是因為原始碼中為了解決迴圈依賴做了很多處理,另外一方面是因為面試的時候,如果問到Spring中比較高階的問題,那麼迴圈依賴必定逃不掉。如果你回答得好,那麼這就是你的必殺技,反正,那就是面試官的必殺技,這也是取這個標題的原因,當然,本文的目的是為了讓你在之後的所有面試中能多一個必殺技,專門用來絕殺面試官!

本文的核心思想就是,

當面試官問:

“請講一講Spring中的迴圈依賴。”的時候,

我們到底該怎麼回答?

主要分下面幾點

  1. 什麼是迴圈依賴?
  2. 什麼情況下迴圈依賴可以被處理?
  3. Spring是如何解決的迴圈依賴?

同時本文希望糾正幾個目前業界內經常出現的幾個關於迴圈依賴的錯誤的說法

  1. 只有在setter方式注入的情況下,迴圈依賴才能解決(
  2. 三級快取的目的是為了提高效率(

OK,鋪墊已經做完了,接下來我們開始正文

什麼是迴圈依賴?

從字面上來理解就是A依賴B的同時B也依賴了A,就像下面這樣

image-20200705175322521

體現到程式碼層次就是這個樣子

@Component
public class A {
    // A中注入了B
	@Autowired
	private B b;
}

@Component
public class B {
    // B中也注入了A
	@Autowired
	private A a;
}

當然,這是最常見的一種迴圈依賴,比較特殊的還有

// 自己依賴自己
@Component
public class A {
    // A中注入了A
	@Autowired
	private A a;
}

雖然體現形式不一樣,但是實際上都是同一個問題----->迴圈依賴

什麼情況下迴圈依賴可以被處理?

在回答這個問題之前首先要明確一點,Spring解決迴圈依賴是有前置條件的

  1. 出現迴圈依賴的Bean必須要是單例
  2. 依賴注入的方式不能全是構造器注入的方式(很多部落格上說,只能解決setter方法的迴圈依賴,這是錯誤的)

其中第一點應該很好理解,第二點:不能全是構造器注入是什麼意思呢?我們還是用程式碼說話

@Component
public class A {
//	@Autowired
//	private B b;
	public A(B b) {

	}
}


@Component
public class B {

//	@Autowired
//	private A a;

	public B(A a){

	}
}

在上面的例子中,A中注入B的方式是通過構造器,B中注入A的方式也是通過構造器,這個時候迴圈依賴是無法被解決,如果你的專案中有兩個這樣相互依賴的Bean,在啟動時就會報出以下錯誤:

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?

為了測試迴圈依賴的解決情況跟注入方式的關係,我們做如下四種情況的測試

依賴情況 依賴注入方式 迴圈依賴是否被解決
AB相互依賴(迴圈依賴) 均採用setter方法注入
AB相互依賴(迴圈依賴) 均採用構造器注入
AB相互依賴(迴圈依賴) A中注入B的方式為setter方法,B中注入A的方式為構造器
AB相互依賴(迴圈依賴) B中注入A的方式為setter方法,A中注入B的方式為構造器

具體的測試程式碼跟簡單,我就不放了。從上面的測試結果我們可以看到,不是隻有在setter方法注入的情況下迴圈依賴才能被解決,即使存在構造器注入的場景下,迴圈依賴依然被可以被正常處理掉。

那麼到底是為什麼呢?Spring到底是怎麼處理的迴圈依賴呢?不要急,我們接著往下看

Spring是如何解決的迴圈依賴?

關於迴圈依賴的解決方式應該要分兩種情況來討論

  1. 簡單的迴圈依賴(沒有AOP)
  2. 結合了AOP的迴圈依賴

簡單的迴圈依賴(沒有AOP)

我們先來分析一個最簡單的例子,就是上面提到的那個demo

@Component
public class A {
    // A中注入了B
	@Autowired
	private B b;
}

@Component
public class B {
    // B中也注入了A
	@Autowired
	private A a;
}

通過上文我們已經知道了這種情況下的迴圈依賴是能夠被解決的,那麼具體的流程是什麼呢?我們一步步分析

首先,我們要知道Spring在建立Bean的時候預設是按照自然排序來進行建立的,所以第一步Spring會去建立A

與此同時,我們應該知道,Spring在建立Bean的過程中分為三步

  1. 例項化,對應方法:AbstractAutowireCapableBeanFactory中的createBeanInstance方法

  2. 屬性注入,對應方法:AbstractAutowireCapableBeanFactorypopulateBean方法

  3. 初始化,對應方法:AbstractAutowireCapableBeanFactoryinitializeBean

這些方法在之前原始碼分析的文章中都做過詳細的解讀了,如果你之前沒看過我的文章,那麼你只需要知道

  1. 例項化,簡單理解就是new了一個物件
  2. 屬性注入,為例項化中new出來的物件填充屬性
  3. 初始化,執行aware介面中的方法,初始化方法,完成AOP代理

基於上面的知識,我們開始解讀整個迴圈依賴處理的過程,整個流程應該是以A的建立為起點,前文也說了,第一步就是建立A嘛!

image-20200706092738559

建立A的過程實際上就是呼叫getBean方法,這個方法有兩層含義

  1. 建立一個新的Bean
  2. 從快取中獲取到已經被建立的物件

我們現在分析的是第一層含義,因為這個時候快取中還沒有A嘛!

呼叫getSingleton(beanName)

首先呼叫getSingleton(a)方法,這個方法又會呼叫getSingleton(beanName, true),在上圖中我省略了這一步

public Object getSingleton(String beanName) {
    return getSingleton(beanName, true);
}

getSingleton(beanName, true)這個方法實際上就是到快取中嘗試去獲取Bean,整個快取分為三級

  1. singletonObjects,一級快取,儲存的是所有建立好了的單例Bean
  2. earlySingletonObjects,完成例項化,但是還未進行屬性注入及初始化的物件
  3. singletonFactories,提前暴露的一個單例工廠,二級快取中儲存的就是從這個工廠中獲取到的物件

因為A是第一次被建立,所以不管哪個快取中必然都是沒有的,因此會進入getSingleton的另外一個過載方法getSingleton(beanName, singletonFactory)

呼叫getSingleton(beanName, singletonFactory)

這個方法就是用來建立Bean的,其原始碼如下:

public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
    Assert.notNull(beanName, "Bean name must not be null");
    synchronized (this.singletonObjects) {
        Object singletonObject = this.singletonObjects.get(beanName);
        if (singletonObject == null) {

            // ....
            // 省略異常處理及日誌
            // ....

            // 在單例物件建立前先做一個標記
            // 將beanName放入到singletonsCurrentlyInCreation這個集合中
            // 標誌著這個單例Bean正在建立
            // 如果同一個單例Bean多次被建立,這裡會丟擲異常
            beforeSingletonCreation(beanName);
            boolean newSingleton = false;
            boolean recordSuppressedExceptions = (this.suppressedExceptions == null);
            if (recordSuppressedExceptions) {
                this.suppressedExceptions = new LinkedHashSet<>();
            }
            try {
                // 上游傳入的lambda在這裡會被執行,呼叫createBean方法建立一個Bean後返回
                singletonObject = singletonFactory.getObject();
                newSingleton = true;
            }
            // ...
            // 省略catch異常處理
            // ...
            finally {
                if (recordSuppressedExceptions) {
                    this.suppressedExceptions = null;
                }
                // 建立完成後將對應的beanName從singletonsCurrentlyInCreation移除
                afterSingletonCreation(beanName);
            }
            if (newSingleton) {
                // 新增到一級快取singletonObjects中
                addSingleton(beanName, singletonObject);
            }
        }
        return singletonObject;
    }
}

上面的程式碼我們主要抓住一點,通過createBean方法返回的Bean最終被放到了一級快取,也就是單例池中。

那麼到這裡我們可以得出一個結論:一級快取中儲存的是已經完全建立好了的單例Bean

呼叫addSingletonFactory方法

如下圖所示:

image-20200706105535307

在完成Bean的例項化後,屬性注入之前Spring將Bean包裝成一個工廠新增進了三級快取中,對應原始碼如下:

// 這裡傳入的引數也是一個lambda表示式,() -> getEarlyBeanReference(beanName, mbd, bean)
protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
    Assert.notNull(singletonFactory, "Singleton factory must not be null");
    synchronized (this.singletonObjects) {
        if (!this.singletonObjects.containsKey(beanName)) {
            // 新增到三級快取中
            this.singletonFactories.put(beanName, singletonFactory);
            this.earlySingletonObjects.remove(beanName);
            this.registeredSingletons.add(beanName);
        }
    }
}

這裡只是新增了一個工廠,通過這個工廠(ObjectFactory)的getObject方法可以得到一個物件,而這個物件實際上就是通過getEarlyBeanReference這個方法建立的。那麼,什麼時候會去呼叫這個工廠的getObject方法呢?這個時候就要到建立B的流程了。

當A完成了例項化並新增進了三級快取後,就要開始為A進行屬性注入了,在注入時發現A依賴了B,那麼這個時候Spring又會去getBean(b),然後反射呼叫setter方法完成屬性注入。

image-20200706114501300

因為B需要注入A,所以在建立B的時候,又會去呼叫getBean(a),這個時候就又回到之前的流程了,但是不同的是,之前的getBean是為了建立Bean,而此時再呼叫getBean不是為了建立了,而是要從快取中獲取,因為之前A在例項化後已經將其放入了三級快取singletonFactories中,所以此時getBean(a)的流程就是這樣子了

image-20200706115959250

從這裡我們可以看出,注入到B中的A是通過getEarlyBeanReference方法提前暴露出去的一個物件,還不是一個完整的Bean,那麼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;
}

它實際上就是呼叫了後置處理器的getEarlyBeanReference,而真正實現了這個方法的後置處理器只有一個,就是通過@EnableAspectJAutoProxy註解匯入的AnnotationAwareAspectJAutoProxyCreator也就是說如果在不考慮AOP的情況下,上面的程式碼等價於:

protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
    Object exposedObject = bean;
    return exposedObject;
}

也就是說這個工廠啥都沒幹,直接將例項化階段建立的物件返回了!所以說在不考慮AOP的情況下三級快取有用嘛?講道理,真的沒什麼用,我直接將這個物件放到二級快取中不是一點問題都沒有嗎?如果你說它提高了效率,那你告訴我提高的效率在哪?

image-20200706124118108

那麼三級快取到底有什麼作用呢?不要急,我們先把整個流程走完,在下文結合AOP分析迴圈依賴的時候你就能體會到三級快取的作用!

到這裡不知道小夥伴們會不會有疑問,B中提前注入了一個沒有經過初始化的A型別物件不會有問題嗎?

答:不會

這個時候我們需要將整個建立A這個Bean的流程走完,如下圖:

image-20200706133018669

從上圖中我們可以看到,雖然在建立B時會提前給B注入了一個還未初始化的A物件,但是在建立A的流程中一直使用的是注入到B中的A物件的引用,之後會根據這個引用對A進行初始化,所以這是沒有問題的。

結合了AOP的迴圈依賴

之前我們已經說過了,在普通的迴圈依賴的情況下,三級快取沒有任何作用。三級快取實際上跟Spring中的AOP相關,我們再來看一看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;
}

如果在開啟AOP的情況下,那麼就是呼叫到AnnotationAwareAspectJAutoProxyCreatorgetEarlyBeanReference方法,對應的原始碼如下:

public Object getEarlyBeanReference(Object bean, String beanName) {
    Object cacheKey = getCacheKey(bean.getClass(), beanName);
    this.earlyProxyReferences.put(cacheKey, bean);
    // 如果需要代理,返回一個代理物件,不需要代理,直接返回當前傳入的這個bean物件
    return wrapIfNecessary(bean, beanName, cacheKey);
}

回到上面的例子,我們對A進行了AOP代理的話,那麼此時getEarlyBeanReference將返回一個代理後的物件,而不是例項化階段建立的物件,這樣就意味著B中注入的A將是一個代理物件而不是A的例項化階段建立後的物件。image-20200706161709829

看到這個圖你可能會產生下面這些疑問

  1. 在給B注入的時候為什麼要注入一個代理物件?

答:當我們對A進行了AOP代理時,說明我們希望從容器中獲取到的就是A代理後的物件而不是A本身,因此把A當作依賴進行注入時也要注入它的代理物件

  1. 明明初始化的時候是A物件,那麼Spring是在哪裡將代理物件放入到容器中的呢?

image-20200706160542584

在完成初始化後,Spring又呼叫了一次getSingleton方法,這一次傳入的引數又不一樣了,false可以理解為禁用三級快取,前面圖中已經提到過了,在為B中注入A時已經將三級快取中的工廠取出,並從工廠中獲取到了一個物件放入到了二級快取中,所以這裡的這個getSingleton方法做的時間就是從二級快取中獲取到這個代理後的A物件。exposedObject == bean可以認為是必定成立的,除非你非要在初始化階段的後置處理器中替換掉正常流程中的Bean,例如增加一個後置處理器:

@Component
public class MyPostProcessor implements BeanPostProcessor {
	@Override
	public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
		if (beanName.equals("a")) {
			return new A();
		}
		return bean;
	}
}

不過,請不要做這種騷操作,徒增煩惱!

  1. 初始化的時候是對A物件本身進行初始化,而容器中以及注入到B中的都是代理物件,這樣不會有問題嗎?

答:不會,這是因為不管是cglib代理還是jdk動態代理生成的代理類,內部都持有一個目標類的引用,當呼叫代理物件的方法時,實際會去呼叫目標物件的方法,A完成初始化相當於代理物件自身也完成了初始化

  1. 三級快取為什麼要使用工廠而不是直接使用引用?換而言之,為什麼需要這個三級快取,直接通過二級快取暴露一個引用不行嗎?

答:這個工廠的目的在於延遲對例項化階段生成的物件的代理,只有真正發生迴圈依賴的時候,才去提前生成代理物件,否則只會建立一個工廠並將其放入到三級快取中,但是不會去通過這個工廠去真正建立物件

我們思考一種簡單的情況,就以單獨建立A為例,假設AB之間現在沒有依賴關係,但是A被代理了,這個時候當A完成例項化後還是會進入下面這段程式碼:

// A是單例的,mbd.isSingleton()條件滿足
// allowCircularReferences:這個變數代表是否允許迴圈依賴,預設是開啟的,條件也滿足
// isSingletonCurrentlyInCreation:正在在建立A,也滿足
// 所以earlySingletonExposure=true
boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
                                  isSingletonCurrentlyInCreation(beanName));
// 還是會進入到這段程式碼中
if (earlySingletonExposure) {
	// 還是會通過三級快取提前暴露一個工廠物件
    addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
}

看到了吧,即使沒有迴圈依賴,也會將其新增到三級快取中,而且是不得不新增到三級快取中,因為到目前為止Spring也不能確定這個Bean有沒有跟別的Bean出現迴圈依賴。

假設我們在這裡直接使用二級快取的話,那麼意味著所有的Bean在這一步都要完成AOP代理。這樣做有必要嗎?

不僅沒有必要,而且違背了Spring在結合AOP跟Bean的生命週期的設計!Spring結合AOP跟Bean的生命週期本身就是通過AnnotationAwareAspectJAutoProxyCreator這個後置處理器來完成的,在這個後置處理的postProcessAfterInitialization方法中對初始化後的Bean完成AOP代理。如果出現了迴圈依賴,那沒有辦法,只有給Bean先建立代理,但是沒有出現迴圈依賴的情況下,設計之初就是讓Bean在生命週期的最後一步完成代理而不是在例項化後就立馬完成代理。

三級快取真的提高了效率了嗎?

現在我們已經知道了三級快取的真正作用,但是這個答案可能還無法說服你,所以我們再最後總結分析一波,三級快取真的提高了效率了嗎?分為兩點討論:

  1. 沒有進行AOP的Bean間的迴圈依賴

從上文分析可以看出,這種情況下三級快取根本沒用!所以不會存在什麼提高了效率的說法

  1. 進行了AOP的Bean間的迴圈依賴

就以我們上的A、B為例,其中A被AOP代理,我們先分析下使用了三級快取的情況下,A、B的建立流程

image-20200706171514327

假設不使用三級快取,直接在二級快取中

image-20200706172523258

上面兩個流程的唯一區別在於為A物件建立代理的時機不同,在使用了三級快取的情況下為A建立代理的時機是在B中需要注入A的時候,而不使用三級快取的話在A例項化後就需要馬上為A建立代理然後放入到二級快取中去。對於整個A、B的建立過程而言,消耗的時間是一樣的

綜上,不管是哪種情況,三級快取提高了效率這種說法都是錯誤的!

總結

面試官:”Spring是如何解決的迴圈依賴?“

答:Spring通過三級快取解決了迴圈依賴,其中一級快取為單例池(singletonObjects),二級快取為早期曝光物件earlySingletonObjects,三級快取為早期曝光物件工廠(singletonFactories)。當A、B兩個類發生迴圈引用時,在A完成例項化後,就使用例項化後的物件去建立一個物件工廠,並新增到三級快取中,如果A被AOP代理,那麼通過這個工廠獲取到的就是A代理後的物件,如果A沒有被AOP代理,那麼這個工廠獲取到的就是A例項化的物件。當A進行屬性注入時,會去建立B,同時B又依賴了A,所以建立B的同時又會去呼叫getBean(a)來獲取需要的依賴,此時的getBean(a)會從快取中獲取,第一步,先獲取到三級快取中的工廠;第二步,呼叫物件工工廠的getObject方法來獲取到對應的物件,得到這個物件後將其注入到B中。緊接著B會走完它的生命週期流程,包括初始化、後置處理器等。當B建立完後,會將B再注入到A中,此時A再完成它的整個生命週期。至此,迴圈依賴結束!

面試官:”為什麼要使用三級快取呢?二級快取能解決迴圈依賴嗎?“

答:如果要使用二級快取解決迴圈依賴,意味著所有Bean在例項化後就要完成AOP代理,這樣違背了Spring設計的原則,Spring在設計之初就是通過AnnotationAwareAspectJAutoProxyCreator這個後置處理器來在Bean生命週期的最後一步來完成AOP代理,而不是在例項化後就立馬進行AOP代理。

一道思考題

為什麼在下表中的第三種情況的迴圈依賴能被解決,而第四種情況不能被解決呢?

提示:Spring在建立Bean時預設會根據自然排序進行建立,所以A會先於B進行建立

依賴情況 依賴注入方式 迴圈依賴是否被解決
AB相互依賴(迴圈依賴) 均採用setter方法注入
AB相互依賴(迴圈依賴) 均採用構造器注入
AB相互依賴(迴圈依賴) A中注入B的方式為setter方法,B中注入A的方式為構造器
AB相互依賴(迴圈依賴) B中注入A的方式為setter方法,A中注入B的方式為構造器

如果本文對你由幫助的話,記得點個贊吧!也歡迎關注我的公眾號,微信搜尋:程式設計師DMZ,或者掃描下方二維碼,跟著我一起認認真真學Java,踏踏實實做一個coder。

公眾號

我叫DMZ,一個在學習路上匍匐前行的小菜鳥!
碼字不易,本文要是對你有幫助的話,記得點個贊吧!

相關文章