流行面试题:Spring循环依赖问题

作者:Vt

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

前言

Spring 如何解决的循环依赖,是近两年流行起来的一道Java 面试题。

其实笔者本人对这类框架源码题还是持一定的怀疑态度的。

如果笔者作为面试官,可能会问一些诸如 “如果注入的属性为 null,你会从哪几个方向去排查” 这些场景题

那么既然写了这篇文章,闲话少说,发车看看 Spring 是如何解决的循环依赖,以及带大家看清循环依赖的本质是什么。

正文

通常来说,如果问 Spring 内部如何解决循环依赖,一定是单默认的单例 Bean 中,属性互相引用的场景。

比如几个 Bean 之间的互相引用:


image

甚至自己 “循环” 依赖自己:

image

先说明前提:原型 (Prototype) 的场景是不支持循环依赖的,通常会走到AbstractBeanFactory类中下面的判断,抛出异常。

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

原因很好理解,创建新的 A 时,发现要注入原型字段 B,又创建新的 B 发现要注入原型字段 A...

这就套娃了, 你猜是先 StackOverflow 还是 OutOfMemory?

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

image

基于构造器的循环依赖,就更不用说了,官方文档都摊牌了,你想让构造器注入支持循环依赖,是不存在的,不如把代码改了。

那么默认单例的属性注入场景,Spring 是如何支持循环依赖的?

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 中。

闲话不说,都浓缩在图里。

image

上面的是一张 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” 了

image

这就是 “循环依赖” 的本质,而不是 “Spring 如何解决循环依赖”。

之所以要举这个例子,是发现一小部分盆友陷入了 “阅读源码的泥潭”,而忘记了问题的本质。

为了看源码而看源码,结果一直看不懂,却忘了本质是什么。

如果真看不懂,不如先写出基础版本,逆推 Spring 为什么要这么实现,可能效果会更好。

what?问题的本质居然是 two sum!

看完笔者刚才的代码有没有似曾相识?没错,和 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。如果我们陷入了某个 “牛角尖”,不妨跳出来看看,可能会更佳清晰哦。

微信搜索:【Java小咖秀】,回复”手册“,获取一份Java全级别攻城狮面试手册.pdf&Linux实战命令手册.pdf

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350