Spring中的循环引用
1. 什么是 Spring 的循环依赖?
简单的来说就是 A 依赖 B 的同时,B 依赖 A。在创建 A 对象的同时需要使用 B 对象,在创建 B 对象的同时需要使用到 A 对象。如下代码所示:
@Component public class A { public A(){ System.out.println("A的构造方法执行了..."); } private B b; @Autowired public void setB(B b) { this.b = b; System.out.println("给A注入B"); } }
@Component public class B { public B(){ System.out.println("B的构造方法执行了..."); } private A a; @Autowired public void setA(A a) { this.a = a; System.out.println("给B注入了A"); } }
2. 出现循环依赖以后会有什么问题?
对象的创建过程会产生死循环,如下所示:
3. Spring 如何解决循环依赖的?
Spring 的 Bean 生命周期创建 bean 的过程回顾:
Spring 解决循环依赖是通过三级缓存,对应的三级缓存如下所示:
缓存 | 源码名称 | 作用 |
---|---|---|
一级缓存 | singletonObjects |
单例池;缓存已经经历了完整声明周期,已经初始化完成的 bean 对象。 |
二级缓存 | earlySingletonObjects |
缓存早期的 bean 对象。(生命周期还没有走完) |
三级缓存 | singletonFactories |
缓存的是 ObjectFactory 表示对象工厂,用来创建某个对象的。 |
一级缓存的作用:
一级缓存解决不了循环依赖问题。
二级缓存的作用:
如果要想打破上述的循环,就需要一个中间人的参与,这个中间人就是缓存。
步骤如下所示:
-
实例化 A 得到 A 的原始对象。
-
将 A 的原始对象存储到二级缓存(
earlySingletonObjects
)中。 -
需要注入 B,B 对象在一级缓存中不存在,此时实例化 B,得到原始对象 B。
-
将 B 的原始对象存储到二级缓存中。
-
需要注入 A,从二级缓存中获取 A 的原始对象。
-
B 对象创建成功。
-
将 B 对象加入到一级缓存中。
-
将 B 注入给 A,A 创建成功。
-
将 A 对象添加到一级缓存中。
三级缓存的作用:
从上面这个分析过程中可以得出,只需要一个缓存就能解决循环依赖了,那么为什么 Spring 中还需要 singletonFactories
?
基于上面的场景想一个问题:如果 A 的原始对象注入给 B 的属性之后,A 的原始对象进行了 AOP 产生一个代理对象,此时就会出现,对于 A 而言,它的 Bean 对象其实应该是 AOP 之后的代理对象,而 B 的 a 属性对应的并不是 AOP 之后的代理对象,这就产生了冲突。也就是说,最终单例池中存放的 A 对象(代理对象)和 B 依赖的 A 对象不是同一个。
所以在该场景下,上述提到的二级缓存就解决不了了。那这个时候 Spring 就利用了第三级缓存 singletonFactories
来解决这个问题。
singletonFactories
中存的是某个 beanName 对应的 ObjectFactory
,在 bean 的生命周期中,生成完原始对象之后,就会构造一个 ObjectFactory
存入 singletonFactories
中,后期其他的 Bean 可以通过调用该 ObjectFactory
对象的 getObject
方法获取对应的 Bean。
整体的解决循环依赖问题的思路如下所示:
步骤如下所示:
- 实例化 A,得到原始对象 A,并且同时生成一个原始对象 A 对应的
ObjectFactory
对象。 - 将
ObjectFactory
对象存储到三级缓存中。 - 需要注入 B,发现 B 对象在一级缓存和二级缓存都不存在,并且三级缓存中也不存在 B 对象所对应的
ObjectFactory
对象。 - 实例化 B,得到原始对象 B,并且同时生成一个原始对象 B 对应的
ObjectFactory
对象,然后将该ObjectFactory
对象也存储到三级缓存中。 - 需要注入 A,发现 A 对象在一级缓存和二级缓存都不存在,但是三级缓存中存在 A 对象所对应的
ObjectFactory
对象。 - 通过 A 对象所对应的
ObjectFactory
对象创建 A 对象的代理对象。 - 将 A 对象的代理对象存储到二级缓存中。
- 将 A 对象的代理对象注入给 B,B 对象执行后面的生命周期阶段,最终 B 对象创建成功。
- 将 B 对象存储到一级缓存中。
- 将 B 对象注入给 A,A 对象执行后面的生命周期阶段,最终 A 对象创建成功,将二级缓存的 A 的代理对象存储到一级缓存中。
注意:
- 后面的生命周期阶段会按照本身的逻辑进行 AOP,在进行 AOP 之前会判断是否已经进行了 AOP,如果已经进行了 AOP 就不会进行 AOP 操作了。
singletonFactories
: 缓存的是一个ObjectFactory
,主要用来去生成原始对象进行了 AOP 之后得到的代理对象, 在每个 Bean 的生成过程中,都会提前暴露一个工厂,这个工厂可能用到,也可能用不到,如果没有出现循环依赖依赖本 bean,那么这个工厂无用,本 bean 按照自己的生命周期执行,执行完后直接把本 bean 放入singletonObjects
中即可,如果出现了循环依赖了本 bean,则另外那个 bean 执行ObjectFactory
提交得到一个 AOP 之后的代理对象(如果没有 AOP,则直接得到一个原始对象)。
4. 只有一级缓存和三级缓存是否可行?
不行,每次从三级缓存中拿到 ObjectFactory
对象,执行 getObject()
方法又会产生新的代理对象,因为 A 是单例的,所有这里我们要借助二级缓存来解决这个问题,将执行了 objectFactory.getObject()
产生的对象放到二级缓存中去,后面去二级缓存中拿,没必要再执行一遍 objectFactory.getObject()
方法再产生一个新的代理对象,保证始终只有一个代理对象。
总结:所以如果没有 AOP 的话确实可以两级缓存就可以解决循环依赖的问题,如果加上 AOP,两级缓存时无法解决的,不可能每次执行 objectFactory.getObject()
方法都给我产生一个新的代理对象,所以还要借助另外一个缓存来保护产生的代理对象。
5. 构造方法出现了循环依赖怎么解决?
Spring 中大部分的循环依赖已经帮我们解决掉了,但是有一些循环依赖还需要我们程序员自己进行解决;如果构造方法出现了循环依赖可以在构造参数前面加 @Lazy
注解,就不会真正的注入真实对象,该注入对象会被延迟加载。此时注入的是一个代理对象。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全网最简单!3分钟用满血DeepSeek R1开发一款AI智能客服,零代码轻松接入微信、公众号、小程
· .NET 10 首个预览版发布,跨平台开发与性能全面提升
· 《HelloGitHub》第 107 期
· 全程使用 AI 从 0 到 1 写了个小工具
· 从文本到图像:SSE 如何助力 AI 内容实时呈现?(Typescript篇)