
1. 项目概述从“鸡生蛋还是蛋生鸡”说起在Java后端开发领域尤其是基于Spring框架构建企业级应用时循环依赖是一个几乎每个开发者都会遇到的经典难题。想象一下你正在设计一个电商系统OrderService需要调用PaymentService来处理支付而PaymentService在支付成功后需要回调OrderService来更新订单状态。这两个服务类相互引用就像“鸡生蛋还是蛋生鸡”的逻辑困境在Spring容器启动、创建Bean的过程中如果处理不当就会抛出那个令人头疼的BeanCurrentlyInCreationException。Spring框架作为Java生态的基石其设计哲学之一就是让开发者专注于业务逻辑而将对象创建、依赖注入这些繁琐的“脏活累活”交给容器管理。循环依赖问题本质上是对这套依赖注入DI和IoC控制反转机制的一个极限挑战。Spring的解决方案就是今天我们要深入剖析的三级缓存机制。这不仅仅是面试八股文里的高频考点更是理解Spring Bean生命周期、设计模式以及框架设计者智慧的绝佳窗口。弄明白它你不仅能从容应对面试更能深刻理解Spring容器内部的工作机制在遇到复杂依赖关系时能够快速定位和解决问题写出更健壮、更优雅的代码。2. 循环依赖的本质与Spring的应对策略2.1 什么是循环依赖一个简单的场景复现让我们先抛开框架从最朴素的Java代码层面理解循环依赖。假设有两个类A和Bpublic class A { private B b; public void setB(B b) { this.b b; } // ... 其他方法 } public class B { private A a; public void setA(A a) { this.a a; } // ... 其他方法 }如果我们尝试手动创建它们A a new A(); B b new B(); a.setB(b); b.setA(a);这个过程是顺畅的因为对象实例化new和属性赋值set是分开的两个步骤。问题在于Spring的IoC容器在管理Bean时其默认的创建过程以单例Bean、构造器注入为例期望一个“完美”的Bean即当Bean被创建出来时它的所有依赖都已经准备就绪。如果A和B都使用构造器注入Component public class A { private final B b; public A(B b) { this.b b; } // 构造器需要B } Component public class B { private final A a; public B(A a) { this.a a; } // 构造器需要A }这时Spring容器就陷入了死锁要创建A必须先有B要创建B必须先有A。这是一个无法解开的结Spring会直接抛出异常因为它无法决定先创建谁。这种构造器循环依赖是Spring无法解决的设计上就应该避免。注意这是理解三级缓存的前提。Spring解决的是**属性注入Field Injection和Setter方法注入Setter Injection**场景下的循环依赖因为这两种方式允许Bean先被实例化得到一个“半成品”对象然后再进行属性填充。2.2 Spring的破局思路空间换时间与提前暴露引用既然构造器注入的死结无法解开Spring就为属性/Setter注入设计了一套巧妙的“迂回”策略。其核心思想可以概括为打破“创建即完美”的假设允许Bean以一个“半成品”早期引用Early Reference的形式提前暴露出来供其他Bean依赖待所有依赖关系都建立后再回头完善这个“半成品”。这个过程就像组装一台电脑先把CPU、主板、内存的包装盒拆开放在桌上实例化但未装配。把CPU装到主板上此时主板一个“半成品”已经可以提供给内存条告诉内存条“我这里有CPU插槽了”提前暴露早期引用。内存条看到主板就位也完成自己的安装解决依赖完成自身装配。最后把装好CPU和内存的主板放入机箱连接电源和硬盘执行初始化回调完成Bean的最终装配。Spring实现这一策略的关键数据结构就是三级缓存。它通过三个Map在不同阶段存放Bean的不同形态协同工作来破解循环依赖。3. 三级缓存机制深度拆解Spring容器通常是DefaultSingletonBeanRegistry类内部维护了三个非常重要的Map这就是我们常说的“三级缓存”。理解每一级缓存存放的内容、目的以及它们之间的协作流程是彻底弄明白循环依赖解决机制的关键。3.1 第一级缓存SingletonObjects成品仓库/** 一级缓存存放完整的、经历过完整生命周期的单例Bean。 */ private final MapString, Object singletonObjects new ConcurrentHashMap(256);角色成品仓库。这是大家最熟悉的缓存我们通过ApplicationContext.getBean()方法最终获取到的Bean就是来自这里。存放对象完全初始化好的Bean。意味着这个Bean已经经历了实例化 - 属性填充 - 执行Aware接口回调 -BeanPostProcessor.postProcessBeforeInitialization- 执行InitializingBean.afterPropertiesSet或自定义init-method-BeanPostProcessor.postProcessAfterInitialization。特点这里的Bean是“安全”的可以直接用于业务逻辑。3.2 第二级缓存earlySingletonObjects半成品展台/** 二级缓存存放早期的单例对象即已实例化但尚未完全初始化的Bean。 */ private final MapString, Object earlySingletonObjects new ConcurrentHashMap(16);角色半成品展台。它是一个中间缓存用于存放那些已经被提前暴露、用于解决循环依赖的“半成品”Bean。存放对象刚从三级缓存中被升级上来的Bean早期引用。这个对象已经可能被BeanPostProcessor特别是SmartInstantiationAwareBeanPostProcessor处理过例如被AOP代理对象所替换。生命周期当一个Bean被提前暴露以解决其他Bean的依赖后它会从三级缓存移动到二级缓存。待该Bean自身完全初始化后会从二级缓存移除并放入一级缓存。目的防止重复创建代理对象。这是二级缓存一个非常关键的作用。如果没有二级缓存在存在AOP代理的情况下每次其他Bean来获取早期引用时都可能触发SmartInstantiationAwareBeanPostProcessor.getEarlyBeanReference导致为同一个Bean创建多个不同的代理对象破坏单例性。3.3 第三级缓存singletonFactories工厂车间/** 三级缓存存放单例工厂对象。 */ private final MapString, ObjectFactory? singletonFactories new HashMap(16);角色工厂车间。这是整个机制中最精妙的一环。它存放的不是Bean对象本身而是一个ObjectFactory?函数式接口。存放对象一个能生产Bean早期引用的工厂。这个工厂的getObject()方法内部通常会执行SmartInstantiationAwareBeanPostProcessor.getEarlyBeanReference()如果存在的话从而在需要的时候才决定是否创建代理对象以及创建什么样的代理对象。触发时机当发生循环依赖其他Bean需要注入当前Bean的引用时Spring会调用这个工厂的getObject()方法来获取早期引用。目的延迟决策将是否创建代理、如何创建代理的决策推迟到真正发生循环依赖、需要早期引用的时候。如果Bean没有循环依赖这个工厂可能永远不会被调用避免了不必要的代理创建开销。解耦将Bean的实例化与可能发生的AOP代理逻辑解耦使得核心创建流程更清晰。3.4 三级缓存协同工作流程图示以A、B循环依赖为例让我们结合一个经典的A、B属性注入循环依赖场景把三级缓存的交互流程串起来开始创建AgetBean(“a”)- 标记A为“创建中” -实例化A调用构造器得到一个原始对象aInstance- 将aInstance包装成一个ObjectFactory并放入三级缓存singletonFactories.put(“a”, factory)。填充A的属性发现A依赖B -getBean(“b”)。开始创建B标记B为“创建中” -实例化B- 将B的工厂放入三级缓存。填充B的属性发现B依赖A -getBean(“a”)。关键步骤B获取A的引用此时检查缓存一级缓存没有A未完成。二级缓存没有A尚未升级。三级缓存有A的工厂 - 调用工厂的getObject()方法。如果A需要AOP代理这里会通过AbstractAutoProxyCreator等处理器生成代理对象aProxy如果不需要则返回原始aInstance。将这个早期引用aEarly放入二级缓存并从三级缓存移除工厂。B成功拿到aEarly完成属性注入。B完成初始化B继续执行后续的初始化回调PostProcessor,InitializingBean等成为一个完整Bean然后从二级缓存移除放入一级缓存。回到A的创建流程此时B已在一级缓存A成功注入B。A继续执行后续的初始化回调。A完成初始化A初始化完成后需要放入一级缓存。此时检查发现二级缓存中还存在aEarly即之前暴露给B的那个引用会用最终初始化好的A或它的代理替换掉二级缓存中的早期引用然后将A放入一级缓存并清理二级缓存。实操心得通过Debug观察三级缓存的变化是理解这个过程的最佳方式。你可以在DefaultSingletonBeanRegistry的getSingleton(String beanName, boolean allowEarlyReference)方法以及AbstractAutowireCapableBeanFactory的doCreateBean方法中设置断点亲眼看着Bean如何在三个缓存间“流动”。4. 为什么是三级两级不行吗这是一个经典的面试题也是理解Spring设计深度的关键。很多人会问既然二级缓存earlySingletonObjects已经存放了早期引用为什么还需要三级缓存singletonFactories这个工厂层直接实例化后就把对象放到二级缓存不行吗答案是为了完美支持AOP代理与循环依赖的共存。让我们分析两种简化方案方案一只有一级缓存完全无法解决循环依赖。因为一级缓存只存放成品Bean而循环依赖的双方在成为“成品”之前都需要对方陷入死锁。方案二只有一级和二级缓存去掉工厂层即Bean实例化后直接将其原始对象或立刻判断并生成代理对象放入二级缓存。这会产生什么问题破坏了AOP代理的懒加载与封装性AOP代理的创建逻辑通常由BeanPostProcessor处理被强制提前到了实例化阶段。即使这个Bean根本没有循环依赖也需要在实例化后立刻判断并生成代理这不符合Spring“在初始化后完成代理”的设计约定也可能带来不必要的性能开销。在复杂场景下可能出错有些BeanPostProcessor的逻辑依赖于Bean的一些Aware接口如BeanNameAware或初始化方法afterPropertiesSet的执行。如果过早生成代理这些依赖的条件可能尚未满足。方案三Spring采用的一、二、三级缓存三级缓存工厂延迟决策。它不立即决定对象最终形态是原始对象还是代理而是保存一个“生产能力”。只有在真正发生循环依赖、有其他Bean来索取引用时才调用工厂方法。此时工厂方法内部可以集成所有必要的处理逻辑如执行getEarlyBeanReference生成一个统一的、确定的早期引用。二级缓存保证单例与性能。一旦通过三级缓存工厂生成了早期引用就把它升级到二级缓存。这样后续再有其他Bean比如C也依赖A在A完全初始化前来索取引用时就可以直接从二级缓存获取而无需重复执行工厂方法保证了早期引用的单例性也提升了性能。所以三级缓存的核心价值在于解耦和延迟决策。它将Bean的实例化、依赖解决、AOP代理等关注点分离使架构更清晰、灵活并能优雅地处理像AOP代理这种需要特殊处理的场景。5. 实操、问题排查与局限性5.1 如何验证与观察循环依赖解决过程代码构造循环依赖Service public class ServiceA { Autowired private ServiceB serviceB; public ServiceA() { System.out.println(ServiceA Constructor); } } Service public class ServiceB { Autowired private ServiceA serviceA; public ServiceB() { System.out.println(ServiceB Constructor); } }启动Spring Boot应用如果控制台正常打印构造器信息且未报错说明循环依赖被成功解决。开启循环依赖检测警告Spring Boot 2.6 默认禁止了循环依赖spring.main.allow-circular-referencesfalse。为了测试你需要在application.properties中开启spring.main.allow-circular-referencestrue但请注意在生产环境中这只是一个临时绕过手段而非解决方案。架构设计上应避免循环依赖。使用Debug工具在IDEA中对DefaultSingletonBeanRegistry类的三个MapsingletonObjects,earlySingletonObjects,singletonFactories设置条件断点或观察点可以最直观地看到Bean在不同缓存间的状态迁移。5.2 常见问题排查实录问题1启动报错BeanCurrentlyInCreationException可能原因1构造器注入导致的循环依赖。这是Spring无法解决的。必须重构代码将至少一方的注入方式改为属性注入或Setter注入。可能原因2Async、Transactional等基于代理的注解在同一个类内部方法调用失效并引发了奇怪的依赖问题。这类注解通常通过AOP代理实现如果A类中的方法a()调用自己的方法b()而b()上有Transactional由于调用走的是this引用而非代理对象事务不生效。如果在这种场景下又混合了复杂的依赖关系可能间接引发问题。考虑将相关方法抽取到另一个Service中。排查步骤仔细阅读异常堆栈找到发生循环的Bean链条。检查它们的注入方式。使用Lazy注解可能临时缓解它创建一个代理延迟注入但根本解决之道是重新设计依赖关系。问题2字段注入为null但Bean明明存在可能原因在构造器或PostConstruct方法中使用了被注入的字段。因为属性注入发生在对象实例化之后、初始化回调之前。如果在构造器里就使用Autowired的字段它肯定是null。Service public class WrongService { Autowired private SomeDependency dependency; public WrongService() { dependency.doSomething(); // 这里dependency为null导致NPE } }解决方案避免在构造器中直接使用依赖字段。如果必须进行初始化操作请将其移至PostConstruct注解的方法中。问题3使用了Lazy但感觉行为不符合预期理解LazyLazy注解可以加在Component、Bean或Autowired上。它的作用不是“解决”循环依赖而是“推迟”依赖的解决。它为依赖项创建一个代理只有当第一次真正使用这个Bean时才会触发它的实际初始化。常见误区Lazy可以解决所有循环依赖。不对。对于构造器注入的循环依赖Lazy也无能为力因为构造器调用时必须提供参数。它主要用于解决属性/Setter注入中某些初始化开销巨大或依赖条件复杂的场景或者作为一种架构上的折衷方案。5.3 三级缓存机制的局限性不支持构造器循环依赖这是根本性限制前文已多次强调。设计时应作为首要规避点。不支持原型Prototype作用域的BeanSpring的三级缓存只针对单例Bean。原型Bean每次getBean都会创建新实例Spring不会缓存它们因此无法解决其循环依赖会直接抛出异常。可能掩盖设计缺陷三级缓存机制非常强大以至于它能让你“侥幸”通过一些不良的依赖设计。过度依赖此机制会导致代码模块间耦合度过高难以测试和维护。一个良好的系统设计应尽可能保持清晰的单向依赖链。初始化顺序的复杂性在复杂的循环依赖网中Bean的初始化后方法PostConstruct,InitializingBean的执行顺序可能变得不确定依赖于容器解决依赖的具体路径这可能会带来隐蔽的bug。6. 最佳实践与架构思考理解了三级缓存的原理和局限性我们应该如何在项目中正确应用和规避呢优先使用构造器注入对于必需依赖Service public class GoodService { private final RequiredDependency dep; // 构造器注入依赖明确不可变易于测试 public GoodService(RequiredDependency dep) { this.dep dep; } }这能强制你在设计时思考依赖的必要性并天然避免了字段注入可能带来的循环依赖。Spring官方也推荐这种方式。使用Autowired的属性/Setter注入时保持警惕当你不得不使用属性注入时心中要绷紧“循环依赖”这根弦。定期通过IDE的依赖分析工具或架构图审视项目中的依赖关系。应用分层与依赖倒置遵循清晰的分层架构如Controller - Service - Repository并利用依赖倒置原则DIP让高层模块定义接口低层模块实现接口从而打破直接的循环依赖。引入中间层或事件驱动模型如Spring Event来解耦强关联的服务。将循环依赖作为重构的警报一旦发现循环依赖不要第一时间想着用Lazy或调整注入方式去掩盖。应该将其视为一个重构代码、优化架构的信号。思考这两个Bean是否职责过于重合能否抽取公共逻辑到第三个组件依赖关系是否可以通过接口或事件进行解耦利用Spring Boot 2.6的严格模式考虑在开发环境中保持spring.main.allow-circular-referencesfalse让循环依赖在启动时就直接失败迫使团队在早期就解决依赖结构问题而不是将问题隐藏到生产环境。三级缓存是Spring框架中一个集精妙设计与实用主义于一身的典范。它不是为了鼓励循环依赖而是为了在复杂的现实业务场景中为开发者提供一个稳健的“安全网”。作为一名开发者我们的目标不应该是熟练运用这张网而应该是通过更好的设计让自己永远不需要掉进这张网里。理解其原理是为了在不得不面对它时能够从容应对并深知其背后的权衡与代价。