流行面試題:Spring循環(huán)依賴問題

作者:Vt

原文:https://juejin.im/post/5e927e27f265da47c8012ed9

前言

Spring 如何解決的循環(huán)依賴,是近兩年流行起來的一道Java 面試題。

其實筆者本人對這類框架源碼題還是持一定的懷疑態(tài)度的。

如果筆者作為面試官,可能會問一些諸如 “如果注入的屬性為 null,你會從哪幾個方向去排查” 這些場景題

那么既然寫了這篇文章,閑話少說,發(fā)車看看 Spring 是如何解決的循環(huán)依賴,以及帶大家看清循環(huán)依賴的本質(zhì)是什么。

正文

通常來說,如果問 Spring 內(nèi)部如何解決循環(huán)依賴,一定是單默認(rèn)的單例 Bean 中,屬性互相引用的場景。

比如幾個 Bean 之間的互相引用:


image

甚至自己 “循環(huán)” 依賴自己:

image

先說明前提:原型 (Prototype) 的場景是不支持循環(huán)依賴的,通常會走到AbstractBeanFactory類中下面的判斷,拋出異常。

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

原因很好理解,創(chuàng)建新的 A 時,發(fā)現(xiàn)要注入原型字段 B,又創(chuàng)建新的 B 發(fā)現(xiàn)要注入原型字段 A...

這就套娃了, 你猜是先 StackOverflow 還是 OutOfMemory?

Spring 怕你不好猜,就先拋出了 BeanCurrentlyInCreationException

image

基于構(gòu)造器的循環(huán)依賴,就更不用說了,官方文檔都攤牌了,你想讓構(gòu)造器注入支持循環(huán)依賴,是不存在的,不如把代碼改了。

那么默認(rèn)單例的屬性注入場景,Spring 是如何支持循環(huán)依賴的?

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

首先,Spring 內(nèi)部維護了三個 Map,也就是我們通常說的三級緩存。

筆者翻閱 Spring 文檔倒是沒有找到三級緩存的概念,可能也是本土為了方便理解的詞匯。

在 Spring 的DefaultSingletonBeanRegistry類中,你會赫然發(fā)現(xiàn)類上方掛著這三個 Map:

singletonObjects 它是我們最熟悉的朋友,俗稱 “單例池”“容器”,緩存創(chuàng)建完成單例 Bean 的地方。

singletonFactories 映射創(chuàng)建 Bean 的原始工廠

earlySingletonObjects 映射 Bean 的早期引用,也就是說在這個 Map 里的 Bean 不是完整的,甚至還不能稱之為 “Bean”,只是一個 Instance.

后兩個 Map 其實是 “墊腳石” 級別的,只是創(chuàng)建 Bean 的時候,用來借助了一下,創(chuàng)建完成就清掉了。

所以筆者前文對 “三級緩存” 這個詞有些迷惑,可能是因為注釋都是以 Cache of 開頭吧。

為什么成為后兩個 Map 為墊腳石,假設(shè)最終放在 singletonObjects 的 Bean 是你想要的一杯 “涼白開”。

那么 Spring 準(zhǔn)備了兩個杯子,即 singletonFactories 和 earlySingletonObjects 來回 “倒騰” 幾番,把熱水晾成“涼白開” 放到 singletonObjects 中。

閑話不說,都濃縮在圖里。

image

上面的是一張 GIF,如果你沒看到可能還沒加載出來。三秒一幀,不是你電腦卡。

筆者畫了 17 張圖簡化表述了 Spring 的主要步驟,GIF 上方即是剛才提到的三級緩存,下方展示是主要的幾個方法。

當(dāng)然了,這個地步你肯定要結(jié)合 Spring 源碼來看,要不肯定看不懂。

如果你只是想大概了解,或者面試,可以先記住筆者上文提到的 “三級緩存”,以及下文即將要說的本質(zhì)。

循環(huán)依賴的本質(zhì)

上文了解完 Spring 如何處理循環(huán)依賴之后,讓我們跳出 “閱讀源碼” 的思維,假設(shè)讓你實現(xiàn)一個有以下特點的功能,你會怎么做?

將指定的一些類實例為單例

類中的字段也都實例為單例

支持循環(huán)依賴

舉個例子,假設(shè)有類 A:

public class A {
    private B b;
}
類 B:

public class B {
    private A a;
}

說白了讓你模仿 Spring:假裝 A 和 B 是被 @Component 修飾,
并且類中的字段假裝是 @Autowired 修飾的,處理完放到 Map 中。

其實非常簡單,筆者寫了一份粗糙的代碼,可供參考:

    /**
     * 放置創(chuàng)建好的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的命名規(guī)則
        String beanName = beanClass.getSimpleName().toLowerCase();
        // 如果已經(jīng)是一個bean,則直接返回
        if (cacheMap.containsKey(beanName)) {
            return (T) cacheMap.get(beanName);
        }
        // 將對象本身實例化
        Object object = beanClass.getDeclaredConstructor().newInstance();
        // 放入緩存
        cacheMap.put(beanName, object);
        // 把所有字段當(dāng)成需要注入的bean,創(chuàng)建并注入到當(dāng)前bean中
        Field[] fields = object.getClass().getDeclaredFields();
        for (Field field : fields) {
            field.setAccessible(true);
            // 獲取需要注入字段的class
            Class<?> fieldClass = field.getType();
            String fieldBeanName = fieldClass.getSimpleName().toLowerCase();
            // 如果需要注入的bean,已經(jīng)在緩存Map中,那么把緩存Map中的值注入到該field即可
            // 如果緩存沒有 繼續(xù)創(chuàng)建
            field.set(object, cacheMap.containsKey(fieldBeanName)
                    ? cacheMap.get(fieldBeanName) : getBean(fieldClass));
        }
        // 屬性填充完成,返回
        return (T) object;
    }

這段代碼的效果,其實就是處理了循環(huán)依賴,并且處理完成后,cacheMap 中放的就是完整的 “Bean” 了

image

這就是 “循環(huán)依賴” 的本質(zhì),而不是 “Spring 如何解決循環(huán)依賴”。

之所以要舉這個例子,是發(fā)現(xiàn)一小部分盆友陷入了 “閱讀源碼的泥潭”,而忘記了問題的本質(zhì)。

為了看源碼而看源碼,結(jié)果一直看不懂,卻忘了本質(zhì)是什么。

如果真看不懂,不如先寫出基礎(chǔ)版本,逆推 Spring 為什么要這么實現(xiàn),可能效果會更好。

what?問題的本質(zhì)居然是 two sum!

看完筆者剛才的代碼有沒有似曾相識?沒錯,和 two sum 的解題是類似的。

不知道 two sum 是什么梗的,筆者和你介紹一下:

two sum 是刷題網(wǎng)站 leetcode 序號為 1 的題,也就是大多人的算法入門的第一題。

常常被人調(diào)侃,有算法面的公司,被面試官欽定了,合的來。那就來一道 two sum 走走過場。

問題內(nèi)容是:給定一個數(shù)組,給定一個數(shù)字。返回數(shù)組中可以相加得到指定數(shù)字的兩個索引。

比如:給定nums = [2, 7, 11, 15], target = 9
那么要返回 [0, 1],因為2 + 7 = 9

這道題的優(yōu)解是,一次遍歷 + 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 中找需要的數(shù)字,沒有就將當(dāng)前的數(shù)字保存在 Map 中,如果找到需要的數(shù)字,則一起返回。

和筆者上面的代碼是不是一樣?

先去緩存里找 Bean,沒有則實例化當(dāng)前的 Bean 放到 Map,如果有需要依賴當(dāng)前 Bean 的,就能從 Map 取到。

結(jié)尾

如果你是上文筆者提到的 “陷入閱讀源碼的泥潭” 的讀者,上文應(yīng)該可以幫助到你。

可能還有盆友有疑問,為什么一道 “two-sum”,Spring 處理的如此復(fù)雜?
這個想想 Spring 支持多少功能就知道了,各種實例方式.. 各種注入方式.. 各種 Bean 的加載,校驗.. 各種 callback,aop 處理等等..

Spring 可不只有依賴注入,同樣 Java 也不僅是 Spring。如果我們陷入了某個 “牛角尖”,不妨跳出來看看,可能會更佳清晰哦。

微信搜索:【Java小咖秀】,回復(fù)”手冊“,獲取一份Java全級別攻城獅面試手冊.pdf&Linux實戰(zhàn)命令手冊.pdf

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容