
作者:小傅哥
部落格:https://bugstack.cn
沉澱、分享、成長,讓自己和他人都能有所收獲!😄
一、前言
延遲滿足能給你帶來什麼?
大學有四年時間,但幾乎所有人都是臨近畢業才發現找一份好工作費勁,尤其是我能非常熟悉的軟體開發行業,即使是畢業了還需要額外花錢到教育訓練機構,在學一遍程式設計技術才能出去找工作。好像在校這幾年壓根就沒學到什麼!
就我個人而言可能是因為上學期間喜歡程式設計,也從師哥、師姐那裡聽到一些關于畢業後找工作的不容易,也了解了一些社會上對程式員開發技能的要求級别。也就是得到了這些消息,又加上自己樂于折騰,我給自己定了一個每天都能完成的小目标:
紅塵世界幾個王,我自不服迎頭上。
日敲代碼兩百行,沖進世界五百強。
哈哈哈,就這麼每天兩百行代碼,一個月就是6千行,一年就是6萬行,三年後開始實習就有18萬行,一個應屆實習生有将近20萬行代碼的敲擊量,幾乎已經可以非常熟練的完成各類簡單的工作,在加上實習中對整個項目流程真正的斷鍊後,找一個
正經
的開發工作,還是很容易的。
而這時候找工作的容易,就來自于你一直以來的學習和沉澱,但如果你沒經過這些努力,可能等畢業後就會變得非常慌亂,最後沒辦法隻能去一些機構再學習一遍。
二、面試題
謝飛機,小記!
,以前感覺Spring沒啥,看過一篇getBean,我的天!
謝飛機:面試官,最近我看了 Spring 的 getBean 發現這裡好多東西,還有一個是要解決循環依賴的,這玩意面試有啥要問的嗎?
面試官:有哇,Spring 是如何解決循環依賴的?
謝飛機:嗯,通過三級緩存提前暴露對象解決的。
面試官:可以哈,那這三個緩存裡都存放了什麼樣的對象資訊呢?
謝飛機:一級緩存存放的是完整對象,也叫成品對象。二級緩存存放的是半成品對象,就是那些屬性還沒指派的對象。三級緩存存放的是
ObjectFactory<?>
類型的 lambda 表達式,就是這用于處理 AOP 循環依賴的。
面試官:可以呀,謝飛機有所準備嘛!那如果沒有三級緩存,隻有二級或者一級,能解決循環依賴嗎?
謝飛機:其實我看過資料了,可以解決,隻不過 Spring 要保證幾個事情,隻有一級緩存處理流程沒法拆分,複雜度也會增加,同時半成品對象可能會有空指針異常。而将半成品與成品對象分開,處理起來也更加優雅、簡單、易擴充。另外 Spring 的兩大特性中不僅有 IOC 還有 AOP,也就是基于位元組碼增強後的方法,該存放到哪,而三級緩存最主要,要解決的循環依賴就是對 AOP 的處理,但如果把 AOP 代理對象的建立提前,那麼二級緩存也一樣可以解決。但是,這就違背了 Spring 建立對象的原則,Spring 更喜歡把所有的普通 Bean 都初始化完成,在處理代理對象的初始化。
面試官:飛機,不錯嘛,這次了解了不少。那問個簡單的,你撸過循環依賴的解決方案?
謝飛機:哦哦,這沒有,沒實踐過!!!确實應該搞一下,試試。
三、什麼是循環依賴?
1. 問題描述
了解問題的本質再分析問題,往往更利于對問題有更深入的了解和研究。是以我們在分析 Spring 關于循環依賴的源碼之前,先要了解下什麼是循環依賴。
- 循環依賴分為三種,自身依賴于自身、互相循環依賴、多組循環依賴。
- 但無論循環依賴的數量有多少,循環依賴的本質是一樣的。就是你的完整建立依賴于我,而我的完整建立也依賴于你,但我們互相沒法解耦,最終導緻依賴建立失敗。
- 是以 Spring 提供了除了構造函數注入和原型注入外的,setter循環依賴注入解決方案。那麼我們也可以先來嘗試下這樣的依賴,如果是我們自己處理的話該怎麼解決。
2. 問題展現
public class ABTest {
public static void main(String[] args) {
new ClazzA();
}
}
class ClazzA {
private ClazzB b = new ClazzB();
}
class ClazzB {
private ClazzA a = new ClazzA();
}
- 這段代碼就是循環依賴最初的模樣,你中有我,我中有你,運作就報錯
java.lang.StackOverflowError
- 這樣的循環依賴代碼是沒法解決的,當你看到 Spring 中提供了 get/set 或者注解,這樣之是以能解決,首先是進行了一定的解耦。讓類的建立和屬性的填充分離,先建立出半成品Bean,再處理屬性的填充,完成成品Bean的提供。
3. 問題處理
在這部分的代碼中就一個核心目的,我們來自己解決一下循環依賴,方案如下:
public class CircleTest {
private final static Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
public static void main(String[] args) throws Exception {
System.out.println(getBean(B.class).getA());
System.out.println(getBean(A.class).getB());
}
private static <T> T getBean(Class<T> beanClass) throws Exception {
String beanName = beanClass.getSimpleName().toLowerCase();
if (singletonObjects.containsKey(beanName)) {
return (T) singletonObjects.get(beanName);
}
// 執行個體化對象入緩存
Object obj = beanClass.newInstance();
singletonObjects.put(beanName, obj);
// 屬性填充補全對象
Field[] fields = obj.getClass().getDeclaredFields();
for (Field field : fields) {
field.setAccessible(true);
Class<?> fieldClass = field.getType();
String fieldBeanName = fieldClass.getSimpleName().toLowerCase();
field.set(obj, singletonObjects.containsKey(fieldBeanName) ? singletonObjects.get(fieldBeanName) : getBean(fieldClass));
field.setAccessible(false);
}
return (T) obj;
}
}
class A {
private B b;
// ...get/set
}
class B {
private A a;
// ...get/set
}
- 這段代碼提供了 A、B 兩個類,互相有依賴。但在兩個類中的依賴關系使用的是 setter 的方式進行填充。也就是隻有這樣才能避免兩個類在建立之初不非得強依賴于另外一個對象。
-
,是整個解決循環依賴的核心内容,A 建立後填充屬性時依賴 B,那麼就去建立 B,在建立 B 開始填充時發現依賴于 A,但此時 A 這個半成品對象已經存放在緩存到getBean
中了,是以 B 可以正常建立,在通過遞歸把 A 也建立完整了。singletonObjects
展開說說,Spring Bean IOC、AOP 循環依賴
四、源碼分析
1. 說說細節
通過上面的例子我們大概了解到,A和B互相依賴時,A建立完後填充屬性B,繼續建立B,再填充屬性A時就可以從緩存中擷取了,如下:
那這個解決事循環依賴的事放到 Spring 中是什麼樣呢?展開細節!
雖然,解決循環依賴的核心原理一樣,但要放到支撐起整個 Spring 中 IOC、AOP 特性時,就會變得複雜一些,整個處理 Spring 循環依賴的過程如下;
- 以上就是關于 Spring 中對于一個有循環依賴的對象擷取過程,也就是你想要的
說說細節
- 乍一看是挺多流程,但是這些也基本是你在調試代碼時候必須經過的代碼片段,拿到這份執行流程,再調試就非常友善了。
2. 處理過程
關于本章節涉及到的案例源碼分析,已更新到 github:https://github.com/fuzhengwei/interview - interview-31
以下是單元測試中對AB依賴的擷取Bean操作,重點在于進入 getBean 的源碼跟進;
@Test
public void test_alias() {
BeanFactory beanFactory = new ClassPathXmlApplicationContext("spring-config.xml");
Bean_A bean_a = beanFactory.getBean("bean_a", Bean_A.class);
logger.info("擷取 Bean 通過别名:{}", bean_a.getBean_b());
}
org.springframework.beans.factory.support.AbstractBeanFactory.java
@Override
public <T> T getBean(String name, Class<T> requiredType) throws BeansException {
return doGetBean(name, requiredType, null, false);
}
- 從 getBean 進入後,擷取 bean 的操作會進入到 doGetBean。
- 之是以這樣包裝一層,是因為 doGetBean 有很多不同入參的重載方法,友善外部操作。
doGetBean 方法
protected <T> T doGetBean(
final String name, final Class<T> requiredType, final Object[] args, boolean typeCheckOnly)
throws BeansException {
// 從緩存中擷取 bean 執行個體
Object sharedInstance = getSingleton(beanName);
// mbd.isSingleton() 用于判斷 bean 是否是單例模式
if (mbd.isSingleton()) {
// 擷取 bean 執行個體
sharedInstance = getSingleton(beanName, new ObjectFactory<Object>() {
@Override
public Object getObject() throws BeansException {
try {
// 建立 bean 執行個體,createBean 傳回的 bean 執行個體化好的
return createBean(beanName, mbd, args);
}
catch (BeansException ex) {
destroySingleton(beanName);
throw ex;
}
}
});
// 後續的處理操作
bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
}
// ...
// 傳回 bean 執行個體
return (T) bean;
}
- 按照在源碼分析的流程圖中可以看到,這一部分是從 getSingleton 先判斷是否有執行個體對象,對于第一次進入是肯定沒有對象的,要繼續往下走。
- 在判斷 mbd.isSingleton() 單例以後,開始使用基于 ObjectFactory 包裝的方式建立 createBean,進入後核心邏輯是開始執行 doCreateBean 操作。
doCreateBean 方法
protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final Object[] args)
throws BeanCreationException {
// 建立 bean 執行個體,并将 bean 執行個體包裝到 BeanWrapper 對象中傳回
instanceWrapper = createBeanInstance(beanName, mbd, args);
// 添加 bean 工廠對象到 singletonFactories 緩存中
addSingletonFactory(beanName, new ObjectFactory<Object>() {
@Override
public Object getObject() throws BeansException {
// 擷取原始對象的早期引用,在 getEarlyBeanReference 方法中,會執行 AOP 相關邏輯。若 bean 未被 AOP 攔截,getEarlyBeanReference 原樣傳回 bean。
return getEarlyBeanReference(beanName, mbd, bean);
}
});
try {
// 填充屬性,解析依賴關系
populateBean(beanName, mbd, instanceWrapper);
if (exposedObject != null) {
exposedObject = initializeBean(beanName, exposedObject, mbd);
}
}
// 傳回 bean 執行個體
return exposedObject;
}
- 在 doCreateBean 方法中包括的内容較多,但核心主要是建立執行個體、加入緩存以及最終進行屬性填充,屬性填充就是把一個 bean 的各個屬性字段涉及到的類填充進去。
-
,建立 bean 執行個體,并将 bean 執行個體包裝到 BeanWrapper 對象中傳回createBeanInstance
-
,添加 bean 工廠對象到 singletonFactories 緩存中addSingletonFactory
-
,擷取原始對象的早期引用,在 getEarlyBeanReference 方法中,會執行 AOP 相關邏輯。若 bean 未被 AOP 攔截,getEarlyBeanReference 原樣傳回 bean。getEarlyBeanReference
-
,填充屬性,解析依賴關系。也就是從這開始去找尋 A 執行個體中屬性 B,緊接着去建立 B 執行個體,最後在傳回回來。populateBean
getSingleton 三級緩存
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
// 從 singletonObjects 擷取執行個體,singletonObjects 是成品 bean
Object singletonObject = this.singletonObjects.get(beanName);
// 判斷 beanName ,isSingletonCurrentlyInCreation 對應的 bean 是否正在建立中
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
synchronized (this.singletonObjects) {
// 從 earlySingletonObjects 中擷取提前曝光未成品的 bean
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null && allowEarlyReference) {
// 擷取相應的 bean 工廠
ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
if (singletonFactory != null) {
// 提前曝光 bean 執行個體,主要用于解決AOP循環依賴
singletonObject = singletonFactory.getObject();
// 将 singletonObject 放入緩存中,并将 singletonFactory 從緩存中移除
this.earlySingletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);
}
}
}
}
return (singletonObject != NULL_OBJECT ? singletonObject : null);
}
-
,從 singletonObjects 擷取執行個體,singletonObjects 是成品 beansingletonObjects.get(beanName)
-
,判斷 beanName ,isSingletonCurrentlyInCreation 對應的 bean 是否正在建立中isSingletonCurrentlyInCreation
-
,從 earlySingletonObjects 中擷取提前曝光未成品的 beanallowEarlyReference
-
,提前曝光 bean 執行個體,主要用于解決AOP循環依賴singletonFactory.getObject()
綜上,是一個處理循環依賴的代碼流程,這部分提取出來的内容主要為核心内容,并沒與長篇大論的全部拆取出來,大家在調試的時候會涉及的比較多,盡可能要自己根據流程圖操作調試幾遍。
3. 依賴解析
綜上從我們自己去嘗試解決循環依賴,學習了循環依賴的核心解決原理。又分析了 Spring 解決的循環依賴的處理過程以及核心源碼的分析。那麼接下來我們在總結下三級緩存分别不同的處理過程,算是一個總結,也友善大家了解。
1. 一級緩存能解決嗎?
- 其實隻有一級緩存并不是不能解決循環依賴,就像我們自己做的例子一樣。
- 但是在 Spring 中如果像我們例子裡那麼處理,就會變得非常麻煩,而且也可能會出現 NPE 問題。
- 是以如圖按照 Spring 中代碼處理的流程,我們去分析一級緩存這樣存放成品 Bean 的流程中,是不能解決循環依賴的問題的。因為 A 的成品建立依賴于 B,B的成品建立又依賴于 A,當需要補全B的屬性時 A 還是沒有建立完,是以會出現死循環。
2. 二級緩存能解決嗎?
- 有了二級緩存其實這個事處理起來就容易了,一個緩存用于存放成品對象,另外一個緩存用于存放半成品對象。
- A 在建立半成品對象後存放到緩存中,接下來補充 A 對象中依賴 B 的屬性。
- B 繼續建立,建立的半成品同樣放到緩存中,在補充對象的 A 屬性時,可以從半成品緩存中擷取,現在 B 就是一個完整對象了,而接下來像是遞歸操作一樣 A 也是一個完整對象了。
3. 三級緩存解決什麼?
- 有了二級緩存都能解決 Spring 依賴了,怎麼要有三級緩存呢。其實我們在前面分析源碼時也提到過,三級緩存主要是解決 Spring AOP 的特性。AOP 本身就是對方法的增強,是
類型的 lambda 表達式,而 Spring 的原則又不希望将此類類型的 Bean 前置建立,是以要存放到三級緩存中處理。ObjectFactory<?>
- 其實整體處理過程類似,唯獨是 B 在填充屬性 A 時,先查詢成品緩存、再查半成品緩存,最後在看看有沒有單例工程類在三級緩存中。最終擷取到以後調用 getObject 方法傳回代理引用或者原始引用。
- 至此也就解決了 Spring AOP 所帶來的三級緩存問題。本章節涉及到的 AOP 依賴有源碼例子,可以進行調試
五、總結
- 回顧本文基本以實際操作的例子開始,引導大家對循環依賴有一個整體的認識,也對它的解決方案可以上手的例子,這樣對後續的關于 Spring 對循環依賴的解決也就不會那麼陌生了。
- 通篇全文下來大家也可以看到,三級緩存并不是非必須不可,隻不過在滿足 Spring 自身建立的原則下,是必須的。如果你可以下載下傳 Spring 源碼對這部分代碼進行改動下,提前建立 AOP 對象儲存到緩存中,那麼二級緩存一樣可以解決循環依賴問題。
- 關于循環依賴可能并不是一個好的編碼方式,如果在自己的程式中還是要盡可能使用更合理的設計模式規避循環依賴,可能這些方式會增加代碼量,但在維護上會更加友善。當然這不是強制,可以根據你的需要而來。
六、系列推薦
- 你說,怎麼把Bean塞到Spring容器?
- Spring IOC 特性有哪些,不會讀不懂源碼!
- 關于 Spring 中 getBean 的全流程源碼解析
- 久等了,小傅哥的《重學Java設計模式》終于出版了,彩印&紙質!
- 一個Bug,讓我發現了 Java 界的.AJ(錐)!
公衆号:bugstack蟲洞棧 | 作者小傅哥多年從事一線網際網路 Java 開發的學習曆程技術彙總,旨在為大家提供一個清晰詳細的學習教程,側重點更傾向編寫Java核心内容。如果能為您提供幫助,請給予支援(關注、點贊、分享)!