当前位置: 首页 > news >正文

什么是 spring 的循环依赖?

什么是 spring 的循环依赖?


首先,认识一下什么是循环依赖,举个例子:A 对象被 Spring 管理,并且引入的 B 对象,同样的 B 对象也被 Spring 管理,并且也引入的 A 对象。这种相互被引用的情况,就是所谓的循环依赖

@Component
public class A {
	@Autowired
	private B b;
}
@Component
public class B {
	@Autowired
	private A a;
}

我们都知道,Spring 是将对象给管理起来,这些对象默认还都是单例的,需要的话从 Spring 中直接取出即可。

1. Spring 是如何存储这些 Bean 呢?

Spring 通过 Map 结构将对象给缓存起来,这里的 Map 其实是分为三个:

  1. 一级缓存(singletonObjects):此缓存中的对象是已经完全创建好的,可以直接使用的 Bean;

    Map<String, Object> singletonObjects = new ConcurrentHashMap<>();
    
  2. 二级缓存(earlySingletonObjects):此缓存中存储尚未完全初始化但已经创建了对象实例的 Bean(即提前暴露的 Bean 实例)

    Map<String, Object> earlySingletonObjects = new HashMap<>();
    
  3. 三级缓存(singletonFactories):比较特殊,存放的是 ObjectFactory,这是一个工厂,等到从缓存中取时,会执行其中的 getObject() 方法,来得到代理对象

    Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>()
    

    这样设计是为了在有 AOP 的情况下,可以返回代理对象,而且也能满足循环依赖。

2. 循环依赖的创建过程

  1. 当对象 a 被创建时,就会在缓存中进行查询,先从一级缓存中进行查询,如果没有,接着再从二级缓存中进行查询,如果依旧没用,最后从第三级缓存中查询,如果还是没有,就接着执行后续操作;

  2. 由于缓存中没有 a 对象,就要回到主流程,执行 a 对象的创建

    1. 利用反射创建对象
    2. 这时候就要用到三级缓存,将创建出的对象包装成 ObjectFctory 类型,放到三级缓存中
  3. 填充 a 对象的属性(Bean 的引入也在此执行)

    其中要引入 b 对象,同样也要执行与 a 对象创建相同的流程:

    1. 查询缓存

    2. b 对象的创建:利用反射创建 b 对象,并存入三级缓存中

    3. 填充 b 对象的属性

      此时又要引入 a 对象,由于已经将 a 存储到缓存中,因此这里要执行一些额外的操作(后面说)后,将得到的 a 对象填充到 b 对象中

    4. 初始化

    5. 缓存转移(具体步骤后面说

  4. a 对象初始化

  5. 缓存转移

总体流程大致如下:

3. 补充说明

3.1 在 b 对象创建中填充属性时,从缓存中读取 a 对象要经过什么操作?

参考源码,读取关键部分(红框位置):


首先从三级缓存中获取到 a 对象,由于这个缓存里存放的是 ObjectFactory 类型,并不是真正的对象,这里就要执行 getObject() 方法,从而创建出真正要使用的对象,将得到的真正的对象 a 存入二级缓存中,并将三级缓存中的 a 删除。

3.2 缓存转移的步骤是什么?

此时 b 已经完全创建完毕,所以要将缓存里面的对象进行转移,参考源码:


可以分析出具体操作为:

  1. 将 b 的完整对象放到一级缓存中
  2. 将三级缓存中的 b 移除掉
  3. 将二级缓存中的 b 移除掉(该场景下二级缓存中并没有 b 对象)

a 对象的缓存转移也是同理。

4. 扩展

4.1 第三级缓存的作用?

从上面的执行步骤,可以感觉到三级缓存和里面的 ObjectFactory 类型似乎有点多余,有一级、二级缓存也能搞定循环依赖,三级缓存的意义是什么?

其实主要作用就是为了 AOP。

举例:假如对 b 对象使用了 AOP 切面功能,那么 a 对象引入的 b 对象就必须是 b 对象的代理对象,当 Spring 在没有循环依赖的情况下,是先将普通的完整对象创建好之后,再生成对应的代理对象,然而 Spring 并没有办法提前知道这个对象有没有循环依赖,也不能直接将每个对象都创建出代理对象,所以就需要吧对象包装成 ObjectFactory 类型,提前曝光,等从三级缓存中获取到 ObjectFactory 后,就可以通过 getObject() 方法生成代理对象。

4.2 避免循环依赖的最佳实践

尽管 Spring 提供了循环依赖的解决方案,但在实际开发中应尽量避免循环依赖,因为它可能导致代码耦合度过高、可维护性差等问题。以下是一些避免循环依赖的建议:

  1. 重构代码:将公共逻辑提取到第三个类中,打破循环依赖。
  2. 使用接口或事件驱动:通过接口或事件机制解耦组件间的直接依赖。
  3. 谨慎使用构造器注入:对于可能存在循环依赖的场景,优先使用 Setter 注入

4.3 构造器注入与 Setter 注入的区别

  1. Setter 注入:支持循环依赖。因为 Spring 可以通过三级缓存机制提前暴露部分初始化的 Bean 实例
  2. 构造器注入:不支持循环依赖。原因在于构造器注入要求在创建 Bean 时必须提供所有依赖项,而循环依赖会导致死锁(A 等待 B,B 等待 A)。

因此,如果使用构造器注入,循环依赖会导致 BeanCurrentlyInCreationException 异常。

BeanCurrentlyInCreationException 异常:表示在尝试实例化一个 Bean 时,Spring 容器检测到正在创建的 Bean 已经在创建过程中,导致循环依赖。这种异常通常是由于循环依赖问题引起的。

5. 总结

Spring 通过三级缓存机制解决了单例 Bean 之间的循环依赖问题,但对于原型 Bean 或构造器注入的场景,Spring 无法解决循环依赖。开发时应尽量避免循环依赖,保持代码的清晰性和可维护性

相关文章:

  • 深入解析EfficientNet:高效深度学习网络与ResNet的对比(使用TensorFlow进行代码复现,并使用cifar10数据集进行实战)
  • UniApp 运行的微信小程序如何进行深度优化
  • 服务器虚拟化:技术原理、实践与未来趋势
  • 测试用大模型组词
  • 文件传输协议(File Transfer Protocol, FTP)
  • 计算机组成原理 第三章 存储系统
  • 机试准备第11天
  • HarmonyOS ArkTS声明式UI开发实战教程
  • Ascend开发板镜像烧录、联网、其他设备访问
  • Laya中runtime的用法
  • 显示器长时间黑屏
  • 【音视频】ffmpeg命令提取音视频数据
  • SYSU-大数据原理与技术-课程知识点-第一章概述
  • git的坑
  • Nginx 缓存清理
  • 【CF】Day1
  • python监控系统资源使用率并钉钉报警脚本
  • C++:string容器(上篇)
  • DeepSeek与浏览器自动化AI Agent构建指南
  • 【Leetcode 每日一题】2597. 美丽子集的数目
  • 网站开发测试/最近几天的重大新闻事件
  • 网站文章列表和图片列表排版切换代码/自媒体营销模式有哪些
  • ipsw 是谁做的网站/传统营销与网络营销的整合方法
  • 义乌网站建设成都网站设计/不受限制的搜索引擎
  • 黄金网站app下载免费/免费的seo优化
  • 哪家公司建设网站/优化设计答案四年级上册语文