理解循环依赖的前提是先要去理解Bean的生命周期,循环依赖问题就是出现在bean进行赋值时,Bean生命周期的流程我将在后续进行总结,首先简单说说Bean的生命周期:
● bean对象首先要进行实例化,也就是在堆中申请空间,使用createBeanInstance方法通过反射创建对象。
● 然后要对bean进行填充,也就是对它的属性进行赋值。属性分为自定义的属性和容器属性,自定义属性的赋值交给populateBean方法进行set赋值,而容器属性就要通过实现Aware接口,在innvokeAwareMethods方法里面进行统一进行赋值。
这里也就是循环依赖可能会出现的地方。
● 初始化
● 使用
● 销毁
来看看循环依赖是一种什么情况:
A对象中有B的属性b,而对象B有A的属性a,在A进行实例化后,就要进行赋值操作,但是A的赋值依赖于B,所以它就要去找内存中有没有B,发现没有B,所以就要创建一个B的实例,在B进行赋值时,发现需要A才能完成填充,而A又处于等待B的状态(是不是很像死锁),所以就会循环的互相依赖,这就叫循环依赖。
这里做一个小补充:在Bean的生命周期中,我们将对象会分为两类,已经进行过实例化和初始化的叫做成品对象,也就是单例对象,放入单例池中。将只进行过实例化未进行初始化的叫做半成品对象,放在半成品池中。
相关方法参考:
● createBeanInstance:实例化,其实也就是调用对象的构造方法实例化对象。
● populateBean:填充属性,这一步主要是对bean的依赖属性进行注入(@Autowired)。
● initializeBean:初始化,回到一些形如initMethod、InitializingBean等方法。
● getBean:先去单例池中寻找 A对象 是否存在,存在则直接返回,不存在则进入后续创建流程。
● createProxy:创建动态代理。
● getEarlyBeanReference:提前处理,在对象 实例化完成 后在工厂池中创建一个工厂,这个工厂会通过调用提前引用来创建代理对象。
● postProcessAfterInitialization:后置处理,在对象 初始化完成 后调用后置处理来创建代理对象
● 提前暴露:在实例化后,我们就可以将对象的引用放入缓存交给需要引用依赖的其他对象,这个过程就是提前暴露。
一级缓存
那么如何解决这个问题呢?
如果我们只持有一个对象的引用,能否在后续步骤中再对其进行赋值呢?
答案是可以的。
我们就在中间加上map结构的一级缓存。
过程如下:
1. 当进行实例化时,先将实例化的半成品A加入到缓存中。
2. 在给A赋值时,查看缓存中是否有B对象。
3. 没有则创建B对象,给B对象进行实例化。
4. 实例化后将B加入到缓存中。
5. 然后在对B的a对象赋值时,查看缓存中有A对象的半成品,赋值给a,这时B就是个成品了,再将B的成品加入到缓存中。
6. 然后回到A的赋值阶段,将成品B赋值给b,再将成品的A加入缓存。
(如下图)
一级缓存通过提前暴露对象就解决了循环依赖问题,但是没有实现划分思想,拿到的也是bean的原始引用,如果我们需要的是bean的代理对象怎么办?Spring里充斥了大量的动态代理模式的架构,典型的AOP就是动态代理模式实现的。
二级缓存
这样确实是解决了问题,但是如果这样的话各种成品和半成品冗杂在一起很乱,效率很低,那么我们可不可以将半成品和成品分开来进行缓存?
所以当我们利用二级缓存来进行处理:
二级缓存,也就是增加了一个半成品池来存放半成品对象。
创建时:对象一旦实例化完成【createBeanInstance("a")】,会先将对象放入半成品池中
对象引用时:会先找单例池,如果不存在则会去半成品池中寻找
二级缓存执行过程如下:
1. A实例化完成后,将实例化后的A对象放入半成品池中。
2. 为A对象填充属性(需要B属性,所以去单例池和半成品池寻找B,没有则创建)
3. 对B进行实例化,然后进行赋值,需要A属性,去实例池找A,发现没有,然后去半成品池中找A,找到了,将A对象填充到B属性中。
4. B填充和初始化完成,放入单例池(成品池)。
5. A对象填充, B属性存在【getBean:单例池中找到了】
6. A对象填充和初始化完成,放入单例池。
7. 将半成品池中的已实例完成的对象的半成品清除。
二级缓存确实是划分了不同的区块来执行相应的职责,据单一职责原理,符合了JAVA编程思想,但是还是没有解决AOP代理问题,拿到的还是原始引用。
三级缓存
所以我们使用加上了工厂池的三级缓存来解决解决循环依赖创建+AOP代理问题。
所谓的三级缓存就是在 单例池、半成品池 的基础上,增加了一个工厂池,里面存储的都是每个对象绑定的ObjectFactory(),此时的 单例池、半成品池 中 存储的 已经 不是对象本身,而是代理对象。
先来看看三级缓存:
名称 | 作用 |
singletonObjects | 一级缓存:存放完整的Bean |
earlySingletonObjects | 二级缓存:存放提前暴露的Bean,Bean不是完整的,为完成属性注入和执行初始化方法 |
singletonFactories | 三级缓存:存放的是Bean工厂,主要生产Bean,存放到二级缓存中 |
注意一些细节,一级缓存的大小为256,且类型为ConcurrentHashMap,三级缓存的val的泛型类型为ObjectFactory。
ObjectFactory()作用特点:
● 为了调用提前处理【getEarlyBeanReference】来执行创建动态代理【createProxy】
● 对象 实例化完成【createBeanInstance】,就会在工厂池中创建一个factory()
● factory()不一定会被执行,只有该对象在创建过程中,又被其他对象所引用才会被调用,从而执行createProxy
这里引用一下别的博客的图,博客链接会在下面放出。
AOP处理器实现了Bean处理器接口
AOP处理器创建代理对象会通过两个入口来调用创建动态代理(createProxy):
postProcessAfterInitialization:【后置处理】在对象 初始化完成 后调用后置处理来创建代理对象
getEarlyBeanReference:【提前处理】在对象 实例化完成 后在工厂池中创建一个工厂,这个工厂会通过调用提前引用来创建代理对象。
知道这些后我们来看看三级缓存下的执行过程:
1. A对象实例化完成后,先会在工厂池中放入一个factory(a)。
2. 对A进行填充,需要从单例池或者半成品池中获取对象B。
3. 没有找到B,执行B的创建流程。
4. B对象实例化,加入工厂池factory(b)。
5. 为B填充属性,要用到A对象。
6. 首先判断单例池和半成品池,没有发现A对象。
7. 从工厂池中调用factory(a),AOP切入,调用执行提前引用,创建A的动态代理。
8. 将A对象的代理放入半成品池,可以被获取到并进行填充。
9. B获取并且填充、初始化,AOP切入,调用执行后置处理,创建B的动态代理。
10 .将B对象的代理放入单例池,可以被获取到并进行填充。
11. A对象获取填充B属性(getBean:单例池中找到了B代理对象)。
12. A属性填充和初始化完成,AOP切入,调用执行后置处理。
13. 这时半成品池中已经有了A的代理,所以不需要执行createProxy来创建代理,会将半成品池中的A代理对象移入单例池(注意是移入,不是复制)。
14. 一旦对象进入单例池,则意味着创建流程已经全部完成,此时会将工厂池的相应factory()进行清理。
通过第三级缓存我们可以拿到可能经过包装的对象,解决对象代理封装的问题。
在实际使用中,要获取一个bean,先从一级缓存一直查找到三级缓存,缓存bean的时候是从三级到一级的顺序保存,并且缓存bean的过程中,三个缓存都是互斥的,只会保持bean在一个缓存中,而且,最终都会在一级缓存中。
最后聊聊各级缓存:
一级缓存singletonObjects
是完整的bean,它可以被外界任意使用,并且不会有歧义。
二级缓存earlySingletonObjects
是不完整的bean,没有完成初始化,它与singletonObjects
的分离主要是职责的分离以及边界划分,可以试想一个Map缓存里既有完整可使用的bean,也有不完整的,只能持有引用的bean,在复杂度很高的架构中,很容易出现歧义,并带来一些不可预知的错误。
三级缓存singletonFactories
,其职责就是包装一个bean,有回调逻辑,所以它的作用非常清晰,并且只能处于第三层。
问:为什么不在创建实例化后直接调用提前引用,而是要通过工厂方法调用?
答:所谓的提前引用指的是在创建的过程当中被引用。A在创建过程中,如果有人要去引用这个A,那么才会执行这个提前引用流程,否则这个factory(a)是不会被执行的,重点取决于是否被引用决定。
如有问题,欢迎指出。
参考文章:
(25条消息) Spring 三级缓存解决循环依赖原理分析_Fox_bert的博客-CSDN博客_spring三级缓存如何解决循环依赖
Spring Bean 循环依赖为什么需要三级缓存 - 我是属车的 - 博客园 (cnblogs.com)
参考视频:
某站马士兵教育spring源码。