spring三级缓存解决循环依赖问题详解
前言
这段时间阅读了spring IOC部分的源码。在学习过程中,自己有遇到过很多很问题,在上网查阅资料的时候,发现很难找到一份比较全面的解答。现在自己刚学习完,一方面出于对自己这段学习的一个总结,检验自己所学;另一方面也希望能把自己对Spring IOC这部分知识的理解分享出来,希望能对后面想要学习的spring 源码的人提供一点经验。
第一次写博客,感觉真的挺辛苦的!不仅是弄明白知识点,把自己的理解合适的表达出来也真的挺困难的!本文如有不足之处,还请大家多多指正!
一、循环依赖的种类
在spring中,循环依赖可以被划分为3类:
- 原型Bean产生的循环依赖;
- 单例bean产生的循环依赖:
- 构造器注入循环依赖;
- setter注入循环依赖;
首先,分为原型Bean产生的循环依赖和单例Bean产生的循环依赖。而单例Bean产生的循环依赖又可以细分为构造器注入的循环依赖和setter注入的循环依赖。在这三类中,只有setter注入的循环依赖可以被spring解决,其他的循环依赖都不会被解决,发生其他情况的循环依赖时,spring会直接抛出异常。
注意:其实这里还有一种依赖,在我们采用xml的形式配置bean时,可以设置depentsOn属性,通过这个属性设置的依赖,如果发生了循环依赖也是不能被解决的。
二、依赖注入
经过上面的分析,我们已经知道:spring能解决的循环依赖实际是基于单例Bean的setter注入循环依赖。那么为什么spring可以解决单例Bean的setter注入的循环依赖呢?在此之前我们首先需要明白什么叫依赖注入。
这里并不会推究依赖注入的理论,而为了方便大家理解解决循环依赖的原理,在这里简单说一下依赖注入究竟干了啥?
class A {
B b;
// 通过构造函数注入
public A(B b) {
this.b = b;
}
// 通过setter注入
public B setB(B b) {
this.b = b;
return this.b;
}
}
class B {
C c;
}
如上图,定义了两个类A和B。在A中定义了一个B类型的变量,可以理解为A依赖B。如果没有Spring的容器,那么我需要自己手动去把B类的对象通过构造方法或者setter方法配置到A的对象中。但是,现在spring容器可以帮我们完成这件事,不再需要我们手动去配置对象之间的依赖了。我们直接从容器中取出的A类型的实例就是已经包含有B类型对象的实例了。那么,spring怎么去做的呢?实际上也是基于我们手动实现的方式一样。要么在调用类的构造器的时候把获取到的依赖(B类型的实例对象)赋值给对应的属性,要么就是在之后调用setter方法对对应的属性进行赋值。以上就是依赖注入大概做的事情。
了解依赖注入大概是干啥的了。那么我们也能大致猜测循环依赖会在哪里发生了?不错,就是在执行构造方法或者调用setter方法进行属性填充的时候。准确来说,是在这前面一点点。因为,在执行构造函数或者setter方法的时候,spring必须先确保对应方法的参数的值已经获取到。也就是说,在spring执行
public A(B b) {
this.b = b;
}
或者执行
public B setB(B b) {
this.b = b;
return this.b;
}
方法前,spring必须先从容器中获取到B类型的对象实例。而如果容器中没有对应的实例,那么容器就会创建对应的实例。之后,同样是在B的实例对象进行依赖注入时,B的实例对象发现它需要先获取到A类型的实例对象。然而,现在A和B都处于创建之中,是获取不到被完整创建实例对象的。也就是,A和B永远也不会被创建完成。这就是循环依赖。如下图:
class A {
B b;
// 通过构造函数注入
public A(B b) {
this.b = b;
}
// 通过setter注入
public B setB(B b) {
this.b = b;
return this.b;
}
}
class B {
A a;
// 通过构造函数注入
public B(A a) {
this.a = a;
}
// 通过setter注入
public A setA(A a) {
this.a = a;
return this.a;
}}
三、Bean的生命周期与对象的生命周期
通过上面的分析,我们了解了依赖注入究竟干了什么,也通过依赖注入推理出了循环依赖是怎么发生的。但是,这对于我们了解怎么去解决循环依赖还是不够。想要知道spring是如何解决循环依赖的,需要我们对Bean的生命周期有一定的了解。
在这里,需要强调一下“bean的生命周期”和“对象的生命周期”两个概念。很多时候,我们可能会直接把两个概念等同起来。虽然这并不会在工作中给我们带来很大的影响。但是,如果我们能把这两个概念理清楚,能更好地帮助我们去理解spring。
对象的生命周期
对象是由Java虚拟机根据对应的类(Class)创建并在在Java虚拟机中的堆上为其分配空间。对象的生命周期如下:
字节码:Java编写的程序.Java文件被编译后得到字节码文件,Java虚拟机通过解释执行字节码文件来执行程序。对应Java程序中的.java文件,可以理解为类。
Class:这个单词的首字母是大写,代表着一个类型(Type)。加载到虚拟机中的字节码文件始终还是文本类型的数据,为了在内存中方便使用它,把这些数据抽象为Class类型的对象。比如,我们要把一个人的信息存进Java虚拟机,那么我们就建一个Person类型的对象来保存这些数据。
如上如,就是字节码与Class之间的区别。如果采用文本的形式,每次获取数据,我们都需要从头开始读取,但是,我们将对应的数据抽象为某种结构的数据之后,就能实现让我们方便地获取数据。对于字节码文件地具体格式,感兴趣地小伙伴可以自己去查阅相关的资料进行深入的学习。不在此处过多涉及。
实例化:为创建的对象在Java虚拟机中的对空间上分配空间,并设置对象的属性值为对应类型的默认值,如,属性为int类型,则其默认值为0;为对象则为null。
初始化:初始化属性字段,如果在属性字段有对应的赋值语句,会执行对应的赋值语句对对象进行初始化。如下图代码,初始化前,所有的属性字段都是对应类型的默认值,如年龄为0。执行实例化之后,所有属性字段都被赋值了。年龄变为了18。
使用 :初始化之后的对象就可以被正常使用了。
垃圾回收:Java对象的销毁是通过垃圾回收机制来实现的。具体原理就是,如果当前程序中不在有引用指向该对象,则该对象就会在下次垃圾回收的时候被回收掉。所以,由此,我们也可推理出spring是怎么销毁以及创建单例的对象的。实际就是管理对象的引用,只要保证指向单例对象的引用的数量始终大于等于1,那么这个对象就永远不会被销毁。如果要销毁该单例对象,则销毁掉指向它的引用,它在下次垃圾回收的时候救护被销毁。
Bean的生命周期
结合我们之前对依赖注入的分析,我们可以发现Java虚拟机为我们创建对象的时候并不会帮助我们完成依赖的注入。因此,依赖注入的行为应该是被封装在Bean生命周期中的。
如上图,上面的是Java对象对应的生命周期,下面的则是Bean对应的生命周期。
BeanDefinition:可以简单理解为Bean之间的依赖关系配置加上Class对象(对应.class文件)。不管spring怎么管理Bean,它最终还是通过Java虚拟机来生成创建对象。因此,在创建对象前,还是需要确保对应类的.class文件已经被加载进入虚拟机并生成了对应的Class对象。同时,从上面的图中,我们可以看见,相比于普通对象的生命周期,Bean的生命周期多出来了属性填充和初始化两个阶段。而这多出来的两个阶段实际就是为了处理Bean之间的依赖注入的问题,所以,BeanDefinition需要提供关于Bean之间依赖关系的配置信息。
实例化:这个过程对应在Java虚拟机创建对象过程中的实例化和初始化过程,不再详细赘述。但是要把Java虚拟机创建对象过程中的初始化概念与Bean创建过程中的初始化概念区分出来。但是这里是一个分界点,也是我们后面解决setter注入发生循环依赖的关键。
属性填充:这阶段主要是依据BeanDefinition中配置好的依赖关系,通过setter方法的方式注入属性对应的依赖。换句话,这里的注入的依赖式静态的,你在启动程序之前,是怎么配置的依赖关系,这就怎么去配置依赖关系
初始化:在完成属性填充以后,在使用对象之前,我们还可以有机会检查依赖整体的导入情况或者可以自定义配置来修改属性的值。这些功能的实现就是通过这一阶段来实现的。
使用:不同于Java虚拟机创建对象的流程,Bean的创建必须在初始化之后才能完全使用。
销毁:spring对于单例的销毁实际上就是销毁Spring容器中保存的对该单例对象的引用。但是,虽然在容器中已经没有对该对象的引用了,但是在程序可能还存在由对该单例对象的引用。因此,虽然容器销毁了它,但是,它却不一定被“销毁”了。只有等到程序中也不存在对该对象的引用时,可标识该对象确实“已经被销毁了”,但是,它实际上还没有被立即销毁。只有在下次垃圾回收之后,它才算时真正被销毁。
Spring解决循环依赖
spring解决循环依赖时充分利用了Bean的生命周期。在经过实例化以后,Java虚拟机就已经为创建的对象在堆上分配了空间。换句话说,这个时候对象就已经是存在了的。只是由于还没有经过属性填充和初始化,对象中对应属性字段的数据还不是我们需要的。因此,如果这个时候把这个对象暴露出去给其他对象使用的话,就可能因为数据不符合预期的问题而报错。因此,所有对象必须是在完全创建之后才能被其他对象使用。但是,反过来思考。在创建bean,进行属性填充的时候,我们只是为了先持有依赖的对象,以此来顺利完成当前对象的创建过程。也就是说,在创建Bean的过程中,正在创建的Bean并不会使用它的依赖bean。由此,我们就可以放心地把它依赖bean地引用提前暴露给他,这样就能打破依赖地循环,从而顺利地完成Bean创建。具体例子如下图:
如上图对象A和对象B两者之间循环依赖。在对象A实例化之后,就将对象A地引用暴露出来。之后在属性填充阶段,发现依赖对象B,于是,开始创建B对象,在B进行属性填充的时候,发现需要依赖A对象。于是,尝试从容器中获取A对象的引用。由于对象A提前暴露了自己的引用,所以,对象B可以从容器中获取到对象A的引用,并顺利完成创建。之后,对象A也可以获取到完整创建之后的对象B,并顺利完成自己的创建。
为什么要使用三级缓存
我们了解了spring解决基于单例bean的setter注入依赖的原理,现在看看为什么spring要使用到三级缓存。我们先了解一下三级缓存各自的作用。
/** Cache of singleton objects: bean name --> bean instance */
/** 一级缓存,存放完整的bean */
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
/** Cache of early singleton objects: bean name --> bean instance */
/** 二级缓存,存放提前暴露的bean,bean是不完整的,没有完成属性注入和执行初始化 */
private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);
/** Cache of singleton factories: bean name --> ObjectFactory */
/** 三级缓存,存放的是bean工厂,主要是生产bean,存放到二级缓存中 */
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
如上面的代码所示:
一级缓存:用于存储被完整创建了的bean。也就是完成了初始化之后,可以直接被其他对象使用的bean。
二级缓存:用于存储提前暴露的Bean。也就是刚实例化但是还没有进行初始化的Bean。这些Bean只能用于解决循环依赖的问题。因为还没有被初始化,对象里面的数据还不完整,无法被正常使用。所以,只能用于那些需要先持有这个Bean但不会使用这个Bean的对象,也就是正在创建过程中的对象了。
三级缓存:三级缓存存储的是工厂对象。工厂对象可以产生对象提前暴露的引用。在spring中,抽象工厂设计模式用到的地方有很多。我们不妨大胆假设一下它的作用:只有我们真正调用工厂对象的getObject()方法时,才会真正去执行创建对象的逻辑。讲到这里,有的小伙伴可能有点晕了。因为二级缓存就可以直接存储对象提前暴露的引用了。为什么还要一个储存工厂方法的三级缓存。那是因为三级缓存不是针对不同的循环依赖,而是针对有动态代理的循环依赖,同样是在填充属性阶段,如果依赖的是动态代理的对象,那么,我们需要提前暴露的就不是原来刚实例化的对象,而是这个对象的动态代理对象。但是,创建动态代理的成本是很高的,因此,我们使用工厂方法,在真正要获取动态代理对象的时候才去创建对象,将这种开销比较大的任务尽量延迟做,能尽量保证我们的性能。
总结
至此,从依赖出入到循环依赖的解决整个过程都已经被我们梳理清楚。现在还剩下一些问题可以供大家思考,帮助大家巩固知识?
- 单例对象分为构造器注入依赖和setter注入依赖,为什么构造器依赖不能被解决?
- 原型对象的循环依赖为什么不能解决?
- 三级缓存一定需要吗?不用三级缓存会有什么影响?
- 二级缓存用来干什么?不用二级缓存会有什么影响?