Spring如何巧妙解决循环依赖问题?深入浅出解析三级缓存机制

ops/2025/3/5 22:47:49/

一、什么是循环依赖?

循环依赖(Circular Dependency)就像两个程序员互相等待对方提交代码的场景:A说"我的代码要调用B的类",B说"但我的类需要A的接口定义"。在Spring中具体表现为:

 

java">@Service
public class ServiceA {@Autowiredprivate ServiceB serviceB;
}@Service
public class ServiceB {@Autowiredprivate ServiceA serviceA;
}

这两个服务类就像"先有鸡还是先有蛋"的问题,传统的对象创建方式根本无法解决这种相互依赖关系。但Spring通过巧妙的三级缓存设计,让这个看似无解的问题迎刃而解。

二、Spring Bean生命周期关键节点

要理解解决方案,先要掌握Bean创建的核心步骤:

  1. 实例化(Instantiate):new ServiceA() 创建原始对象

  2. 属性填充(Populate):通过反射注入依赖

  3. 初始化(Initialize):执行@PostConstruct等方法

三、三级缓存机制全景解析

Spring在DefaultSingletonBeanRegistry中维护了三个关键缓存

缓存名称存储内容级别
singletonObjects完整的单例Bean一级
earlySingletonObjects提前暴露的早期引用二级
singletonFactories创建Bean的ObjectFactory工厂对象三级

处理流程详解(以ServiceA和ServiceB为例)

  1. 创建ServiceA

    • 实例化ServiceA原始对象

    • 将ObjectFactory存入三级缓存(singletonFactories)

    • 开始属性注入,发现需要ServiceB

  2. 创建ServiceB

    • 实例化ServiceB原始对象

    • 将ObjectFactory存入三级缓存

    • 属性注入时发现需要ServiceA

  3. 获取ServiceA早期引用

    • 从三级缓存获取ObjectFactory

    • 执行getObject()得到ServiceA的早期引用(可能生成代理)

    • 将引用存入二级缓存,清除三级缓存记录

  4. 完成ServiceB创建

    • 继续完成ServiceB的属性注入和初始化

    • 将ServiceB存入一级缓存

  5. 回填ServiceA

    • 用已创建的ServiceB完成属性注入

    • 执行初始化方法

    • 将ServiceA存入一级缓存

四、源码级深度剖析

关键源码在AbstractAutowireCapableBeanFactory

java">protected Object doCreateBean(...) {// 1. 实例化BeaninstanceWrapper = createBeanInstance(beanName, mbd, args);// 2. 加入三级缓存(重要!)addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));// 3. 属性注入populateBean(beanName, mbd, instanceWrapper);// 4. 初始化initializeBean(beanName, exposedObject, mbd);
}

缓存操作核心方法:

java">protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {synchronized (this.singletonObjects) {if (!this.singletonObjects.containsKey(beanName)) {this.singletonFactories.put(beanName, singletonFactory);this.earlySingletonObjects.remove(beanName);this.registeredSingletons.add(beanName);}}
}

五、不同注入方式的差异

注入方式是否支持解决循环依赖原因分析
Setter注入属性注入在对象实例化之后
字段注入同Setter注入
构造器注入实例化前就需要完成注入

构造器注入失败示例:

java">@Service
public class ServiceC {private final ServiceD serviceD;@Autowiredpublic ServiceC(ServiceD serviceD) {this.serviceD = serviceD;}
}@Service
public class ServiceD {private final ServiceC serviceC;@Autowiredpublic ServiceD(ServiceC serviceC) {this.serviceC = serviceC;}
}

这种情况会直接抛出BeanCurrentlyInCreationException

六、应用场景与限制条件

适用场景

  • 单例作用域(Singleton)的Bean

  • 非构造器注入方式

  • 没有同时使用AOP代理的情况

限制条件

  1. 原型(Prototype)作用域的Bean无法解决

  2. 需要开启allowCircularReferences(默认true)

  3. 存在AOP代理时需要特殊处理(通过ObjectFactory延迟生成代理)

七、最佳实践建议

  1. 架构设计层面

    • 尽量避免循环依赖,使用中介者模式解耦

    • 将公共逻辑抽取到第三方的Common模块

  2. 编码实现层面

    • 优先使用构造器注入明确依赖关系

    • 对于必须的循环依赖使用@Lazy延迟加载

    • 复杂场景考虑使用ApplicationContext.getBean()

  3. 调试技巧

    • 开启Spring调试日志:logging.level.org.springframework=DEBUG

    • 分析Bean创建顺序:spring.main.lazy-initialization=true

    • 使用@DependsOn显式控制初始化顺序

八、常见问题解答

Q:为什么需要三级缓存而不是两级?

A:主要是为了处理AOP代理的情况。ObjectFactory可以延迟决定返回原始对象还是代理对象,保证最终注入的Bean类型正确。

Q:Spring如何检测循环依赖?

A:通过DefaultSingletonBeanRegistrysingletonsCurrentlyInCreation集合记录正在创建的Bean,当发现重复创建时抛出异常。

Q:多级循环依赖(如A->B->C->A)能解决吗?

A:可以,只要依赖链是setter/字段注入且都是单例Bean,三级缓存机制可以处理任意长度的循环链。

九、总结

Spring的三级缓存设计充分体现了"空间换时间"的智慧:

  1. 一级缓存:存储完整可用的成品Bean

  2. 二级缓存:临时保存半成品Bean的早期引用

  3. 三级缓存:通过ObjectFactory实现灵活的代理处理

通过这种分层缓存的机制,Spring既保证了单例Bean的唯一性,又巧妙地打破了循环依赖的死锁状态。理解这个机制不仅能帮助我们更好地使用Spring,更能够学习到框架设计中解决复杂问题的思路。


http://www.ppmy.cn/ops/163430.html

相关文章

FastGPT 源码:基于 LLM 实现 Rerank (含Prompt)

文章目录 基于 LLM 实现 Rerank函数定义预期输出实现说明使用建议完整 Prompt 基于 LLM 实现 Rerank 下边通过设计 Prompt 让 LLM 实现重排序的功能。 函数定义 class LLMReranker:def __init__(self, llm_client):self.llm llm_clientdef rerank(self, query: str, docume…

5. Nginx 负载均衡配置案例(附有详细截图说明++)

5. Nginx 负载均衡配置案例(附有详细截图说明) 文章目录 5. Nginx 负载均衡配置案例(附有详细截图说明)1. Nginx 负载均衡 配置实例3. 注意事项和避免的坑4. 文档: Nginx 的 upstream 配置技巧5. 最后&#xff1a; 1. Nginx 负载均衡 配置实例 需求说明/图解 windows 浏览器输…

15-YOLOV8OBB损失函数详解

一、YOLO OBB支持的OBB 在Ultralytics YOLO 模型中,OBB 由YOLO OBB 格式中的四个角点表示。这样可以更准确地检测到物体,因为边界框可以旋转以更好地适应物体。其坐标在 0 和 1 之间归一化: class_index x1 y1 x2 y2 x3 y3 x4 y4 YOLO 在内部处理损失和输出是xywhr 格式,x…

Redis——缓存穿透、击穿、雪崩

缓存穿透 什么是缓存穿透 缓存穿透说简单点就是大量请求的 key 根本不存在于缓存中&#xff0c;导致请求直接到了数据库上&#xff0c;根本没有经过缓存这一层。举个例子&#xff1a;某个黑客故意制造我们缓存中不存在的 key 发起大量请求&#xff0c;导致大量请求落到数据库…

[数据结构]二叉树详解

目录 一、二叉树概念及结构 1.1概念 1.2现实中的二叉树&#xff1a; 1.3 特殊的二叉树&#xff1a; 1.4 二叉树的存储结构 1. 顺序存储 2. 链式存储 二、二叉树的顺序结构及实现 2.1 二叉树的顺序结构 三、二叉树链式结构的实现 3.1 前置说明 3.2二叉树的遍历 3.2…

HTTP~文件 MIME 类型

MIME&#xff08;Multipurpose Internet Mail Extensions&#xff09;类型&#xff0c;即多用途互联网邮件扩展类型&#xff0c;是一种标准&#xff0c;用来表示文档、文件或字节流的性质和格式。最初是为了在电子邮件系统中支持非 ASCII 字符文本、二进制文件附件等而设计的&a…

P8651 [蓝桥杯 2017 省 B] 日期问题--注意日期问题中2月的天数 / if是否应该连用

P8651 [P8651 [蓝桥杯 2017 省 B] 日期问题--注意日期问题中2月的天数 / if是否应该连用 题目 分析代码 题目 分析 代码中巧妙的用到3重循环&#xff0c;完美的解决了输出的顺序问题【题目要求从小到大】 需要注意的是2月的值&#xff0c;在不同的年份中应该更新2月的值 还有…

CSS—背景属性与盒子模型(border、padding、margin)

目录 一.背景属性 二.盒子模型 1.边框border a. 圆角属性border-radius b. 图像属性border-image 2. 内边距padding 3. 外边距margin 3. 宽度width与高度height 一.背景属性 浏览器背景图默认是平铺效果&#xff08;复制图片直至填满设置的区域大小&#xff09; 背景…