
Spring 循环依赖这个问题我估计每个做 Java 开发的人都遇到过。要么是在面试题里看到它要么是项目启动的时候突然给你抛一个BeanCurrentlyInCreationException然后一脸懵。说句实在话这个问题从 Spring 诞生那天起就是高频热点但很多人对它的理解停留在“三级缓存”这个名词上脑子里大概知道有个三级缓存能解决循环依赖但再往深问一层——为什么是三级而不是两级一级缓存直接放成品不行吗就卡壳了。这篇博文我就把循环依赖从表象到原理、从源码到实践完整拆一遍。包括 Spring 到底在什么条件下能解决循环依赖、三级缓存每层各自管什么、为什么 AOP 代理非要搞一个单独的第三级来兜底以及 Spring Boot 2.6 之后默认禁止循环依赖你应该怎么办。内容会穿插源码层面的逻辑和实用的排查经验适合正在准备面试的开发者也适合那些项目里真遇到了循环依赖报错、正愁怎么改代码的人。1. 循环依赖的本质先搞清楚 Spring 在解决什么问题1.1 什么叫做循环依赖循环依赖简单说就是 Bean A 的创建过程中需要注入 Bean B而 Bean B 的创建过程中又反过来需要注入 Bean A两者互相等待对方先创建完形成一个死锁般的闭环。除了这种最常见的两者互相引用还有 A 依赖 B、B 依赖 C、C 又依赖 A 的三角循环原理一样。Component public class A { private final B b; public A(B b) { this.b b; } } Component public class B { private final A a; public B(A a) { this.a a; } }这种代码写出来Spring 启动时就会直接报错。但是同样的互相引用如果你把构造器注入改成Autowired字段注入或者 setter 注入Spring 又能正常启动。这种差别恰恰是理解循环依赖解决方案的关键切入点。从本质上讲循环依赖之所以能解决靠的既不是魔法也不是死等而是 Spring 把“创建 Bean”这件事拆成了两个阶段实例化和初始化。实例化就是调用构造器 new 出一个原始对象此时对象还是空壳依赖属性都还没赋值。初始化则是填充属性、执行各种 Aware 回调、BeanPostProcessor 增强等。两个阶段之间有一个时间差Spring 正是在这个时间差里做了手脚——把还没初始化完成的早期对象提前暴露出去让依赖它的另一个 Bean 先拿着用。这件事的生活化类比很直观你点了一份外卖商家先把你点的饮料做好送到你手上让你先喝着主菜还在锅里炒。你拿到饮料时它不是完整的套餐但能解渴于是你把“收到餐”这件事标记完成了结果就是整个流程没有被卡死。Spring 解决循环依赖的做法本质上就是“先把半成品给你后面再补齐”。1.2 哪些循环依赖能解决哪些不能先说结论Spring 默认能解决的循环依赖必须同时满足两个条件Bean 是单例singleton、依赖注入方式是字段注入或 setter 注入。这两个条件缺一个都不行。为什么 singleton 是硬前提因为只有单例 Bean 才会被放进缓存里供后续复用每次getBean都新建的 prototype Bean 根本没有缓存的概念你 A 要 B、B 又要 A两个都是原型谁也不存在于缓存中只能无限循环创建下去直到内存溢出。所以 prototype Bean 的循环依赖 Spring 直接放弃治疗。为什么构造器注入不行用一个半小时能讲清楚。构造器注入时new这个动作是发生在实例化阶段的一个 Bean 在构造器里就把依赖要齐了但此时它自己连对象都还没 new 出来根本没有“半成品”可以提前暴露。拿前面的 A 和 B 举例Spring 创建 A 需要先调用 A 的构造器构造器参数里有 B于是去创建 BB 的构造器里又有 A回去找 A 发现 A 还在创建中、没有成品也没有半成品直接抛异常。只要有一个依赖是构造器注入整个链路就会被卡死。还有一个容易忽略的坑Async、Transactional这类注解引发的循环依赖问题。A 依赖 BB 依赖 AB 上标了Async按理说是字段注入应该能解决但实际启动时报错或者 A 拿到的 B 里异步方法不生效。原因和 AOP 代理的创建时机有关这块下面专门用一小节说。2. 三级缓存机制拆解每一层都是干嘛的2.1 三个 Map 各管一件事Spring 解决循环依赖的“基础设施”是 DefaultSingletonBeanRegistry 里的三个缓存代码层面就是三个 ConcurrentHashMap。源码里长这样// 一级缓存存放创建完成的成品 Bean private final MapString, Object singletonObjects new ConcurrentHashMap(256); // 二级缓存存放提前暴露的早期 Bean半成品 private final MapString, Object earlySingletonObjects new ConcurrentHashMap(16); // 三级缓存存放 Bean 的 ObjectFactory也就是生成早期 Bean 的工厂 private final MapString, ObjectFactory? singletonFactories new HashMap(16);三个缓存虽然都是 Map但定位完全不同。一级缓存singletonObjects是最终存放处里面所有 Bean 都已经完成了初始化、属性填充、AOP 代理等全流程可以直接被getBean拿走使用。二级缓存earlySingletonObjects是临时的半成品停放区里面放的是实例化完成但初始化未完成的 Bean 原始对象或者说是提前生成好的代理对象。三级缓存singletonFactories是最特殊的一层它不存 Bean 对象本身存的是ObjectFactory也就是一个用来生成 Bean 的工厂函数。这个设计的精妙之处在于第三级。为什么要存工厂而不是直接存对象因为 Spring 无法在实例化刚完成时就知道这个 Bean 最终会不会被 AOP 代理。如果直接存原始对象到二级缓存后面发现需要代理又得替换如果直接执行 AOP 生成代理对象存着又可能导致所有 Bean 不管有没有循环依赖都提前被代理一遍性能损耗不可接受。用ObjectFactory延迟决定谁需要谁去调工厂拿结果恰好解决了这个矛盾。2.2 三级缓存的核心方法 addSingletonFactory三级缓存是在什么时候写入的答案藏在AbstractAutowireCapableBeanFactory的doCreateBean方法里。一个 Bean 完成了实例化也就是构造器执行完之后Spring 马上会做一件事把用于创建早期 Bean 的ObjectFactory放入三级缓存。// 源码简化版实例化完成后的关键代码 if (earlySingletonExposure) { addSingletonFactory(beanName, () - getEarlyBeanReference(beanName, mbd, bean)); }这段代码里的 lambda 表达式就是第三级缓存里存的那个工厂。注意它调用的getEarlyBeanReference是核心中的核心它的作用是如果有SmartInstantiationAwareBeanPostProcessor类型的后置处理器存在就调用它们的getEarlyBeanReference方法给 Bean 一次提前被代理的机会。举个例子。假设 B 依赖 AA 正在创建过程中已经实例化完成、放进三级缓存了。此时 Spring 去创建 BB 的某个字段需要注入 A于是触发getSingleton(A)。查找时一级缓存没有、二级缓存没有但在三级缓存里找到了 A 对应的工厂。Spring 调用这个工厂工厂内部执行了getEarlyBeanReference如果 A 需要 AOP 代理这个过程中会创建出 A 的代理对象然后把这个代理对象放入二级缓存同时从三级缓存中移除。之后 B 拿到的就是 A 的代理对象或者原始对象取决于需不需要代理。这里有一个很多人会忽略的细节如果某个 Bean 没有循环依赖三级缓存里的工厂根本不会被调用这个 Bean 最终走的是正常初始化流程在initializeBean里通过postProcessAfterInitialization完成 AOP 代理。三级缓存中的工厂在 Bean 初始化完成后也会被清理掉。这才是三级缓存最优雅的地方——懒但是聪明。2.3 为何必须是三级两级甚至一级行不行这是一道非常经典的面试追问也恰恰是很多人说不清楚的地方。先说结论如果完全不考虑 AOP 代理一级缓存加三级缓存中的工厂也就是两级缓存就能解决循环依赖。如果把 AOP 代理也考虑进来三级缓存是必要且充分的。一级缓存直接只存成品行不行不行。A 处于创建中时还没有成品B 依赖 A 拿不到对象循环仍然卡死。必须有一个地方能拿到早期对象。那一级缓存存早期对象、二级缓存存成品行不行我们可以模拟一下A 实例化后直接放入一级缓存B 依赖 A 时从一级缓存拿到早期原始对象注入成功。等到 B 创建完成后再回来继续处理 A 的初始化最后把 A 的成品也放入一级缓存。但这里出现了一个致命问题如果 A 需要 AOP 代理B 拿到的 A 是原始对象而最终容器里的 A 是代理对象完成后容器里存在两份 AB 持有的还是那个没有增强的原始对象Transactional、Async之类的功能全部失效。必须在 B 拿 A 的时候就知道 A 将来要代理并提前生成代理对象交给 B。那两级缓存加一个提前判断能不能行也就是实例化后马上判断这个 Bean 是否需要 AOP需要则立即生成代理不需要则存原始对象。逻辑上可行但有副作用。Spring 里判断一个 Bean 是否需要 AOP需要经过AbstractAutoProxyCreator的后置处理逻辑而此时 Bean 的属性还没有填充、初始化方法还没有执行很多切面判断依赖的上下文信息还不完整。提前创建代理很可能因为切面条件未满足而判断错误或者强行把所有 Bean 都代理一遍导致无谓的性能开销。三级缓存通过ObjectFactory把“是否提前创建代理”的决定延迟到了“真正有人需要这个早期对象”的瞬间既保证了代理对象的正确性又保证了性能。所以本质问题不是容器放不下三个 Map而是 Spring 在不确定性和及时性之间选了一个精妙平衡点。3. getSingleton 核心流程与手写 Demo 验证3.1 源码级流程拆解理解了三个缓存的分工接下来要看它们是怎么配合完成一次循环依赖的“营救”。Spring 的getSingleton方法内部逻辑可以简化为以下步骤第一步先查一级缓存。如果一级缓存中有目标 Bean直接返回。此时 Bean 是完成品没有任何问题。第二步如果一级缓存没有但这个 Bean 正在创建中也就是singletonsCurrentlyInCreation里包含它的名字则去查二级缓存。二级缓存命中则直接返回这是之前被提前暴露过的早期对象。第三步如果二级缓存也没有则从三级缓存中取ObjectFactory调用getObject()方法得到早期对象引用。得到对象后放入二级缓存并从三级缓存中删除该工厂。// 简化后的核心逻辑 protected Object getSingleton(String beanName, boolean allowEarlyReference) { Object singletonObject this.singletonObjects.get(beanName); if (singletonObject null isSingletonCurrentlyInCreation(beanName)) { singletonObject this.earlySingletonObjects.get(beanName); if (singletonObject null allowEarlyReference) { ObjectFactory? singletonFactory this.singletonFactories.get(beanName); if (singletonFactory ! null) { singletonObject singletonFactory.getObject(); this.earlySingletonObjects.put(beanName, singletonObject); this.singletonFactories.remove(beanName); } } } return singletonObject; }注意一个细节三级缓存里存的ObjectFactory在调用getObject()的同时就会被移除之后对象被升格到二级缓存。这意味着整个生命周期中这个工厂只会被调用一次同一个早期对象不会被重复创建。这也带来一个问题如果 Bean 在循环依赖中已经被提前代理后续初始化完成时的postProcessAfterInitialization不会再重复代理Spring 会判断当前对象已经是代理对象而跳过。这个机制保证了代理对象的唯一性。3.2 手写一个精简版三级缓存光看流程还是有一点抽象我写一个极简的模拟代码帮助你理解整个运行过程。这个 Demo 保留核心设计思想去掉 Spring 各种繁琐的后置处理。public class CircularDependencyDemo { static class BeanA { private BeanB b; public void setB(BeanB b) { this.b b; } Override public String toString() { return BeanA{ b b }; } } static class BeanB { private BeanA a; public void setA(BeanA a) { this.a a; } Override public String toString() { return BeanB{ a a }; } } // 模拟 Spring 容器 static MapString, Object singletonObjects new HashMap(); static MapString, Object earlySingletonObjects new HashMap(); static MapString, ObjectFactory? singletonFactories new HashMap(); static SetString singletonsCurrentlyInCreation new HashSet(); public static void main(String[] args) throws Exception { // 创建 BeanA注意箭头函数里就是 getEarlyBeanReference 的模拟 createBean(beanA, BeanA.class); // 创建 BeanB createBean(beanB, BeanB.class); Object a getSingleton(beanA); Object b getSingleton(beanB); System.out.println(a); System.out.println(b); System.out.println(beanA 中注入的 beanB 是否为容器中的 beanB ((BeanA) a).b b); System.out.println(beanB 中注入的 beanA 是否为容器中的 beanA ((BeanB) b).a a); } static void createBean(String name, Class? clazz) throws Exception { // 标记正在创建 singletonsCurrentlyInCreation.add(name); // 实例化调用无参构造器创建原始对象 Object bean clazz.getDeclaredConstructor().newInstance(); System.out.println(实例化 name - bean); // 放入三级缓存这里用 () - bean 模拟工厂 singletonFactories.put(name, () - bean); System.out.println(已将 name 的 ObjectFactory 放入三级缓存); // 初始化阶段填充属性模拟依赖注入 if (bean instanceof BeanA) { BeanB b (BeanB) getSingleton(beanB); ((BeanA) bean).setB(b); } else if (bean instanceof BeanB) { BeanA a (BeanA) getSingleton(beanA); ((BeanB) bean).setA(a); } // 初始化完成从三级缓存移除工厂放入一级缓存 singletonFactories.remove(name); singletonObjects.put(name, bean); singletonsCurrentlyInCreation.remove(name); System.out.println(初始化完成 name 放入一级缓存); } static Object getSingleton(String name) { Object bean singletonObjects.get(name); if (bean ! null) return bean; if (singletonsCurrentlyInCreation.contains(name)) { bean earlySingletonObjects.get(name); if (bean null) { ObjectFactory? factory singletonFactories.get(name); if (factory ! null) { bean factory.getObject(); earlySingletonObjects.put(name, bean); singletonFactories.remove(name); } } } return bean; } }这段代码直接跑了就能看到效果实例化 beanA 后放入三级缓存填充属性时发现需要 beanB开始创建 beanB同样实例化后放入三级缓存填充属性时通过getSingleton(beanA)发现 beanA 在创建中于是从三级缓存拿到早期对象完成注入。最后 beanB 顺利创建完回到 beanA 继续填充。所有依赖都指向同一个对象。这个 Demo 很粗糙但已经完整演示了 Spring 循环依赖解决的核心骨架逻辑。建议你亲手跑一遍把每个 println 的输出顺序看一遍关于“半成品先给出去”这件事你就会形成肌肉记忆。4. 实操中的各种坑为什么有时候就是解决不了4.1 Spring Boot 2.6 之后默认禁止循环依赖如果你用的是 Spring Boot 2.6 及以上版本即使代码是字段注入启动时也会看到一个醒目的警告提示你检测到了循环依赖并且明确说明循环依赖默认不再被允许。新版 Spring 在DefaultListableBeanFactory里做了一层校验创建 Bean 时如果发现有循环引用直接抛异常。这不是说 Spring 解决不了循环依赖了而是官方决定把这个能力默认关掉。原因很现实循环依赖本质上是设计上的坏味道它让 Bean 之间的耦合变得隐晦难懂也让一些 AOP 场景下的行为变得诡异。Spring Boot 官方宁愿让开发者被迫重构代码也不希望大家靠容器的巧妙设计来掩盖糟糕的类结构。如果你确实因为历史原因暂时改不了代码可以临时在配置文件里打开开关spring.main.allow-circular-referencestrue注意这只是应急手段。打开这个开关后循环依赖能跑起来但潜在的 AOP 代理隐患依然存在后面该重构还得重构。4.2 Async 与 AOP 代理的冲突这个坑非常隐蔽我当年在真实项目里踩过一次。场景是一个服务类上有Async方法另一个类依赖它而这个服务类自身又反过来依赖了那个类形成了循环依赖。启动时倒是没有直接报循环依赖错误但异步方法始终不生效调用后直接就同步执行了排查了很久才定位到问题是循环依赖和 AOP 代理的交互。原因在前面提到过Async的代理是通过AsyncAnnotationBeanPostProcessor在postProcessAfterInitialization阶段增强 Bean 的。如果一个 Bean 在循环依赖中被提前暴露那么暴露出去的早期对象是从三级缓存的工厂里拿到的此时的getEarlyBeanReference并不会触发Async的代理创建。最终结果就是依赖方拿到的对象是没有异步增强的原始对象而容器中的成品是增强了异步能力的代理对象。两边不一致异步自然失效。这种场景如果确实绕不开有两个思路。一是打破循环依赖让异步类不要反向依赖它的调用方或者反向依赖改为Lazy注入。二是用Lazy注解标注循环依赖中的某一方让 Spring 延迟创建。Lazy在这个场景下特别管用它不要求目标 Bean 提前暴露而是注入一个懒加载代理真正使用时才去解析目标 Bean。4.3 常见报错消息与排查思路速查表报错特征可能原因处理方式BeanCurrentlyInCreationException构造器注入循环依赖或 prototype 循环依赖改字段注入/setter 注入或重新设计依赖关系The dependencies of some of the beans in the application context form a cycleSpring Boot 2.6 默认禁止循环依赖设置allow-circular-referencestrue然后尽快重构BeanNotOfRequiredTypeException循环依赖加 AOP 导致注入的是原始对象而非代理优先打破循环或使用Lazy延迟依赖方Asynchronous method not executedAsyncBean 被提前暴露导致增强失效打破循环或Lazy注入排查循环依赖问题时不要只盯着报错信息要顺着依赖链往上捋。一个比较实用的做法是打断点看 Spring 的singletonsCurrentlyInCreation集合那里会按创建顺序记录当前正在创建的 Bean 名字循环链路一目了然。也可以用 IDE 的依赖分析插件画一下 Bean 之间的引用关系图往往一张图胜过看半天代码。5. 循环依赖的规避手段与设计层面的替代方案5.1 重构优先用设计消灭循环依赖说实话循环依赖能解决是 Spring 的本事但依赖环本身是代码设计的问题。官方在文档里都建议开发者尽量避免循环依赖理由很充分循环依赖意味着两个模块谁也离不开谁后续任何一个类的修改都可能引起连锁反应单元测试和模块复用也变得困难。最常见的重构手段是抽中间层。A 和 B 互相依赖往往说明它们共同依赖了某些公共能力或数据模型。把这些公共部分抽成一个单独的 C让 A 和 B 都只依赖 C循环结构就变成了树状结构。这个方案需要一定的业务分析能力但长期收益最大。其次是事件驱动解耦。A 调用 B 的方法获取结果后做进一步处理反过来 B 又调 A 的方法这种双向调用在业务里经常可以用事件发布订阅模式替代。A 发布一个事件B 作为监听者消费事件依赖方向就变成单向的了。Spring 的ApplicationEventPublisher天然支持这个玩法。事件驱动还能顺手降低方法调用的实时耦合是个一举多得的方案。还有一种是用依赖查找替代依赖注入。具体做法是保留字段上的ObjectProviderT或ApplicationContext在使用时再去取 Bean。这不算最佳实践但在代码迁移的过渡期可以作为应急手段至少比打开allow-circular-references更可控。5.2 Lazy 注入的正确使用姿势Lazy注解是解决循环依赖的一个捷径它的作用和三级缓存完全不一样。三级缓存是通过提前暴露对象让循环链路能走下去而Lazy的思路是切断链路B 依赖 A 的时候不直接拿 A而是注入一个 A 的代理对象等 B 真正调用 A 的方法时才去容器里寻找 A 的成品。用法非常简单Component public class B { Lazy Autowired private A a; }注意Lazy写在字段上作用范围是本字段。如果写在类上则整个类所有字段的注入都会被延迟。Lazy对构造器注入同样适用所以即使是构造器注入的循环依赖给其中一个构造器参数标上Lazy也能打破死循环。但代价是每次通过懒加载代理访问目标对象时会有一次额外的方法分派开销而且调试时对象状态变得不那么直观。我一般建议Lazy只作为过渡方案尤其在遗留系统改造期比直接改类结构要安全得多风险可控。5.3 设计层面的三板斧除了上面两种直接方案还有一套设计层面的经验可以分享。首先是单向依赖原则写代码前先画一下模块依赖图确保依赖关系是有向无环图。其次是接口隔离类之间不要直接互依赖而是通过接口定义契约具体实现类可以单独管理。最后是分层次初始化有些所谓的循环依赖其实是因为初始化顺序没有安排好把初始化逻辑用PostConstruct拆开把需要对方参与的部分延后执行也能避免互相依赖。经验是真正需要在代码层面用三级缓存来解决循环依赖的场景非常少大部分所谓的循环依赖都能通过改结构或者延迟到使用阶段解决。Spring 的三级缓存机制作为源码面试题来研究非常有价值但在业务代码里重点永远是防患于未然而不是事后靠容器的技巧来兜底。6. 面试延伸一分钟讲清楚三级缓存为什么是“必要且充分”的这篇文章最后再补充一段面试向的内容因为“Spring 循环依赖”简直是 Java 面试里的必考题而且追问层次很深。面试官一般会先让你画一下三级缓存的结构然后把问题层层递进第一层三个缓存分别存什么这一个问题考察的是基础概念答清楚就行。第二层为什么需要三级缓存而不是两级这里要答出 AOP 代理的关键矛盾——三级缓存里的ObjectFactory将代理创建延迟到了真正发生循环依赖的瞬间。第三层如果我去掉三级缓存只用两级能解决所有场景吗这里需要你想到 AOP 提前代理的问题由此引出getEarlyBeanReference和getObject()的设计意图。第四层构造器注入的循环依赖为什么无法解决要答出实例化和初始化的时间差以及构造器注入在实例化阶段就需要依赖此时早期对象还未产生的根本原因。还有最后一类的偏门问题如果 Spring 里一个 Bean 的代理被提前创建了后续初始化时的代理后置处理器还会再代理一次吗答案是不会AbstractAutoProxyCreator中有一个earlyProxyReferences集合专门记录已经被提前代理过的 Bean初始化后见是这个集合里的对象直接跳过代理逻辑。这个细节知道的人不多讲出来挺加分的。从源码到实践从机制到坑点Spring 循环依赖这个问题其实并不复杂复杂的是把每个环节里的“为什么”都想清楚。我自己的经验是与其死记硬背三级缓存不如亲手写一个简化 Demo再实际造一个工程里跑一遍循环依赖、看一眼缓存里的内容变化。这个过程走完面试也好、排查也罢都会顺畅很多。