news 2026/2/1 5:58:20

咱们聊聊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/1/31 19:28:41

主散线指标 通达信源码

{}M:60; N:30; 散户线: 100*(HHV(HIGH,M)-CLOSE)/(HHV(HIGH,M)-LLV(LOW,M)),COLORGREEN,LINETHICK2; RSV:(CLOSE-LLV(LOW,N))/(HHV(HIGH,N)-LLV(LOW,N))*100; K:SMA(RSV,5,1); D:SMA(K,3,1); J:3*K-2*D; 主力线:EMA(J,6),COLORRED,LINETHICK2; VAR2:REF(LOW,1); VAR3:SMA(ABS(L…

作者头像 李华
网站建设 2026/1/24 21:48:29

提升开关频率(一) PRISEMI芯导科技MOSFET工艺结构的发展与演进

在手机快充、新能源汽车电驱、光伏逆变器等电力电子设备中&#xff0c;藏着一个关键“电子开关”——功率MOSFET。它的核心使命是快速控制电流的通断&#xff0c;而“开关频率”直接决定了设备的体积、效率和性能。频率越高&#xff0c;能量转换效率也越高。从早期的平面结构到…

作者头像 李华
网站建设 2026/1/28 20:21:43

音频录制和编辑软件

链接&#xff1a;https://pan.quark.cn/s/ead22a2177a3Reaper软件是一款非常强大的音频处理的工具&#xff0c;能够处理和制作多种音频的内容&#xff0c;让音频能够满足用户个人的需求&#xff0c;从软件上面来处理你所需要的音频&#xff0c;并且这款软件还是完全的免费的&am…

作者头像 李华
网站建设 2026/1/31 20:07:02

Quick CPU(CPU性能优化软件)

链接&#xff1a;https://pan.quark.cn/s/bd60bc94b310Quick CPU是一款功能强大的CPU监控软件&#xff0c;能够帮助用户随时监测CPU的运行情况&#xff0c;为CPU的优化提供了帮助。软件提供了Core Parking、频率缩放、Turbo Boost、C状态/变速等调整功能&#xff0c;满足用户的…

作者头像 李华