1. 什么是循环依赖问题 ?
循环依赖问题是指对象与对象之间存在相互依赖关系,而且形成了一个闭环,导致两个或多个对象都无法准确的完成对象的创建和初始化。
两个对象间的循环依赖:
多个对象间的循环依赖 :
解决 Spring 中的循环依赖有两个前提条件:
- 存在相互依赖的 Bean 必须是单例的 Bean;
- 依赖注入 Bean 的方式不能是构造方法注入。
为什么要满足条件 1:
如果说相互依赖的 Bean 不是单例的,那么 A 需要 B,就得重新 new 一个 B,B 需要 A,就得重新 new 一个 A,新 new 出来的 A 又需要 B,新 new 出来的 B 又需要 A,又进入最初的步骤了,这样下去将会无穷尽也,就根本解决不了了,所以要求存在相互依赖的 Bean 必须是单例的 Bean。
为什么要满足条件 2:
在了解这个原因之前,必须得先了解 Bean 的生命周期,不太清楚的可以先去看看这篇博客:https://blog.csdn.net/xaiobit_hl/article/details/128025295
因为构造方法的执行时机太靠前了,如果依赖注入 Bean 的方式是构造方法注入,就会导致 A 需要 B 的时候,B 压根还没执行到属性赋值那一步,而 B 需要 A 的时候,A 也压根没执行到属性赋值这一步,这就是先有鸡还是先有蛋的问题了。(A 依赖 B 对象的时候,需要 DI 注入 B 对象到当前对象中,这一步在 Bean 的生命周期中也叫做属性赋值,而需要给 B 进行赋值,B 对象就得先执行完实例化、属性赋值、初始化这三个生命周期)
2. Spring 三级缓存如何解决循环依赖问题 ?
就拿两个对象间的循环依赖来举例:
首先单例对象一定需要存储下来,所以需要一个一级缓存来存储完全初始化好的对象:
A 和 B 相互依赖时, Bean 的执行流程:
A 和 B 相互依赖的执行流程(不牵扯 AOP):
- 实例化 A 对象;
- 对 A 的依赖对象 B 进行属性赋值,发现 B 对象还没有初始化好,就需要先初始化 B 对象;
- B 对象实例化;
- 对 B 的依赖对象 A 进行属性赋值,此时 A 还是一个半成品,还没有初始化好,于是 A 对象的引用地址赋值给 B 中的依赖对象;
- B 对象进行初始化,B 对象初始化完之后,A 对象也就属性赋值完毕;
- A 对象执行初始化了。
上述流程中,第 4 步,B 对象在给 A 对象进行属性赋值的时候,此时的 A 对象还是个半成品,那么 B 在给 A 属性进行属性赋值的时候,这个半成品也是需要进行存起来的,否则 B 对象后续的流程就无法执行下去了,这时候就有了二级缓存,二级缓存就是用来存放没有初始化好的对象。
本来有了一级缓存,二级缓存就可以解决循环依赖问题了,但是第三者 AOP 的出现,就导致故事变得复杂起来了。
【前置铺垫】程序中如果使用了 AOP(动态代理),那么 Spring 中存储的就是代理对象,而不是目标对象了,那么在构建代理对象的时候,一定是需要目标对象的,所以代理对象既不能存储在一级缓存,也不能存储在二级缓存,但是这个代理对象是一定要存储下来的,否则就变成多例的了,这就违背了解决循环依赖的前置条件 1,所以这时候就引入了三级缓存。(代理对象中存放的是工厂对象 FactoryBean,代理对象就是通过工厂对象生成的)
这三个缓存在源码中其实就是三个 map :
- singletonObjects:一级缓存(ConcurrentHashMap)
- singletonFactories:三级缓存(HashMap)
- earlySingletonObjects:二级缓存(ConcurrentHashMap)
为什么三级缓存使用 HashMap:(doCreateBean())
因为它里面放的是一个 lambda 表达式,它不是一个真正的对象,所以它就不怕线程安全问题。
所以 Spring 里面解决循环依赖的问题,就是引入了三级缓存来解决的。
程序中如果使用到了 AOP,那么前面 A 依赖 B,B 依赖 A 的执行流程就需要稍作改变:
上述流程执行完毕后,一级缓存中就有了 A 对象和 B 对象了,其他对象需要依赖这俩对象时,就可以从一级缓存中去取了。
上述流程 4 中,B 进行属性赋值时,寻找 A 对象的流程源码如下:
B 对象执行属性赋值时,寻找 A 对象流程:
先从一级缓存中找,没找到,就去二级缓存找,也没找到,就去三级缓存找,此时就会执行 A 的 lambda 表达式,此时 A 对象不管是代理对象还是目标对象,都会被晋级到二级缓存中(考虑性能),当其他对象中依赖 A 对象时,就不再需要从三级缓存中拿了,而是直接从二级缓存中取;
虽然循环依赖问题确实存在,也不可避免,但是 SpringBoot 3.0 以及 Spring framework 6.0 之后,默认情况下就关闭了对于循环依赖的一个支持了,也就是说在 Spring 高版本底下,如果存在循环依赖的问题,Spring 就会告诉你,你的项目中有循环依赖,项目启动失败。