1. 从一个经典的面试题说起“Spring是如何解决循环依赖的” 这几乎是每一位Java后端开发者在面试Spring框架时都无法绕开的问题。我第一次被问到这个问题时脑子里瞬间闪过的是“三级缓存”然后试图把网上看来的标准答案复述一遍。但当我真正在项目中遇到一个由循环依赖引发的、极其隐蔽的启动异常时我才发现仅仅知道“三级缓存”这四个字远不足以让我定位和解决问题。那个异常日志指向一个BeanCurrentlyInCreationException但堆栈信息却像一团乱麻让我在Bean定义的海洋里迷失了方向。从那时起我意识到理解循环依赖的原理绝不仅仅是为了应付面试。它关乎我们能否写出更健壮、更易于维护的代码关乎在项目启动失败时我们能否快速、精准地找到问题的根源。Spring通过一套精巧的机制在绝大多数情况下为我们屏蔽了循环依赖的复杂性但这套机制并非万能也有其明确的边界和前提条件。一旦我们的代码设计越过了这些边界Spring就会抛出异常而如果我们不理解背后的原理排查起来将异常痛苦。本文将带你深入Spring IoC容器的核心拆解循环依赖的解决全过程。我们不会停留在“三级缓存”的概念上而是会深入到源码层面看看Spring在创建Bean的每一个关键时刻都做了什么为什么需要三级缓存而不是两级或一级以及那些看似简单的注解如Lazy,Autowired是如何影响这个过程的。更重要的是我会结合自己踩过的坑分享在实际开发中如何避免和排查由循环依赖引发的问题。无论你是正在准备面试还是希望提升对Spring框架的深层理解这篇文章都将为你提供一份详尽的“地图”。2. 循环依赖的本质与Spring的解决边界在深入原理之前我们必须先明确什么是循环依赖以及Spring在什么情况下能解决什么情况下无能为力。2.1 什么是循环依赖循环依赖顾名思义就是两个或多个Bean之间相互依赖形成了一个闭环。最常见的是双向依赖例如Service public class ServiceA { Autowired private ServiceB serviceB; } Service public class ServiceB { Autowired private ServiceA serviceA; }ServiceA的创建需要先注入ServiceB而ServiceB的创建又需要先注入ServiceA这就构成了一个死锁。更复杂的场景可能涉及三个或更多Bean形成A-B-C-A这样的循环链。2.2 Spring的解决前提单例Bean与Setter/字段注入Spring的IoC容器主要是DefaultListableBeanFactory解决循环依赖有一个非常重要的前提条件仅限于单例Singleton作用域的Bean并且是通过Setter方法或字段Field进行注入的情况。为什么构造函数注入无法解决循环依赖这是理解整个机制的关键起点。我们来看一个例子Service public class ServiceA { private final ServiceB serviceB; // 构造函数注入 public ServiceA(ServiceB serviceB) { this.serviceB serviceB; } } Service public class ServiceB { private final ServiceA serviceA; // 构造函数注入 public ServiceB(ServiceA serviceA) { this.serviceA serviceA; } }当Spring尝试创建ServiceA时它发现其构造函数需要一个ServiceB的实例。于是容器转而去创建ServiceB但在创建ServiceB时其构造函数又需要一个ServiceA的实例。此时最初的ServiceA还处在“正在创建”的状态并未完全生成无法提供给ServiceB。这就形成了一个无法打破的僵局Spring会直接抛出BeanCurrentlyInCreationException。注意这里有一个常见的误解认为Spring“不能”解决构造函数注入的循环依赖。更准确的说法是Spring的设计选择是不去解决。因为从设计模式的角度看构造函数循环依赖通常意味着代码结构存在缺陷两个类耦合过于紧密违反了“单一职责”原则。Spring通过抛出异常强制开发者重新审视和优化这类设计。而对于Setter注入或字段注入情况则不同。Bean的实例化调用构造函数和属性填充调用Setter或反射设值是分开的两个步骤。Spring可以先调用无参或默认构造函数把一个“半成品”Bean对象已创建但属性为空实例化出来然后去处理它的依赖。正是这个“半成品”的概念为打破循环提供了可能。2.3 原型Prototype作用域Bean的循环依赖对于原型Prototype作用域的BeanSpring完全无法处理其循环依赖无论是哪种注入方式。这是因为原型Bean每次请求都会创建一个新的实例。假设存在原型Bean A和B的循环依赖当容器为A注入B时会去创建新的B实例而为这个新的B实例注入A时又会触发创建另一个新的A实例……这个过程会无限递归下去直到栈溢出。因此Spring在检测到原型Bean的循环依赖时会直接抛出异常。理解这些边界非常重要它告诉我们Spring的循环依赖解决机制是一把“安全锁”只在特定的、相对安全的场景下生效而不是一个可以随意依赖的“万能钥匙”。在项目设计中我们应当尽量避免循环依赖如果无法避免也应确保其发生在单例Bean且非构造函数注入的范围内。3. 三级缓存机制深度拆解现在让我们进入核心部分Spring到底是如何通过“三级缓存”来解决单例Bean的Setter/字段注入循环依赖的。网上很多文章只给出了三级缓存的名字但很少说清楚每一级缓存具体存的是什么、为什么需要三级、以及它们是如何协同工作的。Spring在DefaultListableBeanFactory中维护了三个重要的Map也就是我们常说的“三级缓存”一级缓存singletonObjectsConcurrentHashMapString, Object。这里存放的是已经完全初始化好的Bean即经历了实例化、属性填充、初始化InitializingBean.afterPropertiesSet、init-method等所有生命周期步骤的“成品Bean”。我们平时从Spring容器getBean拿到的就是这里的对象。二级缓存earlySingletonObjectsHashMapString, Object。这里存放的是提前暴露的“早期引用”。这些Bean已经实例化但可能还未进行属性填充和初始化是一个“半成品”。它的存在主要是为了解决循环依赖过程中对“半成品”进行代理增强的问题。三级缓存singletonFactoriesHashMapString, ObjectFactory?。这里存放的是生产“早期引用”的工厂对象ObjectFactory。这是整个机制中最精妙的一环。工厂对象可以在被调用时决定返回原始的Bean实例还是一个经过AOP代理的Bean实例。3.1 解决循环依赖的全流程推演我们以最经典的A、B循环依赖为例结合源码的关键步骤推演整个过程。假设A依赖BB也依赖A且都是单例采用Autowired字段注入。步骤1开始创建A容器调用getBean(“a”)发现A不在任何缓存中。标记A为“正在创建”singletonsCurrentlyInCreation集合中加入”a”。实例化A通过反射调用A的构造函数在堆内存中创建出A的对象。此时A的属性b为null。我们称这个对象为rawA原始A。暴露早期引用这是最关键的一步。Spring判断A是单例、正在创建、且允许循环依赖默认是允许的于是执行以下操作将一个ObjectFactory工厂对象放入三级缓存singletonFactories。这个工厂的getObject()方法逻辑是如果A需要被AOP代理例如有Transactional注解则调用后置处理器生成代理对象如果不需要则直接返回rawA。注意此时一级和二级缓存都没有A。代码层面对应AbstractAutowireCapableBeanFactory的doCreateBean方法中的addSingletonFactory(beanName, () - getEarlyBeanReference(beanName, mbd, bean))。步骤2为A注入属性B触发创建B开始对rawA进行属性填充populateBean。发现属性b需要注入于是调用getBean(“b”)去获取B的实例。步骤3开始创建B容器调用getBean(“b”)发现B不在任何缓存中。标记B为“正在创建”。实例化B反射创建出B的对象rawB属性a为null。暴露早期引用同样将一个能生产B早期引用的ObjectFactory放入三级缓存。步骤4为B注入属性A打破循环的关键开始对rawB进行属性填充。发现属性a需要注入于是调用getBean(“a”)去获取A的实例。这次调用getBean(“a”)流程与第一次不同发现A被标记为“正在创建”。首先检查二级缓存earlySingletonObjects没有A。然后检查三级缓存singletonFactories找到了在步骤1中放入的工厂对象调用这个工厂的getObject()方法。如果A需要代理此时会生成代理对象proxyA如果不需要则返回rawA。将得到的结果可能是rawA或proxyA放入二级缓存并从三级缓存中移除对应的工厂。将这个早期引用返回给B的注入流程。rawB成功获得了A的早期引用可能是代理对象属性a被成功设置。步骤5完成B的创建B继续完成后续的初始化流程如PostConstruct,InitializingBean。B创建完毕成为一个“成品Bean”。将成品B放入一级缓存singletonObjects并从二级、三级缓存中清理掉B的相关记录。B的getBean流程结束将成品B返回给步骤2中等待的A。步骤6完成A的创建A的属性填充流程步骤2收到了返回的成品B将其注入到rawA的属性b中。A继续完成后续的初始化流程。A创建完毕成为一个“成品Bean”。将成品A放入一级缓存。这里有一个细节在放入一级缓存前Spring会再次检查。因为A的早期引用可能已经被B用过放在了二级缓存所以需要从二级缓存中取出早期引用无论是rawA还是proxyA然后对这个引用执行初始化回调如PostConstruct最终得到的就是放入一级缓存的成品。从二级、三级缓存中清理掉A的相关记录。至此循环依赖被完美解决A和B都成为了完整的Bean存放在一级缓存中供后续使用。3.2 为什么必须是三级缓存两级不行吗这是面试中最常被追问的问题。很多人会想既然二级缓存earlySingletonObjects已经存了早期引用为什么还需要三级缓存singletonFactories这个工厂层为什么不直接在实例化后就把早期引用放到二级缓存关键在于AOP代理的创建时机。如果只有二级缓存我们尝试修改一下流程在实例化A之后直接把rawA对象放入二级缓存。那么当B依赖A时直接从二级缓存拿到rawA注入。这看起来没问题。但是如果A这个Bean需要被AOP代理呢比如A类上标注了Transactional。按照Spring AOP的设计代理对象通常是在Bean初始化完成之后由BeanPostProcessor如AbstractAutoProxyCreator创建的。如果我们在实例化后立刻把rawA暴露出去那么B注入的将是原始的、没有被代理的A对象。而最终初始化完成后放入一级缓存的却是代理对象。这就导致了B持有的A和容器最终管理的A不是同一个对象事务注解等AOP功能在通过B调用A时就会失效。三级缓存中的ObjectFactory就是为了解决这个“代理时机”问题而设计的。它是一个懒加载的工厂。只有当真正发生循环依赖、有其他Bean需要注入A的早期引用时才会调用这个工厂。工厂的getEarlyBeanReference方法会检查该Bean是否需要被代理如果需要就提前创建代理对象并返回如果不需要就返回原始对象。这样就保证了即使发生了循环依赖被注入的早期引用和最终成品的Bean可能被代理过在逻辑上是同一个对象对于代理场景早期返回的就是代理对象本身。所以二级缓存earlySingletonObjects是一个中间缓存用于存放已经确定好的早期引用可能是原始对象也可能是代理对象避免工厂被重复调用。而三级缓存singletonFactories是生产这个早期引用的“方案决策者”它确保了在循环依赖发生时能给出一个正确的、与最终成品兼容的对象引用。4. 从源码角度验证关键流程光有理论推演还不够我们深入到Spring源码以Spring Framework 5.3.x为例中看看几个关键节点是如何实现的。这能帮助我们更牢固地理解整个机制。4.1 暴露早期引用的入口addSingletonFactory在AbstractAutowireCapableBeanFactory.doCreateBean方法中创建Bean实例createBeanInstance之后初始化之前有这样一段代码// 判断是否允许提前暴露早期引用 boolean earlySingletonExposure (mbd.isSingleton() this.allowCircularReferences isSingletonCurrentlyInCreation(beanName)); if (earlySingletonExposure) { if (logger.isTraceEnabled()) { logger.trace(Eagerly caching bean beanName to allow for resolving potential circular references); } // 重点添加SingletonFactory到三级缓存 addSingletonFactory(beanName, () - getEarlyBeanReference(beanName, mbd, bean)); }addSingletonFactory方法很简单就是将beanName和工厂函数存入singletonFactories三级缓存protected void addSingletonFactory(String beanName, ObjectFactory? singletonFactory) { Assert.notNull(singletonFactory, Singleton factory must not be null); synchronized (this.singletonObjects) { if (!this.singletonObjects.containsKey(beanName)) { this.singletonFactories.put(beanName, singletonFactory); this.earlySingletonObjects.remove(beanName); this.registeredSingletons.add(beanName); } } }4.2 获取Bean的核心getSingleton当B在属性填充时需要A会调用getBean(“a”)最终会调用DefaultSingletonBeanRegistry.getSingleton(String beanName, boolean allowEarlyReference)方法。这个方法是解决循环依赖的核心逻辑所在protected Object getSingleton(String beanName, boolean allowEarlyReference) { // 1. 首先从一级缓存成品缓存查找 Object singletonObject this.singletonObjects.get(beanName); // 如果没找到并且该Bean正在创建中... if (singletonObject null isSingletonCurrentlyInCreation(beanName)) { synchronized (this.singletonObjects) { // 2. 从二级缓存早期引用缓存查找 singletonObject this.earlySingletonObjects.get(beanName); if (singletonObject null allowEarlyReference) { // 3. 从三级缓存工厂缓存查找 ObjectFactory? singletonFactory this.singletonFactories.get(beanName); if (singletonFactory ! null) { // 调用工厂的getObject方法获取早期引用可能是代理对象 singletonObject singletonFactory.getObject(); // 将获取到的引用放入二级缓存 this.earlySingletonObjects.put(beanName, singletonObject); // 从三级缓存移除该工厂 this.singletonFactories.remove(beanName); } } } } return singletonObject; }这段代码清晰展示了“三级缓存”的查询顺序和升级逻辑一级 - 二级 - 三级。当从三级缓存获取到对象后会将其升级到二级缓存并删除三级缓存中的工厂。这保证了每个Bean的工厂最多只被调用一次。4.3 提前生成代理getEarlyBeanReference那么三级缓存中的工厂函数getEarlyBeanReference到底做了什么我们看AbstractAutowireCapableBeanFactory中的实现protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) { Object exposedObject bean; if (!mbd.isSynthetic() hasInstantiationAwareBeanPostProcessors()) { for (BeanPostProcessor bp : getBeanPostProcessors()) { if (bp instanceof SmartInstantiationAwareBeanPostProcessor) { SmartInstantiationAwareBeanPostProcessor ibp (SmartInstantiationAwareBeanPostProcessor) bp; // 关键调用后置处理器的getEarlyBeanReference方法 exposedObject ibp.getEarlyBeanReference(exposedObject, beanName); } } } return exposedObject; }它会遍历所有BeanPostProcessor找到SmartInstantiationAwareBeanPostProcessor类型的处理器并调用其getEarlyBeanReference方法。在Spring AOP中AbstractAutoProxyCreator就实现了这个接口。它的getEarlyBeanReference方法会检查该Bean是否需要被代理如果需要则提前创建并返回代理对象如果不需要则直接返回原始Bean。正是这个步骤确保了在循环依赖发生时被注入的早期引用已经是一个“正确”的对象可能是代理与Bean完全初始化后经过正常AOP流程产生的对象保持一致。5. 实战中的避坑指南与排查技巧理解了原理最终要服务于实践。在实际开发中我们如何避免循环依赖问题出了问题又该如何高效排查5.1 如何避免循环依赖虽然Spring提供了解决机制但循环依赖本质上是一种代码设计上的“坏味道”Code Smell表明类之间的职责边界可能不够清晰。以下是一些避免策略使用构造函数注入这是最有效、最推荐的方式。如前所述Spring对构造函数注入的循环依赖会直接报错这迫使你在编码阶段就发现并消除循环依赖。这符合“快速失败”Fail-Fast的原则。重新审视设计出现循环依赖往往意味着两个类耦合过紧。考虑是否可以通过以下方式解耦提取公共逻辑到第三个类将A和B相互依赖的部分提取到一个新的Service C中让A和B都依赖C。使用事件驱动将A调用B或B调用A的某些操作改为发布一个事件由对方监听并处理。这样双方不再直接依赖。使用回调或接口有时依赖是单向的可以通过接口回调如InitializingBean的afterPropertiesSet在Bean准备好后再设置关系但这需谨慎使用。使用Lazy注解在注入点添加Lazy注解。这告诉Spring延迟初始化被依赖的Bean或者注入一个代理对象。当实际调用被依赖Bean的方法时代理才会触发真实Bean的初始化。这可以打破某些特定场景下的循环依赖。但Lazy更像是一种“缓兵之计”可能会掩盖设计问题并带来一些微妙的运行时行为需谨慎使用。Service public class ServiceA { Autowired Lazy // 延迟注入ServiceB private ServiceB serviceB; }5.2 循环依赖问题排查实战当你面对一个BeanCurrentlyInCreationException时不要慌张。按照以下步骤可以快速定位问题根源第一步阅读异常堆栈Spring的异常信息通常比较友好。异常信息中会包含类似Requested bean is currently in creation: Is there an unresolvable circular reference?的提示并会列出涉及循环的Bean名称。仔细看BeanCurrentlyInCreationException后面的消息和堆栈找到第一个被提及的Bean。第二步分析Bean的依赖关系根据异常信息中的Bean名称去你的代码中找到对应的类。检查它们的依赖注入方式如果是构造函数注入那么这就是根本原因Spring无法解决。如果是字段/Setter注入继续下一步。第三步检查Bean的作用域确认涉及的Bean都是单例Scope(“singleton”)默认即是。如果其中有原型Scope(“prototype”)Bean那么循环依赖也无法解决。第四步使用Spring的调试信息在application.properties或application.yml中开启更详细的Bean创建日志有助于观察创建过程。logging.level.org.springframework.beans.factory.supportDEBUGDEBUG日志会打印大量的Bean生命周期事件包括何时开始创建、何时放入缓存、何时注入属性等。你可以从中看到Bean创建的顺序和卡住的位置。第五步绘制依赖图对于复杂的循环链超过两个Bean在纸上或使用工具画出类之间的依赖关系图。从异常信息中最早出现的Bean开始根据Autowired字段一步步画出依赖箭头很快就能找到闭环。第六步审查配置与后置处理器极少数情况下问题可能出在自定义的BeanPostProcessor或BeanFactoryPostProcessor中。这些处理器如果自身依赖于其他Bean或者其处理逻辑改变了Bean的创建顺序也可能引发奇怪的循环依赖问题。检查是否有自定义的处理器。我遇到过最棘手的一个循环依赖问题不是直接的A-B-A而是A-B-C-Async-A。其中C的一个方法被标注了AsyncSpring会为C创建一个代理。这个代理的创建时机与普通的AOP代理不同干扰了三级缓存的正常逻辑导致了一个非常隐蔽的循环依赖异常。最终是通过分析DEBUG日志并查阅Async代理的创建源码才定位到问题。这个案例告诉我对于Spring那些通过代理实现的特性如Transactional,Async,Cacheable在循环依赖场景下要格外小心。6. 进阶话题与AOP、事务等特性的交互循环依赖的解决机制与Spring的其他特性尤其是基于代理实现的特性AOP、声明式事务Transactional、异步Async等有着密切的交互。理解这些交互能帮助你预判和解决更复杂的问题。6.1 AOP代理与循环依赖的协同正如我们在三级缓存原理中提到的AbstractAutoProxyCreator作为SmartInstantiationAwareBeanPostProcessor参与了getEarlyBeanReference的过程。这意味着如果一个Bean被AOP切面匹配例如有Transactional那么在发生循环依赖时被提前注入的引用就已经是代理对象了。这带来一个关键影响Bean的初始化生命周期方法如PostConstruct,InitializingBean.afterPropertiesSet是在代理对象上执行的还是在原始对象上执行的答案是在原始对象上执行。代理对象包装了原始对象。Spring在完成属性填充后会对早期暴露的引用可能是代理执行初始化回调。这些回调方法是通过代理最终调用到原始对象的方法上的。这保证了初始化逻辑只执行一次且是在正确的目标上执行。6.2Async方法带来的特殊挑战Async注解的处理与普通AOP略有不同。它通常由AsyncAnnotationBeanPostProcessor或AnnotationAsyncExecutionInterceptor处理。这个处理器也可能实现SmartInstantiationAwareBeanPostProcessor接口。问题在于Async代理的创建时机可能更晚或者在处理循环依赖时逻辑与AbstractAutoProxyCreator不完全一致。这就可能导致在复杂的循环依赖场景下Spring无法正确地将早期引用升级为Async代理从而引发异常。我踩过的那个坑正是如此。应对策略对于包含Async方法的Bean尽量避免让其陷入复杂的循环依赖链。如果无法避免可以尝试显式地使用Lazy注解在注入点进行修饰或者考虑将Async方法抽取到一个独立的、不参与循环的Bean中。6.3 构造函数注入与Lazy的组合拳前面提到构造函数注入无法解决循环依赖。但有一个“例外”情况当在构造函数的参数上使用Lazy注解时。Service public class ServiceA { private final ServiceB serviceB; public ServiceA(Lazy ServiceB serviceB) { // 构造函数参数使用Lazy this.serviceB serviceB; } }这种情况下Spring注入给ServiceA的并不是一个真实的ServiceB实例而是一个ServiceB的代理对象。这个代理对象会延迟ServiceB的真实初始化。因此在创建ServiceA时并不需要立即去初始化ServiceB从而打破了循环。当ServiceA首次调用serviceB的某个方法时代理才会触发ServiceB的完整创建流程。这是一种有效的技术手段但它同样掩盖了设计上的耦合。它适用于那些确实有紧密逻辑关系但又必须使用构造函数注入来保证不可变性的场景。使用时需要清楚你注入的是一个代理这可能会对调试和某些反射操作产生影响。7. 总结与最佳实践思考深入剖析Spring循环依赖的原理不仅仅是为了理解一个面试知识点更是为了掌握一种设计思维和排查复杂问题的能力。回顾整个过程我们可以提炼出几个核心要点和最佳实践理解机制的前提是理解边界Spring只解决单例Bean、非构造函数注入的循环依赖。这是设计上的取舍构造函数注入的循环依赖被禁止正是为了促使我们写出更松耦合的代码。三级缓存的核心价值是处理代理一级存成品二级存早期引用三级存生产早期引用的工厂。工厂的存在是为了在循环依赖发生时能够决策并返回一个正确的对象可能是原始对象也可能是代理对象保证依赖注入的一致性和AOP功能的正常。循环依赖是设计上的警示灯尽管Spring提供了解决机制但在项目中频繁出现循环依赖尤其是复杂的多Bean循环通常意味着模块职责划分不清。应当优先考虑通过重构提取公共组件、使用事件、接口隔离等来消除循环而不是依赖Spring的机制。善用工具和日志进行排查当遇到BeanCurrentlyInCreationException时系统地按照“看异常 - 查注入方式 - 核作用域 - 开调试日志 - 画依赖图”的流程进行大部分问题都能快速定位。对于涉及AOP、Async等特性的复杂问题需要结合源码和特性本身的创建逻辑进行分析。谨慎使用LazyLazy是一个强大的工具可以打破许多僵局。但它引入了“延迟初始化”和“代理”的复杂性可能会带来意想不到的副作用如配置顺序问题、调试困难。将其作为设计优化后的补充手段而非首选方案。在我个人的经验中坚持使用构造函数注入作为主要注入方式迫使团队在编码阶段就思考依赖关系的合理性是提升代码质量非常有效的一环。当项目因为历史原因确实存在一些难以消除的循环依赖时对Spring这套机制的理解能让我们在遇到问题时心中有数快速找到那条走出迷宫的正确路径。Spring的优雅之处在于它用一套看似复杂的缓存机制为开发者处理了依赖管理中最棘手的闭环问题而我们要做的是在享受这份便利的同时不忘追求更清晰、更稳健的代码设计。