做者:Vtjava
原文:https://juejin.im/post/5e927e...面試
Spring
如何解決的循環依賴,是近兩年流行起來的一道Java 面試題。算法
其實筆者本人對這類框架源碼題
仍是持必定的懷疑態度的。數組
若是筆者做爲面試官,可能會問一些諸如 「若是注入的屬性爲 null
,你會從哪幾個方向去排查」 這些場景題
。緩存
那麼既然寫了這篇文章,閒話少說,發車看看 Spring 是如何解決的循環依賴,以及帶你們看清循環依賴的本質是什麼。框架
一般來講,若是問 Spring 內部如何解決循環依賴,必定是單默認的單例 Bean 中,屬性互相引用的場景。post
好比幾個 Bean 之間的互相引用:網站
甚至本身 「循環」 依賴本身:spa
先說明前提:原型
(Prototype) 的場景是不支持
循環依賴的,一般會走到AbstractBeanFactory類中下面的判斷,拋出異常。code
if (isPrototypeCurrentlyInCreation(beanName)) { throw new BeanCurrentlyInCreationException(beanName); }
緣由很好理解,建立新的 A 時,發現要注入原型字段 B,又建立新的 B 發現要注入原型字段 A...
這就套娃了, 你猜是先 StackOverflow 仍是 OutOfMemory?
Spring 怕你很差猜,就先拋出了 BeanCurrentlyInCreationException
基於構造器的循環依賴,就更不用說了,官方文檔都攤牌了,你想讓構造器注入支持循環依賴,是不存在的,不如把代碼改了。
那麼默認單例的屬性注入場景,Spring
是如何支持循環依賴的?
首先,Spring 內部維護了三個 Map,也就是咱們一般說的三級緩存。
筆者翻閱 Spring 文檔卻是沒有找到三級緩存的概念,可能也是本土爲了方便理解的詞彙。
在 Spring 的DefaultSingletonBeanRegistry
類中,你會赫然發現類上方掛着這三個 Map:
singletonObjects 它是咱們最熟悉的朋友,俗稱 「單例池」「容器」,緩存建立完成單例 Bean 的地方。
singletonFactories
映射建立 Bean 的原始工廠
earlySingletonObjects 映射 Bean 的早期引用,也就是說在這個 Map 裏的 Bean 不是完整的,甚至還不能稱之爲 「Bean」,只是一個 Instance.
後兩個 Map 實際上是 「墊腳石」 級別的,只是建立 Bean 的時候,用來藉助了一下,建立完成就清掉了。
因此筆者前文對 「三級緩存」 這個詞有些迷惑,多是由於註釋都是以 Cache of 開頭吧。
爲何成爲後兩個 Map 爲墊腳石,假設最終放在 singletonObjects 的 Bean 是你想要的一杯 「涼白開」。
那麼 Spring 準備了兩個杯子,即 singletonFactories 和 earlySingletonObjects 來回 「倒騰」 幾番,把熱水晾成「涼白開」 放到 singletonObjects 中。
閒話不說,都濃縮在圖裏。
上面的是一張 GIF,若是你沒看到可能還沒加載出來。三秒一幀,不是你電腦卡。
筆者畫了 17 張圖簡化表述了 Spring 的主要步驟,GIF 上方便是剛纔提到的三級緩存,下方展現是主要的幾個方法。
固然了,這個地步你確定要結合 Spring 源碼來看,要不願定看不懂。
若是你只是想大概瞭解,或者面試,能夠先記住筆者上文提到的 「三級緩存」,以及下文即將要說的本質。
上文了解完 Spring 如何處理循環依賴以後,讓咱們跳出 「閱讀源碼」 的思惟,假設讓你實現一個有如下特色的功能,你會怎麼作?
將指定的一些類實例爲單例
類中的字段也都實例爲單例
支持循環依賴
舉個例子,假設有類 A:
public class A { private B b; } 類 B: public class B { private A a; }
說白了讓你模仿 Spring:僞裝 A 和 B 是被 @Component 修飾,
而且類中的字段僞裝是 @Autowired 修飾的,處理完放到 Map 中。
其實很是簡單,筆者寫了一份粗糙的代碼,可供參考:
/** * 放置建立好的bean Map */ private static Map<String, Object> cacheMap = new HashMap<>(2); public static void main(String[] args) { // 僞裝掃描出來的對象 Class[] classes = {A.class, B.class}; // 僞裝項目初始化實例化全部bean for (Class aClass : classes) { getBean(aClass); } // check System.out.println(getBean(B.class).getA() == getBean(A.class)); System.out.println(getBean(A.class).getB() == getBean(B.class)); } @SneakyThrows private static <T> T getBean(Class<T> beanClass) { // 本文用類名小寫 簡單代替bean的命名規則 String beanName = beanClass.getSimpleName().toLowerCase(); // 若是已是一個bean,則直接返回 if (cacheMap.containsKey(beanName)) { return (T) cacheMap.get(beanName); } // 將對象自己實例化 Object object = beanClass.getDeclaredConstructor().newInstance(); // 放入緩存 cacheMap.put(beanName, object); // 把全部字段當成須要注入的bean,建立並注入到當前bean中 Field[] fields = object.getClass().getDeclaredFields(); for (Field field : fields) { field.setAccessible(true); // 獲取須要注入字段的class Class<?> fieldClass = field.getType(); String fieldBeanName = fieldClass.getSimpleName().toLowerCase(); // 若是須要注入的bean,已經在緩存Map中,那麼把緩存Map中的值注入到該field便可 // 若是緩存沒有 繼續建立 field.set(object, cacheMap.containsKey(fieldBeanName) ? cacheMap.get(fieldBeanName) : getBean(fieldClass)); } // 屬性填充完成,返回 return (T) object; }
這段代碼的效果,其實就是處理了循環依賴,而且處理完成後,cacheMap 中放的就是完整的 「Bean」 了
這就是 「循環依賴」 的本質,而不是 「Spring 如何解決循環依賴」。
之因此要舉這個例子,是發現一小部分盆友陷入了 「閱讀源碼的泥潭」,而忘記了問題的本質。
爲了看源碼而看源碼,結果一直看不懂,卻忘了本質是什麼。
若是真看不懂,不如先寫出基礎版本,逆推 Spring 爲何要這麼實現,可能效果會更好。
看完筆者剛纔的代碼有沒有似曾相識?沒錯,和 two sum 的解題是相似的。
不知道 two sum 是什麼梗的,筆者和你介紹一下:
two sum 是刷題網站 leetcode 序號爲 1 的題,也就是大多人的算法入門的第一題。
經常被人調侃,有算法面的公司,被面試官欽定了,合的來。那就來一道 two sum 走走過場。
問題內容是:給定一個數組,給定一個數字。返回數組中能夠相加獲得指定數字的兩個索引。
好比:給定nums = [2, 7, 11, 15], target = 9
那麼要返回 [0, 1],由於2 + 7 = 9
這道題的優解是,一次遍歷 + HashMap:
class Solution { public int[] twoSum(int[] nums, int target) { Map<Integer, Integer> map = new HashMap<>(); for (int i = 0; i < nums.length; i++) { int complement = target - nums[i]; if (map.containsKey(complement)) { return new int[] { map.get(complement), i }; } map.put(nums[i], i); } throw new IllegalArgumentException("No two sum solution"); } } //做者:LeetCode //連接:https://leetcode-cn.com/problems/two-sum/solution/liang-shu-zhi-he-by-leetcode-2/ //來源:力扣(LeetCode)
先去 Map 中找須要的數字,沒有就將當前的數字保存在 Map 中,若是找到須要的數字,則一塊兒返回。
和筆者上面的代碼是否是同樣?
先去緩存裏找 Bean,沒有則實例化當前的 Bean 放到 Map,若是有須要依賴當前 Bean 的,就能從 Map 取到。
若是你是上文筆者提到的 「陷入閱讀源碼的泥潭」 的讀者,上文應該能夠幫助到你。
可能還有盆友有疑問,爲何一道 「two-sum」,Spring 處理的如此複雜?
這個想一想 Spring 支持多少功能就知道了,各類實例方式.. 各類注入方式.. 各類 Bean 的加載,校驗.. 各類 callback,aop 處理等等..
Spring 可不僅有依賴注入,一樣 Java 也不只是 Spring。若是咱們陷入了某個 「牛角尖」,不妨跳出來看看,可能會更佳清晰哦。
本文已經被 https://www.javaxks.com 收錄