写在开头
学习 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 要解决循环依赖?
首先简单了解下,我们用 Spring 框架,它帮我们做了什么事情?总结性来说,六字真言:IoC 和 AOP。
由于 Spring 解决循环依赖是考虑到 IoC 和 AOP 相关知识了,所以这里我先提一下。
由于本文主要的核心是 Spring 的循环依赖处理,所以不会对 IoC 和 AOP 做详细的说明,想了解以后有机会再说 😶
IoC,主要是将对象的创建、管理都交给了 Spring 来管理,能够解决对象之间的耦合问题,对开发人员来说也是省时省力的。
AOP,主要是在不改变原有业务逻辑情况下,增强横切逻辑代码,也是解耦合,避免横切逻辑代码重复;也是对 OOP 的延续、补充。
既然类的实例化都交给了 Spring 来管理了,那么循环依赖 Spring 肯定也要考虑到怎么去处理(怎么总觉得有点像是废话 😶)。
🤯 解决循环依赖的方式
参考我们能想到的肯定是手动处理的方式,先将对象都 new 出来,然后进行 set 属性值,而 Spring 也是通过这样的形式来处理的(你说巧不巧?其实一点都不巧 🙃,后面再说为什么),其实 Spring 管理 Bean 的实例化底层其实是由反射实现的。
而我们实例化的方式也有好多种,比如通过构造函数,一次性将属性赋值,像下面这样
// 假设有学生这个类
public class Student {
private int id;
private String name;
public Student(int id, String name) {
this.id = id;
this.name = name;
}
}
// 通过构造器方式实例化并赋值
new Student(1, "Suremotoo");
但是使用构造器这样的方式,是无法解决循环依赖的!为什么不能呢?
我们还是以文中开头的 A 和 B 互相依赖来说, 要通过构造器的方式实现 A 的实例化,如下
new A(b);
😮 Wow,是不是发现问题了?要通过构造器的方式,首先要将属性值实例化出来啊!A 要依赖属性 b,就需要先将 B 实例化,可是 B 的实例化是不是还是需要依赖 A❗️ 这不就是文中开头描述的样子嘛,所以通过构造器的方式,Spring 也没有办法解决循环依赖。
我们使用 set 可以解决,那么 Spring 也使用 set 方式呢?答案是可以的。
既然底层是通过反射实现的,我们自己也用反射实现的话,大概思路是这样的(还是以 A 和 B 为例)
先实例化 A 类
再实例化 B 类
set B 类中的 a 属性
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 的初始化
现在是不是有点感觉了,先把狗骗进来,再杀 😬
Spring 如何解决循环依赖问题
先上个通俗的答案解释,三级缓存。
/**
* 单例对象的缓存: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 里获取,那么<u>仅仅使用这一个 singletonObjects</u>,可以吗?肯定不可以的。
首先 singletonObjects 存入的是完全初始化好的 Bean,可以拿来直接用的。
如果我们直接将未初始化完的 Bean 放在 singletonObjects 里面,注意,这个未初始化完的 Bean 极有可能会被其他的类拿去用,它都没完事呢,就被拿去造了,肯定要出事啊!
我们以 A 、 B、C 举栗子🌰
- 先实例化 A 类,叫 a
- 将 a 放入 singletonObjects 中(此时 a 中的 b 属性还是空的呢)
- C 类需要使用 A 类,去 singletonObjects 获取,且获取到了 a
- C 类使用 a,拿出 a 类的 b 属性,然后 NPE了.
诺,出事了吧,这下就不是解决循环依赖的问题了,反而设计就不对了。
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 中 Bean 的管理
要明白 Spring 中的循环依赖,首先得了解下 Spring 中 Bean 的生命周期。
被 Spring 管理的对象叫 Bean
这里不会对 Bean 的生命周期进行详细的描述,只是描述一下大概的过程,方便大家去理解循环依赖。
Spring 中 Bean 的生命周期,指的就是 Bean 从创建到销毁的一系列生命活动。
那么由 Spring 来管理 Bean,要经过的主要步骤有:
Spring 根据开发人员的配置,扫描哪些类由 Spring 来管理,并为每个类生成一个 BeanDefintion,里面封装了类的一些信息,如全限定类名、哪些属性、是否单例等等
根据 BeanDefintion 的信息,通过反射,去实例化 Bean(此时就是实例化但未初始化 的 Bean)
填充上述未初始化对象中的属性(依赖注入)
如果上述未初始化对象中的方法被 AOP 了,那么就需要生成代理类(也叫包装类)
最后将完成初始化的对象存入缓存中(此处缓存 Spring 里叫: singletonObjects),下次用从缓存获取 ok 了
如果没有涉及到 AOP,那么第四步就没有生成代理类,将第三步完成属性填充的对象存入缓存中。
二级缓存有什么问题?
如果 Bean 没有 AOP,那么用二级缓存其实没有什么问题的,一旦有上述生命周期中第四步,就会导致的一个问题。因为 AOP 处理后,往往是需要生成代理对象的,代理对象和原来的对象根本就不是 1 个对象。
以二级缓存的场景来说,假设 A 类的某个方法会被 AOP,过程就是这样的:
- 生成 a 的实例,然后放入缓存,a 需要 b
- 再生成 b ,填充 b 的时候,需要 a,从缓存中取到了 a,完成 b 的初始化;
- 紧接着 a 把初始化好的 b 拿过来用,完成 a 的属性填充和初始化
- 由于 A 类涉及到了 AOP,再然后 a 要生成一个代理类,这里就叫:代理 a 吧
结果就是: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),而且,这个表达式<u>并没有</u>执行。
那么 getEarlyBeanReference 具体做了什么事情?
核心就是两步:
第一步:根据 beanName 将它对应的实例化后且未初始化完的 Bean,存入 java Map<Object, Object> earlyProxyReferences = new ConcurrentHashMap<>(16);
第二步:生成该 Bean 对应的代理类返回
这个 earlyProxyReferences 其实就是用于记录哪些 Bean 执行过 AOP,防止后期再次对 Bean 进行 AOP
那么 getEarlyBeanReference 什么时候被触发,什么时候执行?
在二级缓存示例中,填充 B 的属性时候,需要 A,然后去缓存中拿 A,此时先去第三级缓存中去取 A,如果存在,此时就执行 getEarlyBeanReference 函数,然后该函数就会返回 A 对应的代理对象。
后续再将该代理对象放入第二级缓存中,也就是 java 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 的代理对象
🎉 附: 一个完整的 Spring 循环依赖时序图
时序图并不标准,但是方便大家去理解 🙃
在理解循环依赖的时候,整体是个递归,你要有种套中套、梦中梦的感觉
由于排版的原因,🤩🤩🤩 关注我的公众号: 浆果捕鼠草,发送关键字: 循环依赖时序图
即可获得超高清时序图!
🎉🎉🎉🎉另外特殊福利 🤩🤩🤩 关注我的公众号: 浆果捕鼠草,发送关键字: 循环依赖精美
即可获得本文的精美 PDF 版本哦!