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

spring为什么使用三级缓存而不是两级?

Spring 使用三级缓存(而不是两级)来解决循环依赖,主要目的是兼容 AOP(动态代理)场景,同时保持 Bean 创建过程的语义一致性扩展性

如果只用两级缓存,在大多数普通属性注入的循环依赖场景下确实可以工作,但一旦引入 AOP(@Transactional、@Async、@Cacheable 等),就会出现非常严重的问题提前暴露的对象和最终容器里的对象不是同一个(类型/引用不一致),导致运行时类型转换异常、代理失效、事务不生效等。

三级缓存各自职责(DefaultSingletonBeanRegistry)

缓存层级Map 类型存放的内容什么时候放入什么时候取出并移除核心目的
一级缓存 singletonObjects普通 bean(成品)完全初始化好的 bean(可能已代理)初始化完成、放入成品后几乎所有 getBean 最终都走这里成品缓存,正常使用的地方
二级缓存 earlySingletonObjects早期普通对象(半成品)已实例化但还没完成属性填充和初始化的原始对象从三级缓存拿到并暴露早期引用后被其他 bean 依赖时取出,移到一级防止重复创建同一个早期对象
三级缓存 singletonFactoriesObjectFactory(lambda 工厂)生成早期引用的工厂(通常是 getEarlyBeanReference 的 lambda)put 时创建 bean 实例后立即放入第一次被循环依赖时调用 getObject() → 生成早期对象(或代理对象)→ 放入二级延迟决定是否要 AOP 代理的关键点

为什么二级缓存不够?(最核心原因)

假设我们只有一级 + 二级(成品 + 早期普通对象):

A → B → A (A 被 AOP 代理)
  1. 创建 A → 实例化(new A) → 放入二级缓存(早期 A,原始对象)
  2. A 需要注入 B → 创建 B
  3. B 需要注入 A → 从二级缓存拿到原始 A(还没代理)
  4. B 完成 → 放入一级缓存
  5. A 继续 → 填充属性、初始化、AOP 后生成代理对象 proxyA
  6. proxyA 放入一级缓存

问题来了

  • B 手里拿到的 A 是原始 A
  • 容器最终暴露的是proxyA
  • B 拿到的引用和最终容器里的 bean 不是同一个对象!

这会导致:

  • B 调用 A 的方法时绕过了代理 → 事务/日志/权限等切面全部失效
  • 类型检查失败(有些地方强转成代理接口会报错)
  • equals()/hashCode() 异常行为

三级缓存如何解决这个问题?

关键就在第三级:它放的不是对象本身,而是一个 ObjectFactory(lambda),这个 lambda 会在真正被别人依赖的时候才执行:

// 大致伪代码protectedObjectgetEarlyBeanReference(StringbeanName,RootBeanDefinitionmbd,Objectbean){returngetSingleton(beanName,()->{// 这里才是真正决定要不要代理的地方returnapplyBeanPostProcessorsBeforeInitialization(bean,beanName);// → post-processor 可能返回代理对象});}

流程变成:

  1. 创建 A → 实例化 → 放入三级缓存(一个 lambda:getEarlyBeanReference)
  2. A 需要 B → 创建 B
  3. B 需要 A → 从三级缓存拿到 lambda →执行 lambda→ 得到早期引用(如果是 AOP 场景,这里就会提前生成代理对象
  4. 把这个早期引用(可能是代理)放入二级缓存,同时从三级移除
  5. B 拿到的是已经代理好的 A(proxyA)
  6. A 继续后续流程,最终 proxyA 放入一级缓存

结果:B 拿到的 A 和最终容器里的 A 是同一个对象(都是 proxyA),语义一致。

总结一句话

Spring 用三级缓存而不是两级,核心是为了在提前暴露引用时仍然能正确应用 AOP 动态代理,保证“别人提前拿到的引用”和“最终容器里的 bean”是同一个对象。

如果你的项目完全关闭 AOP(never proxy),理论上两级缓存就够了(很多手写 IoC 框架就是这么干的)。但 Spring 要做成通用框架,必须兼容 AOP,所以必须用三级。

你项目里遇到过因为循环依赖 + AOP 导致的诡异 bug 吗?或者你更倾向于“能不循环就不循环”的设计哲学?

http://www.jsqmd.com/news/302112/

相关文章:

  • 为什么go和rust语言都舍弃了继承?
  • Silk V3音频解码技术实践指南:从环境搭建到故障排除
  • BilibiliDown视频下载工具全攻略:多场景解决方案与高效使用指南
  • Android设备控制与跨平台工具:QtScrcpy零基础入门指南
  • 戴森球计划蓝图仓库新手指南:零门槛构建高效生产体系
  • YOLOE部署踩坑记录:这些错误千万别犯
  • mptools v8.0在CS32系列中的应用完整示例
  • 如何通过用户脚本优化123云盘使用体验
  • 3步完成LivePortrait跨平台部署:让静态肖像动起来的AI工具全指南
  • 深度测评9个AI论文网站,专科生轻松搞定毕业论文!
  • 打造智能协作机械臂:LeRobot SO-101从硬件到控制全攻略
  • 英雄联盟LCU接口应用框架:Akari技术架构与实践指南
  • B站直播推流专业指南:从原理到实战的技术解析
  • Qwen3-1.7B效果惊艳!猫娘角色生成案例展示
  • 解密高效翻译:Crow Translate如何引发效率革命
  • YimMenu游戏助手完全掌握指南:从入门到精通
  • 5分钟上手GPEN图像修复,科哥版WebUI一键增强老照片
  • 如何解决AList夸克TV驱动授权二维码过期问题:3种实用方案
  • 降噪麦克风搭配使用,识别准确率再提升
  • 光纤光源聚焦模式的像差效应
  • 基于MATLAB的GFSK调制解调实现
  • 2026年推荐靠谱的电商平台律师,杭州地区有哪些
  • 5个步骤打造你的本地AI知识管理中心:开源工具Open Notebook全攻略
  • WinSetView:实现Windows文件夹视图统一的高效工具
  • TikTok商城跌落测试:筑牢运输防护防线
  • 基于ESP-IDF的ESP32-S3深度睡眠模式全面讲解
  • 未来社交新模式,Live Avatar虚拟分身使用畅想
  • League Akari:提升胜率的智能游戏辅助工具 英雄联盟玩家的全能解决方案
  • FSMN-VAD本地运行不联网,隐私安全有保障
  • 音乐解密终极方案:Unlock Music完全攻略