在关于Spring的面试中,我们经常会被问到一个问题:Spring是如何解决循环依赖的问题的。 这个问题算是关于Spring的一个高频面试题,因为如果不刻意研读,相信即使读过源码,面试者也不一定能够一下子思考出个中奥秘。本文主要针对这个问题,对其实现原理进行深入分析!
举个例子
/**
* A 类,引入 B 类的属性 b
*/
public class A {
private B b;
}
/**
* B 类,引入 A 类的属性 a
*/
public class B {
private A a;
}
再看个简单的图:
像这样,创建 a 的时候需要依赖 b,那就创建 b,结果创建 b 的时候又需要依赖 a,那就创建 a,创建 a 的时候需要依赖 b,那就创建 b,结果创建 b 的时候又需要依赖 a ……
互相依赖何时了,死循环了吧? 诺,这就是循环依赖!
循环依赖其实不算个问题或者错误,我们实际在开发的时候,也可能会用到。 再拿最开始的 A 和 B 来说,我们手动使用的时候会用以下方式:
A a = new A();
B b = new B();
b.setA(a);
a.setB(b);
其实这样就解决了循环依赖,功能上是没有问题的,但是为什么 Spring 要解决循环依赖?
首先简单了解下,我们用 Spring 框架,它帮我们做了什么事情?总结上来说,六字真言:IoC 和 AOP。
由于 Spring 解决循环依赖是考虑到 IoC 和 AOP 相关知识了,所以这里我先提一下。
由于本文主要的核心是 Spring 的循环依赖处理,所以不会对 IoC 和 AOP 做详细的说明,想了解以后有机会再说
IoC,主要是将对象的创建、管理都交给了 Spring 来管理,能够解决对象之间的耦合问题,对开发人员来说也是省时省力的。
AOP,主要是在不改变原有业务逻辑情况下,增强横切逻辑代码,也是解耦合,避免横切逻辑代码重复;也是对 OOP 的延续、补充。
既然类的实例化都交给了 Spring 来管理了,那么循环依赖 Spring 肯定也要考虑到怎么去处理(怎么总觉得有点像是废话 )。
参考我们能想到的肯定是手动处理的方式,先将对象都 new 出来,然后进行 set 属性值,而 Spring 也是通过这样的形式来处理的(你说巧不巧?其实一点都不巧 ,后面再说为什么),其实 Spring 管理 Bean 的实例化底层其实是由反射实现的。
而我们实例化的方式也有好多种,比如通过构造函数,一次性将属性赋值,像下面这样
// 假设有这个类
public class Shop {
private int id;
private String name;
public Shop(int id, String name) {
this.id = id;
this.name = name;
}
}
// 通过构造器方式实例化并赋值
new Shop(1, "Suremotoo");
但是使用构造器这样的方式,是无法解决循环依赖的!为什么不能呢?
我们还是以文中开头的 A 和 B 互相依赖来说, 要通过构造器的方式实现 A 的实例化,如下
new A(b);
Wow,是不是发现问题了?要通过构造器的方式,首先要将属性值实例化出来啊!A 要依赖属性 b,就需要先将 B 实例化,可是 B 的实例化是不是还是需要依赖 A!这不就是文中开头描述的样子嘛,所以通过构造器的方式,Spring 也没有办法解决循环依赖。
我们使用 set 可以解决,那么 Spring 也使用 set 方式呢?答案是可以的。
既然底层是通过反射实现的,我们自己也用反射实现的话,大概思路是这样的(还是以 A 和 B 为例)
其实就是通过反射,实现以下代码
A a = new A();
B b = new B();
b.setA(a);
a.setB(b);
这里可以稍微说明一下,为什么这样可以?
A a = new A(),说明 A 只是实例化,还未初始化
同理,B b = new B() 也只是实例化,并未初始化
a.setB(b);, 对 a 的属性赋值,完成 a 的初始化
b.setA(a);, 对 b 的属性赋值,完成 b 的初始化
现在是不是有点感觉了,先把狗骗进来,再杀
先上个通俗的答案解释,三级缓存。
/**
* 单例对象的缓存:bean 名称——bean 实例,即:所谓的单例池。
* 表示已经经历了完整生命周期的 Bean 对象
* <b>第一级缓存</b>
*/
Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
/**
* 早期的单例对象的高速缓存:bean 名称——bean 实例。
* 表示 Bean 的生命周期还没走完(Bean 的属性还未填充)就把这个 Bean 存入该缓存中
* 也就是实例化但未初始化的 bean 放入该缓存里
* <b>第二级缓存</b>
*/
Map<String, Object> earlySingletonObjects = new HashMap<>(16);
/**
* 单例工厂的高速缓存:bean 名称——ObjectFactory。
* 表示存放生成 bean 的工厂
* <b>第三级缓存</b>
*/
Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
代码中注释可能不清晰,我再给贴一下三级缓存:
第一级缓存(也叫单例池):Map<String, Object> singletonObjects,存放已经经历了完整生命周期的 Bean 对象
第二级缓存:Map<String, Object> earlySingletonObjects,存放早期暴露出来的 Bean 对象,Bean 的生命周期未结束(属性还未填充完)
第三级缓存:Map<String, ObjectFactory<?>> singletonFactories,存放可以生成 Bean 的工厂
Spring 管理的 Bean 其实默认都是单例的,也就是说 Spring 将最终可以使用的 Bean 统一放入第一级缓存中,也就是 singletonObjects(单例池)里,以后凡是用到某个 Bean 了都从这里获取就行了。
既然都从 singletonObjects 里获取,那么仅仅使用这一个 singletonObjects,可以吗?肯定不可以的。 首先 singletonObjects 存入的是完全初始化好的 Bean,可以拿来直接用的。 如果我们直接将未初始化完的 Bean 放在 singletonObjects 里面,注意,这个未初始化完的 Bean 极有可能会被其他的类拿去用,它都没完事呢,就被拿去造了,肯定要出事啊!
我们以 A 、 B、C 举例子
诺,出事了吧,这下就不是解决循环依赖的问题了,反而设计就不对了。
NPE 就是 NullPointerException
再来回顾下循环依赖的问题:A→B→A→B……
说到底就是怎么打破这个循环,一级缓存不行,我们就再加一级,可以吗? 我们看个图
图中的缓存就是二级缓存
看完图,可能还会有疑惑,A 没初始化完成放入了缓存,那么 B 用的岂不是就是未完成的 A,是这样的没错! 在整个过程当中,A 是只有 1 个,而 B 那里的 A 只是 A 的引用,所以后面 A 完成了初始化,B 中的 A 自然也就完成了。这里就是文中前面提到的手动 setA,setB 那里,我再贴一下代码:
A a = new A();
B b = new B();
b.setA(a); // 这里设置 b 的属性 a,其实就是 a 的引用
a.setB(b); // 这里设置 a 的属性 b,此时的 b 已经完成了初始化,设置完 a 的属性, a 也就完成了初始化,那么对应的 b 也就完成了初始化
分析到这里呢,我们就会发现二级缓存就解决了循环依赖的问题了,可是为什么还要三级缓存呢? 这里就要说说 Spring 中 Bean 的生命周期。
要明白 Spring 中的循环依赖,首先得了解下 Spring 中 Bean 的生命周期。
被 Spring 管理的对象叫 Bean 这里不会对 Bean 的生命周期进行详细的描述,只是描述一下大概的过程,方便大家去理解循环依赖。
Spring 中 Bean 的生命周期,指的就是 Bean 从创建到销毁的一系列生命活动。
那么由 Spring 来管理 Bean,要经过的主要步骤有:
如果没有涉及到 AOP,那么第四步就没有生成代理类,将第三步完成属性填充的对象存入缓存中。
如果 Bean 没有 AOP,那么用二级缓存其实没有什么问题的,一旦有上述生命周期中第四步,就会导致的一个问题。因为 AOP 处理后,往往是需要生成代理对象的,代理对象和原来的对象根本就不是 1 个对象。
以二级缓存的场景来说,假设 A 类的某个方法会被 AOP,过程就是这样的:
结果就是:a 最终的产物是代理 a,那 b 中其实也应该用代理 a,而现在 b 中用的却是原始的 a 代理 a 和原始的 a 不是一个对象,现在这就有问题了。
二级缓存还是有问题,那就再加一层缓存,也就是第三级缓存:Map<String, ObjectFactory<?>> singletonFactories,在 bean 的生命周期中,创建完对象之后,就会构造一个这个对象对应的 ObjectFactory 存入 singletonFactories 中。
singletonFactories 中存的是某个 beanName 及对应的 ObjectFactory,这个 ObjectFactory 其实就是生成这个 Bean 的工厂。实际中,这个 ObjectFactory 是个 Lambda 表达式:() -> getEarlyBeanReference(beanName, mbd, bean),而且,这个表达式并没有执行。
核心就是两步:
第一步:根据 beanName 将它对应的实例化后且未初始化完的 Bean,存入 Map<Object, Object> earlyProxyReferences = new ConcurrentHashMap<>(16);
第二步:生成该 Bean 对应的代理类返回
这个 earlyProxyReferences其实就是用于记录哪些 Bean 执行过 AOP,防止后期再次对 Bean 进行 AOP
那么 getEarlyBeanReference 什么时候被触发,什么时候执行?
在二级缓存示例中,填充 B 的属性时候,需要 A,然后去缓存中拿 A,此时先去第三级缓存中去取 A,如果存在,此时就执行 getEarlyBeanReference 函数,然后该函数就会返回 A 对应的代理对象。
后续再将该代理对象放入第二级缓存中,也就是 Map<String, Object> earlySingletonObjects里。
此时就拿到的代理对象,也是未填充属性的,也就是仍然是未初始化完的对象。
如果直接放入第一级缓存,此时被其他类拿去使用,肯定有问题了。
那么什么时候放入第一级缓存?
这里需要再简单说下第二级缓存的作用,假如 A 经过第三级缓存,获得代理对象,这个代理对象仍然是未初始化完的!那么就暂时把这个代理对象放入第二级缓存,然后删除该代理对象原本在第三级缓存中的数据(确保后期不会每次都生成新的代理对象),后面其他类要用了 A,就去第二级缓存中找,就获取到了 A 的代理对象,而且都用的是同一个 A 的代理对象,这样后面只需要对这一个代理对象进行完善,其他引入该代理对象的类就都完善了。
再往后面,继续完成 A 的初始化,那么先判断 A 是否存在于 earlyProxyReferences 中, 存在就说明 A 已经经历过 AOP 了,就无须再次 AOP。那 A 的操作就转换从二级缓存中获取,把 A 的代理类拿出来,填充代理类的属性。
完成后再将 A 的代理对象加入到第一级缓存,再把它原本在第二级缓存中的数据删掉,确保后面还用到 A 地类,直接从第一级缓存中获取。
看个图理解下
说了这么多,总结下三级缓存:
第一级缓存(也叫单例池):Map<String, Object> singletonObjects,存放已经经历了完整生命周期的 Bean 对象
第二级缓存:Map<String, Object> earlySingletonObjects,存放早期暴露出来的 Bean 对象,Bean 的生命周期未结束(属性还未填充完),可能是代理对象,也可能是原始对象
第三级缓存:Map<String, ObjectFactory<?>> singletonFactories,存放可以生成 Bean 的工厂,工厂主要用来生成 Bean 的代理对象
?
如果你觉得这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙:
?
点赞,转发,有你们的 『点赞和评论』,才是我创造的动力。
原文:https://www.cnblogs.com/rxhh/p/14654477.html