作者:愛寶貝丶
來源:my.oschina.net/zhangxufeng/blog/3096394

在關于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的核心思想,建議大家看下,
圖中的黑色箭頭表示一開始的方法呼叫走向,走到最后,回傳了Spring中快取的A物件之后,表示遞回呼叫回傳了,此時使用綠色的箭頭表示,
從圖中我們可以很清楚的看到,B物件的a屬性是在第三步中注入的半成品A物件,而A物件的b屬性是在第二步中注入的成品B物件,此時半成品的A物件也就變成了成品的A物件,因為其屬性已經設定完成了,一張圖搞懂Spring bean的完整生命周期,建議閱讀下,
2、原始碼講解
對于Spring處理回圈依賴問題的方式,我們這里通過上面的流程圖其實很容易就可以理解,需要注意的一個點就是,Spring是如何標記開始生成的A物件是一個半成品,并且是如何保存A物件的,
這里的標記作業Spring是使用ApplicationContext的屬性Set
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()方法的作用是嘗試創建目標物件,并且為該物件注入其所依賴的屬性,
關注微信公眾號:Java技術堆疊,在后臺回復:spring,可以獲取我整理的 N 篇最新 Spring 教程,都是干貨,
這里其實就是主干邏輯,我們前面圖中已經標明,在整個程序中會呼叫三次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的裝配作業的,
關注公眾號Java技術堆疊回復"面試"獲取我整理的2020最全面試題及答案,
推薦去我的博客閱讀更多:
1.Java JVM、集合、多執行緒、新特性系列教程
2.Spring MVC、Spring Boot、Spring Cloud 系列教程
3.Maven、Git、Eclipse、Intellij IDEA 系列工具教程
4.Java、后端、架構、阿里巴巴等大廠最新面試題
覺得不錯,別忘了點贊+轉發哦!
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/143463.html
標籤:Java
上一篇:萬字長文,62道Java核心面試題,一次性打包送給積極向上的你
下一篇:C++編程求助。江湖救急啊
