做者: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 收录