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 表示对象工厂,用来创建某个对象的。

一级缓存的作用:

一级缓存解决不了循环依赖问题。

二级缓存的作用:

如果要想打破上述的循环,就需要一个中间人的参与,这个中间人就是缓存。

步骤如下所示:

  1. 实例化 A 得到 A 的原始对象。

  2. 将 A 的原始对象存储到二级缓存(earlySingletonObjects)中。

  3. 需要注入 B,B 对象在一级缓存中不存在,此时实例化 B,得到原始对象 B。

  4. 将 B 的原始对象存储到二级缓存中。

  5. 需要注入 A,从二级缓存中获取 A 的原始对象。

  6. B 对象创建成功。

  7. 将 B 对象加入到一级缓存中。

  8. 将 B 注入给 A,A 创建成功。

  9. 将 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。

整体的解决循环依赖问题的思路如下所示:

步骤如下所示:

  1. 实例化 A,得到原始对象 A,并且同时生成一个原始对象 A 对应的 ObjectFactory 对象。
  2. ObjectFactory 对象存储到三级缓存中。
  3. 需要注入 B,发现 B 对象在一级缓存和二级缓存都不存在,并且三级缓存中也不存在 B 对象所对应的 ObjectFactory 对象。
  4. 实例化 B,得到原始对象 B,并且同时生成一个原始对象 B 对应的 ObjectFactory 对象,然后将该 ObjectFactory 对象也存储到三级缓存中。
  5. 需要注入 A,发现 A 对象在一级缓存和二级缓存都不存在,但是三级缓存中存在 A 对象所对应的 ObjectFactory 对象。
  6. 通过 A 对象所对应的 ObjectFactory 对象创建 A 对象的代理对象。
  7. 将 A 对象的代理对象存储到二级缓存中。
  8. 将 A 对象的代理对象注入给 B,B 对象执行后面的生命周期阶段,最终 B 对象创建成功。
  9. 将 B 对象存储到一级缓存中。
  10. 将 B 对象注入给 A,A 对象执行后面的生命周期阶段,最终 A 对象创建成功,将二级缓存的 A 的代理对象存储到一级缓存中。

注意:

  1. 后面的生命周期阶段会按照本身的逻辑进行 AOP,在进行 AOP 之前会判断是否已经进行了 AOP,如果已经进行了 AOP 就不会进行 AOP 操作了。
  2. 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 注解,就不会真正的注入真实对象,该注入对象会被延迟加载。此时注入的是一个代理对象。

posted @   是小陈呀  阅读(248)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 全网最简单!3分钟用满血DeepSeek R1开发一款AI智能客服,零代码轻松接入微信、公众号、小程
· .NET 10 首个预览版发布,跨平台开发与性能全面提升
· 《HelloGitHub》第 107 期
· 全程使用 AI 从 0 到 1 写了个小工具
· 从文本到图像:SSE 如何助力 AI 内容实时呈现?(Typescript篇)
点击右上角即可分享
微信分享提示