news 2026/5/1 7:06:19

咱们聊聊Spring循环依赖那点事儿:从“死锁”到“三级缓存”的奇妙之旅

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
咱们聊聊Spring循环依赖那点事儿:从“死锁”到“三级缓存”的奇妙之旅

最近看了点面试题,发现Spring循环依赖,一二三级缓存还是一个盲点,估计很多人也是一样吧,就专门查了资料了解了这部分内容,希望给在这部分内容茫然的同仁们一点点启发,先赞后看你必能学会👍💗~ ~ ~

你有没有写过这样的代码:两个类A和B,A里要用到B,B里又要用到A,结果Spring启动时“啪”地抛了个BeanCurrentlyInCreationException,告诉你“循环依赖了”?别慌,这事儿Spring其实早有预案——今天咱们就用最接地气的方式,把这个“死锁”怎么破、三级缓存怎么玩,掰开揉碎讲明白。

一、先举个“生活化”的例子:机器人组装厂的死锁危机

想象你在开个机器人组装厂(这就是Spring容器),专门生产各种机器人(Bean)。每个机器人得按流程造:先搭骨架(实例化,调构造函数)→ 装零件(填属性,比如依赖其他机器人)→ 测试出厂(初始化,调@PostConstruct等方法)→ 合格了进“成品仓库”(一级缓存),随时能领用。

某天接了两个订单:造A机器人和B机器人。

  • A的说明书:“我得装个B的核心零件才能干活!”(A依赖B)
  • B的说明书:“我得装个A的能源核心才能启动!”(B依赖A)

工人开工了:

  1. 先造A:搭好骨架(A的“裸体”对象),准备装零件时发现要B——B还没造呢!
  2. 转头造B:搭好骨架(B的“裸体”对象),准备装零件时发现要A——A也没造完呢!

得,A等B,B等A,俩机器人都卡在“等零件”这一步,工厂差点停工。这就是循环依赖:两个Bean互相指着对方说“你得先给我,我才完整”,结果谁都动不了。

二、Spring的“救场神器”:三级缓存是个啥?

厂长急中生智,搞了个“半成品暂存系统”——这就是Spring大名鼎鼎的三级缓存。简单说,就是给刚搭好骨架的机器人发张“预订券”,谁急着用,先领个“毛坯版”顶上,等正式零件造好再替换。

这个系统分三层(对应DefaultSingletonBeanRegistry类里的三个Map):

缓存层级比喻说法真实身份(类名)存啥玩意儿?
一级缓存成品仓库singletonObjectsConcurrentHashMap完全造好的机器人(成品Bean):实例化+装零件+测试全搞定,随时能领。
二级缓存毛坯暂存处earlySingletonObjectsHashMap刚搭好骨架的“裸体”机器人(早期对象),或从三级缓存“兑换”来的毛坯(可能带“贴膜”=AOP代理)。
三级缓存工厂仓库(预订券)singletonFactoriesHashMap“预订券”(ObjectFactory工厂对象):凭券能现场领个毛坯机器人(含贴膜逻辑)。
三、三级缓存咋破解死锁?一步步看流程(附“流程图”)

还是用A→B→A的例子,咱们跟着工人师傅走一遍:

1. 造A(实例化)→ 发“预订券”进三级缓存 → 装零件时发现要B ↓ 2. 造B(实例化)→ 发“预订券”进三级缓存 → 装零件时发现要A ↓ 3. B找A:成品库(一级)无→毛坯暂存处(二级)无→工厂仓库(三级)找到A的“预订券” ↓ 4. 拿A的券“兑换”:工厂现场给A的毛坯(裸体骨架,要代理就贴膜)→ 毛坯进二级缓存,券从三级缓存删掉 ↓ 5. 把A的毛坯当零件装给B → B装完测试 → 送进成品库(一级缓存) ↓ 6. 回头给A装零件:去成品库领B → A装完测试 → 送进成品库(一级缓存)

结果:A和B都造好了!死锁解开,靠的就是“先领毛坯顶上,再补零件”的思路。

四、关键原理:为啥三级缓存这么设计?
1. 为啥构造器注入会“死锁”?

如果用构造器注入(比如A的构造函数必须传B,B的构造函数必须传A),那问题就大了:造A得先有B,造B得先有A——俩机器人连骨架都没搭起来(实例化都没完成),哪来的“预订券”进三级缓存?这不就死锁了吗?所以构造器注入的循环依赖,Spring直接摆烂:抛异常!

2. 为啥需要三级缓存,两级不行吗?

假设只有“成品库”(一级)和“毛坯暂存处”(二级):

  • 造A时,得先把A的毛坯放进二级缓存(不然B找A时找不到),但毛坯要不要用AOP代理(比如加日志、事务)?
  • 如果A本来不需要代理,提前放毛坯没问题;但如果A需要代理,放原始毛坯就错了(应该用代理对象)。

三级缓存的聪明之处在于:用“预订券”(ObjectFactory)延迟生成毛坯。只有真的发生循环依赖(比如B急着要A),才调用ObjectFactory.getObject()生成毛坯(顺便判断要不要代理),生成后放进二级缓存。这样既避免了“提前代理”的浪费,又保证了代理的正确性。

五、源码瞅一眼:三级缓存的真实面目

光说不练假把式,咱们看段Spring源码(DefaultSingletonBeanRegistry类),感受下三级缓存的“物理形态”:

/* by yours.tools - online tools website : yours.tools/zh/tripledes.html */ // 一级缓存:成品Bean(key: bean名, value: 成品Bean) private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256); // 二级缓存:早期Bean(毛坯,key: bean名, value: 原始对象或代理对象) private final Map<String, Object> earlySingletonObjects = new HashMap<>(16); // 三级缓存:ObjectFactory工厂(key: bean名, value: 生成早期引用的工厂) private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);

关键方法:提前暴露“预订券”
在Bean实例化后(调完构造函数),Spring会把ObjectFactory放进三级缓存,代码在AbstractAutowireCapableBeanFactory.doCreateBean()里:

/* by yours.tools - online tools website : yours.tools/zh/tripledes.html */ // 实例化Bean后,暴露早期引用工厂到三级缓存 boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences && isSingletonCurrentlyInCreation(beanName)); if (earlySingletonExposure) { // 把ObjectFactory放进三级缓存,工厂逻辑是调用getEarlyBeanReference生成早期引用 addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean)); } // addSingletonFactory方法:往三级缓存塞ObjectFactory 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); } } }

getEarlyBeanReference:判断是否要“贴膜”(AOP代理)
这个方法是生成早期引用的核心,会检查Bean是否需要AOP代理(比如被@Transactional标注):

protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) { Object exposedObject = bean; // 遍历所有BeanPostProcessor,处理早期引用(比如AOP代理) if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) { for (BeanPostProcessor bp : getBeanPostProcessors()) { if (bp instanceof SmartInstantiationAwareBeanPostProcessor) { SmartInstantiationAwareBeanPostProcessor ibp = (SmartInstantiationAwareBeanPostProcessor) bp; exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName); // AOP代理在这儿生成 } } } return exposedObject; // 返回原始对象或代理对象 }
六、咋避免循环依赖?老司机的建议
  1. 优先用构造器注入“排雷”:构造器注入一报错,你就知道“这儿有循环依赖,得重构!”,倒逼你把代码解耦(比如引入中间层Service)。
  2. 实在绕不开,用Setter/字段注入:Spring的三级缓存只认这种“实例化后装零件”的注入方式。
  3. @Lazy注解“缓兵之计”:在构造器注入的某个依赖上加@Lazy,Spring会注入个“代理对象”(相当于“提货单”),等真用的时候再去领成品,打破死锁。
  4. 别用Prototype作用域:每次new一个对象,三级缓存根本帮不上忙,循环依赖必炸。
七、总结:三级缓存的本质

Spring的三级缓存(singletonFactoriesearlySingletonObjectssingletonObjects),说白了就是“用空间换时间”:提前暴露半成品(毛坯),让依赖方先用着,等正式零件造好再替换。核心是用ObjectFactory工厂“延迟生成早期引用”,顺便搞定AOP代理的坑。

不过话说回来,循环依赖能解决不代表应该出现——它往往是代码耦合太高的信号。理解了三级缓存的原理,下次遇到循环依赖,你不仅能知道“为啥报错”,还能笑着跟同事说:“来,咱用@Lazy或者重构一下,别让机器人组装厂再停工啦!”

(完)

❤️ 如果你喜欢这篇文章,请点赞支持! 👍 同时欢迎关注我的博客,获取更多精彩内容!

本文来自博客园,作者:佛祖让我来巡山,转载请注明原文链接:https://www.cnblogs.com/sun-10387834/p/19346114

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/1 3:49:21

23、Linux文本处理工具全解析

Linux文本处理工具全解析 在Linux系统中,文本处理是一项非常重要的工作。无论是日常的系统管理,还是软件开发过程中的代码处理,都离不开各种文本处理工具。本文将详细介绍一些常用的Linux文本处理工具及其使用方法。 排序与去重 在处理文本文件时,排序和去重是常见的操作…

作者头像 李华
网站建设 2026/5/1 3:50:04

YouTube开放稳定币收款,加密货币支付正在全球普及?

2025年底&#xff0c;一则看似简单的支付功能更新&#xff0c;却引起了全球科技与金融圈的热烈讨论。全球最大的影音平台YouTube宣布&#xff0c;将允许美国地区的内容创作者选择使用PayPal发行的美元稳定币PYUSD来领取他们的收益。这不仅仅是为创作者提供一个新的收款选项&…

作者头像 李华
网站建设 2026/5/1 3:49:19

显卡卸载工具DDU

链接&#xff1a;https://pan.quark.cn/s/b675ab53fa00显卡卸载工具DDU是款可用于显卡驱动卸载的程序工具&#xff0c;这个驱动卸载工具支持卸载NVIDIA, AMD, Intel品牌的显卡驱动&#xff0c;也就是说绝大部分电脑都可以使用它彻底卸载显卡驱动。功能特点可用于显卡驱动程序的…

作者头像 李华
网站建设 2026/5/1 3:49:34

直播工具OBS Studio

链接&#xff1a;https://pan.quark.cn/s/b675ab53fa00OBS Studio是一款非常知名&#xff0c;使用用户庞大的一款OBS直播软件&#xff0c;是专为视频直播开发的一款直播软件&#xff0c;与经典版相比&#xff0c;它在这个基础上进行了升级改进&#xff0c;音频分路简单&#xf…

作者头像 李华
网站建设 2026/5/1 4:57:42

针对原网格的流场单变量进行本征正交分解pod程序 输出模态tecplot文件,特征值,时间系数等参数

针对原网格的流场单变量进行本征正交分解pod程序 输出模态tecplot文件&#xff0c;特征值&#xff0c;时间系数等参数&#xff0c;输出重构流场tecplot文件 包含视频教程和实例数据以及程序代码最近在搞流场分析的朋友应该都听说过POD这玩意儿。这玩意说白了就是把复杂流动数据…

作者头像 李华
网站建设 2026/5/1 4:57:11

如何在 Vim 启用行号显示和语法高亮功能

文章目录 检查是否支持语法高亮命令方式启用行号显示和语法高亮功能&#xff08;仅对当次有效&#xff09;配置模式启用行号显示和语法高亮功能&#xff08;永久有效&#xff09;总结开启行号显示关闭行号显示开启语法高亮关闭语法高亮 Vim 是 Linux 平台上优秀的文本编辑器的开…

作者头像 李华