在大型 Spring 项目中循环依赖是一个几乎无法避免的挑战。当 Bean A 依赖 Bean B而 Bean B 又依赖 Bean A 时如果没有特殊处理就会形成“先有鸡还是先有蛋”的死锁问题最终导致BeanCurrentlyInCreationException异常 。那么Spring 是如何优雅地破解这个难题的呢答案就在于其设计的三级缓存机制。什么是循环依赖循环依赖是指两个或多个 Bean 相互持有对方的引用形成一个闭环。例如Component public class BeanA { Autowired private BeanB beanB; // A 依赖 B } Component public class BeanB { Autowired private BeanA beanA; // B 依赖 A }循环依赖问题在Spring中主要有三种情况1、通过构造器方法进行依赖注入时产生的循环依赖问题2、通过setter方法进行依赖注入且是在多列模式下产生的循环依赖问题3、通过setter方法或字段进行依赖注入且是在单例模式下产生的循环依赖问题只有第三种方式的循环依赖问题被Spring解决了在单例作用域下Spring 可以解决 Setter 注入或字段注入Autowired带来的循环依赖问题。破解之道三级缓存Spring 解决循环依赖的核心是三个 Map它们在不同的阶段存放着不同状态的 Bean 对象 。缓存级别缓存名称存储内容作用一级缓存singletonObjects完全初始化好的、可用的成品 Bean存放最终的单例 Bean供外部直接使用二级缓存earlySingletonObjects提前暴露的半成品 Bean的原始对象引用或早期代理对象引用存放已完成实例化但未完成属性填充的原始对象专门用于解决循环依赖三级缓存singletonFactoriesObjectFactory对象工厂存放生成 Bean 早期引用的工厂对象用于延迟生成代理对象这是解决循环依赖和AOP代理协作的关键核心流程揭秘我们以 Bean A 和 Bean B 相互依赖为例来梳理整个流程 实例化 ASpring 开始创建 Bean A通过反射调用构造方法实例化出 A 的原始对象。提前暴露 AA 实例化后Spring 会将它包装成一个ObjectFactory并放入三级缓存singletonFactories中。此时A 已经可以被外界获取了虽然它还是个“半成品”。填充 A 的属性开始对 A 进行属性填充发现它依赖 Bean B。实例化 BSpring 转而去创建 Bean B同样实例化出 B 的原始对象。提前暴露 B将 B 的ObjectFactory放入三级缓存。填充 B 的属性开始对 B 进行属性填充发现它依赖 Bean A。打破循环B 去获取 A 的实例。此时它会先从一级缓存找发现没有再从二级缓存找也没有最后从三级缓存中找到了 A 的ObjectFactory。通过调用getObject()方法B 拿到了 A 的早期引用如果 A 需要 AOP 代理这里返回的就是代理对象。缓存升级拿到 A 的早期引用后Spring 会将其放入二级缓存earlySingletonObjects中并从三级缓存中移除 A 的工厂对象。这样A 的“半成品”引用就被“升级”到了二级缓存。B 完成初始化B 顺利注入 A 的引用后完成自己的初始化成为一个完整的 Bean并被放入一级缓存singletonObjects。A 完成初始化B 创建完成后A 继续执行顺利注入 B 的完整引用随后完成自己的初始化也放入一级缓存。至此循环依赖被完美解决。为什么需要三级缓存二级不够吗这可能是关于循环依赖最经典的面试题了。先说结论用二级缓存理论上可以解决循环依赖但无法在保证性能的前提下同时支持 AOP 代理对象的“延迟生成”。三级缓存存在的核心目的是为了将代理对象的创建时机从“Bean 实例化后立即生成”推迟到“真正发生循环依赖时再生成”。先回顾二级缓存能做什么假设我们只有两级缓存一级缓存成品 Map存放完全初始化好的 Bean二级缓存半成品 Map存放实例化完成、但尚未填充属性/初始化的原始对象流程是这样的A 实例化放入二级缓存半成品A 填充属性发现依赖 BB 实例化放入二级缓存B 填充属性发现依赖 AB 从二级缓存拿到 A 的半成品引用注入成功B 完成初始化移入一级缓存A 继续完成初始化移入一级缓存结论如果只处理普通 Java 对象没有 AOP 代理二级缓存完全够用。那三级缓存到底解决了什么问题出在 AOP 代理对象上。Spring 中很多 Bean 最终需要被代理比如Transactional、Async或自定义切面。代理对象不是原始对象而是通过CGLIB或JDK Proxy生成的一个增强过的子类/实现类。关键矛盾来了代理对象的生成通常发生在 Bean 初始化完成之后因为需要调用后置处理器BeanPostProcessor但如果循环依赖发生时B 需要在 A 还没完成初始化的情况下拿到 A 的引用这个引用必须是最终的代理对象否则 B 里存的原始对象A 后面生成的代理对象就失效了如果只用二级缓存怎么办那 Spring 就只能提前生成代理对象——即在 A 实例化完成后、放入二级缓存之前就立即执行 AOP 后置处理器生成代理对象再放进去。这带来的问题是无论最终是否发生循环依赖所有 Bean 在实例化后都要先生成代理对象。这是巨大的性能浪费。绝大多数 Bean 并不会被循环依赖但为了那少数情况让所有 Bean 都提前走一遍 AOP 代理生成逻辑显然不优雅。三级缓存如何做到“懒加载”三级缓存的设计是缓存存什么一级 singletonObjects最终成品 Bean二级 earlySingletonObjects提前暴露的代理/原始对象真正的半成品引用三级 singletonFactoriesObjectFactory一个函数式接口能按需生成代理对象流程关键点A 实例化后不立即生成代理而是把一个ObjectFactory放入三级缓存。B 发现依赖 A 时从三级缓存拿到这个工厂调用getObject()。只有在此时getObject()内部才会去执行AbstractAutoProxyCreator的后置处理器判断 A 是否需要代理生成代理对象。生成的代理对象被放入二级缓存并从三级缓存移除。后续再有人获取 A 的早期引用直接从二级缓存拿不会重复生成代理。这样一来不发生循环依赖的 Bean全程不会走三级缓存的getObject()代理对象照常在其生命周期末尾生成性能不受影响。发生循环依赖的 Bean只在需要提前暴露时才“按需”生成代理对象。总结三级缓存机制通过singletonObjects、earlySingletonObjects和singletonFactories三个 Map在对象的不同生命周期阶段进行存储和转移。提前暴露核心思想是在 Bean 实例化后立即暴露其引用即使尚未完成属性注入打破循环等待。AOP支持三级缓存通过ObjectFactory实现了代理对象的延迟创建只有在循环依赖发生时才会生成代理保证了框架的优雅和性能。理解 Spring 的三级缓存机制不仅能帮你应对面试更能让你在日常开发中更加深刻地理解框架的运行原理避免因循环依赖导致的诡异问题。