在 Spring 的日常开发中,循环依赖(Circular Dependency)是一个绕不开的话题。当 Bean A 依赖 Bean B,而 Bean B 又反过来依赖 A 时,如果没有特殊的处理机制,容器在创建对象时就会陷入无限递归的死锁。
然而,Spring 却能在绝大多数场景下优雅地化解这一危机。其背后的核心功臣,便是大名鼎鼎的"三级缓存"机制。但如果你深入探究,往往会产生一连串的疑问:为什么非要三级?两级不行吗?AOP 代理在其中扮演了什么角色?
本文将带你拨开源码的迷雾,用通俗的逻辑和代码链路,彻底讲透三级缓存的设计哲学。
1. Spring 的"破局"三板斧
Spring 解决这个问题的核心思路是:"先上车,后补票"。它允许一个对象在还未完全创建好(比如属性还没填完)时,就提前暴露一个"半成品"引用,让依赖它的其他对象先拿着用,等自己真正完成创建后,再"补全"自己。
为了实现这个精妙的机制,Spring 在 DefaultSingletonBeanRegistry 中准备了三层缓存,它们各司其职:
- 一级缓存 (singletonObjects):成品库,存放完全初始化好的 Bean 实例。
- 二级缓存 (earlySingletonObjects):半成品库,存放提前暴露的、但还未完成属性填充和初始化的 Bean 实例。
- 三级缓存 (singletonFactories):工厂库,存放 ObjectFactory 对象工厂,用于按需生成半成品引用或 AOP 代理。
为了让你直观地看到它们是如何协同工作的,我们以经典的 A 依赖 B、B 依赖 A 为例,来看一张时序图:
看到这里,你可能和我一样,脑子里会冒出第一个疑问。
2. 常见疑问一:为什么二级缓存不够用?
等等,从流程图上看,A 实例化后直接把半成品放到二级缓存,B 不就能直接从二级缓存拿到 A 了吗?那三级缓存是不是多余的?"
答案是:单看解决循环依赖,二级缓存确实足够了。 但三级缓存的存在,是为了解决一个更棘手的问题------AOP 代理。
如果没有三级缓存,只在二级缓存中存放原始对象,那么当 A 被 AOP 增强后(比如有 @Transactional 注解),B 依赖的 A 应该是代理对象,而不是原始对象。如果二级缓存中只存了原始对象,B 就拿不到正确的代理对象,导致事务等功能失效。
如果为了解决这个问题,把二级缓存改成"实例化后立即检查是否要生成 AOP 代理,需要就生成代理再放进去",那又会引发新的问题:每一个 Bean 在实例化后都要执行一遍 AOP 检查,这对绝大多数没有循环依赖的 Bean 来说,是一种性能浪费。
这就是三级缓存设计的精妙之处:它存储的是一个 ObjectFactory(对象工厂),而不是最终对象。这个工厂只在 "真正发生循环依赖" 时(即其他 Bean 来获取早期引用时)才会被触发执行,从而按需、延迟地创建 AOP 代理。没有循环依赖的 Bean,就不会触发这个工厂,代理会照常在其生命周期后期生成。
3. 常见疑问二:那我把"工厂"和"结果"合并到一个缓存里不行吗?
"既然三级缓存是工厂,二级缓存是工厂的执行结果,那为什么不合二为一?缓存里如果没有值,就当场执行工厂逻辑拿到对象再返回,不也行吗?"
这是一个非常深入的思考,我们刚才也重点探讨过。这个方案在逻辑上完全可行,但 Spring 最终选择了"分离"而不是"合并",主要出于以下考虑:
- 单一职责:三级缓存只管"怎么造"(工厂),二级缓存只管"造出来什么"(结果)。职责清晰,代码可读性极高。
- 并发安全:在并发场景下,如果有两个线程同时来获取 A,Spring 通过从三级缓存中移除工厂(remove 操作)这一原子动作,确保了工厂逻辑有且仅有一个线程能成功执行并生成对象。如果工厂和结果混在一个 Map 里,你就需要用额外的锁或状态标记来防止重复执行,逻辑会变得复杂且容易出错。
所以,Spring 的设计是典型的用空间(多一个 Map)换清晰度和并发安全性,这是一种非常优雅的架构权衡。
4. 总结
Spring 的三级缓存,是一个层层递进、逻辑严密的解决方案:
- 一级缓存 是最终成品库,确保单例。
- 二级缓存 是半成品库,也是三级缓存执行结果的缓存,保证循环依赖中大家拿到的是同一个对象。
- 三级缓存 是工厂库,实现了 AOP 代理的按需、延迟创建,避免了不必要的性能开销。
这套机制在保证高性能的同时,优雅地解耦了 Spring 的 IoC 容器和 AOP 等高级功能,无愧于 Spring 框架最精妙的设计之一。