简介:本文从单例Bean的完整生命周期出发,逐级拆解三级缓存的职责分工与协作时序,推演两级缓存在“循环依赖+AOP代理”叠加场景下的两种缺陷方案,还原工厂层“延迟决策”的设计精髓,并结合异步代理导致启动失败的真实案例,说明该机制的适用边界与重构建议。
在企业级Java开发中,“循环依赖”是面试高频题,也是线上问题高发区。很多开发者能脱口而出:“主流IoC容器用三级缓存解决了单例Bean的循环依赖。”但一旦追问——为什么偏偏是三级,两级不行吗?能讲清楚的人寥寥无几。
更现实的一幕是:不少团队以为“框架既然号称能解决循环依赖,那怎么写都不会出事”。直到某天,两个互相注入的服务中,因为一个方法上多了个异步注解,项目启动直接失败,异常信息还相当拗口。要理解这次报错的根因,必须先把三级缓存的设计动机吃透。
一个单例Bean从无到有,要依次经历三个阶段:
假设A依赖B、B依赖A,按朴素流程走:创建A→属性填充时需要B→转去创建B→B填充属性时需要A→而A尚未成为成品、不在单例池里→流程卡死。
破局思路其实很朴素:提前曝光半成品。A在实例化完成后、属性填充之前,先把“自己”暴露出去;B需要A时先拿这个半成品应急。由于Java的引用指向同一块内存,等A走完属性填充与初始化后,B手里的引用会“自动升级”为最终形态。
这个机制并非万能,必须先明确三条边界:
主流IoC容器的单例注册表内部,大致维护着三个Map(伪代码):
Map<String, Object> level1; // 一级:成品池,存放完整走完生命周期的单例Map<String, Object> level2; // 二级:早期引用池,存放被提前曝光过的对象(可能是代理)Map<String, Factory<Object>> level3; // 三级:工厂池,存放能按需产出早期引用的回调
getBean(A):一级缓存未命中,将A标记为“创建中”,执行实例化,得到原始对象;getBean(B);getBean(A):一级缓存没有→发现A处于“创建中”,允许提前曝光→二级缓存没有→三级缓存命中工厂→调用工厂:容器借此询问各后置处理器“如果现在必须提前暴露A,该暴露什么?”若A需要被AOP代理,此处会提前生成代理→结果升入二级缓存,工厂同时从三级缓存移除;结果:B持有的、以及后续任何Bean获取到的,都是同一个代理对象,身份一致性保住了。
假设砍掉工厂层,只保留“成品池 + 早期池”,有两种摆法,逐一推演。
为了保一致性,也可以规定:凡需要代理的Bean,实例化一完成就立即生成代理放入二级缓存。
| 方案 | 引用一致性 | 代理生成时机 | 对普通Bean的影响 |
|---|---|---|---|
| 两级·存原始对象 | 无AOP时成立,有AOP时静默崩坏 | 初始化后(正常) | 无 |
| 两级·存提前代理 | 成立 | 实例化后(全局提前) | 全部Bean受影响 |
| 三级缓存 | 恒成立 | 默认初始化后;仅循环依赖时提前 | 零影响 |
工厂层(三级缓存)的本质,是把“要不要提前生成代理”这个昂贵决策,从“创建Bean时”推迟到“真的有别的Bean提前要它时”:
二级缓存则承担两个关键职责:
一句话总结:第三级缓存解决的从来不是“循环依赖能不能解”——没有AOP时,两级缓存足矣;它真正解决的是“循环依赖与AOP同时发生时,代理的生成时机与引用一致性”问题。这是用一层间接(工厂)换来的设计弹性。
看一个真实案例的骨架(示意代码,注解语义以中文表达,仅用于说明结构):
public class OrderService {@自动注入private StockService stockService;@异步执行 // 关键:方法需要被异步代理包装public void notifyUser() { ... }}public class StockService {@自动注入private OrderService orderService;}
两个服务互相注入构成循环依赖,同时OrderService的方法上带了一个异步注解。启动直接失败,异常大意是:该Bean曾以原始版本被注入其它Bean,但最终又被包装过,导致其它Bean持有的并非最终版本。
为什么三级缓存这次没兜住?关键在于:异步代理的包装器没有参与“早期引用钩子”:
这个报错恰恰反证了设计的严谨:与其让StockService拿着一个“看起来正常、实际绕过异步逻辑”的引用在线上静默运行,不如在启动阶段就暴露矛盾(fail fast),把排查成本压到最低。
遇到这类问题,推荐按优先级处理:
最后必须强调:容器提供的循环依赖支持,是框架的“人道主义援助”,而不是鼓励写出循环依赖的通行证。循环依赖往往意味着职责划分出了问题——能靠重构解决的,永远不要指望缓存来兜底。理解“为什么是三级”,是为了理解框架在灵活性、性能与正确性之间的精妙权衡,而不是为了理直气壮地写出互相纠缠的代码。