在构建复杂的企业级应用时,我们经常会遇到各种棘手的问题,而“循环依赖”无疑是 Spring 开发者最容易感到困惑,也最常面临的挑战之一。你是否曾经看到过控制台疯狂输出红色的错误堆栈,最终落脚点是一个令人头疼的 BeanCurrentlyInCreationException?别担心,在这篇文章中,我们将不仅仅停留在表面,而是像经验丰富的架构师一样,深入 Spring 的内核,去探索循环依赖产生的根本原因。我们会一起剖析它为什么会对应用造成危害,更重要的是,我们将掌握多种行之有效的解决方案,并结合2026年的技术视角,探讨如何在现代化开发流中利用 AI 辅助和架构演进来彻底根治这一问题。
循环依赖的本质:高耦合的代价
首先,让我们从最基础的概念入手。在软件工程中,耦合度是衡量代码质量的重要指标,而循环依赖则是“高耦合”的一种极端表现。简单来说,循环依赖是指两个或多个模块(或对象)之间存在一种闭环的依赖关系。想象一下,如果类 A 需要类 B 才能工作,而类 B 反过来又必须依赖类 A,这就形成了一个死结。
当我们把视角切换到 Spring 框架(或者更准确地说是 IoC 容器)时,这个问题就变得更加具体了。在 Spring 中,我们讨论的是 Bean 之间的循环依赖。让我们通过一个简单的模型来直观地理解一下:
> Bean A 依赖于 Bean B
> Bean B 反过来依赖于 Bean A
这就构成了一个最简单的闭环:A → B → A。
你可能会问:“这看起来很自然,为什么 Spring 处理不了呢?”为了回答这个问题,我们需要深入了解 Spring 容器在启动时是如何创建和组装 Bean 的。
Spring 容器的创建逻辑与死锁困境
Spring 容器在启动时,会读取我们的配置元数据(注解或 XML 文件),然后尝试实例化所有定义的 Bean。在这个过程中,Spring 遵循一个原则:在创建一个 Bean 之前,必须先创建它所依赖的所有 Bean。
让我们先看一个没有循环依赖的正常场景:
> Bean A → Bean B → Bean C
在这个链路中,Spring 会非常聪明地识别出创建顺序:
- Spring 发现 A 依赖 B,于是暂停创建 A,去创建 B。
- 在创建 B 时,发现 B 依赖 C,于是暂停创建 B,去创建 C。
- C 没有任何依赖,Spring 成功实例化 C。
- C 实例化完成,Spring 回过头来继续完成 B 的初始化(将 C 注入 B)。
- B 初始化完成,Spring 最后完成 A 的初始化(将 B 注入 A)。
一切井然有序,对吧?但是,如果我们把这个链条首尾相接,变成循环依赖:
> Bean A → Bean B → Bean A
这时候,Spring 陷入了困境:
- Spring 尝试创建 A,发现需要 B,于是去创建 B。
- 在创建 B 的过程中,发现需要 A。
- 此时,A 还处于“正在创建中”的状态,并没有完成初始化。
- Spring 不知道该怎么办,它无法把一个还没创建好的 A 给 B。
- 结果:死锁发生,Spring 只能抛出异常终止启动。
2026年视角:AI 辅助开发与循环依赖检测
在进入具体的代码解决方案之前,我想特别强调一下2026年我们开发模式的转变。现在,我们在编写代码时,往往会使用 Cursor、Windsurf 或 GitHub Copilot 等 AI 辅助 IDE。这些工具不仅仅是自动补全代码,它们正在成为我们的“架构审查员”。
在我们最近的一个大型微服务重构项目中,我们引入了 Agentic AI 工作流。AI 代理不仅负责生成代码,还实时监控我们的依赖图。当我们尝试编写一个循环依赖的代码结构时,IDE 会立即通过多模态提示(高亮代码 + 依赖图预览)警告我们:“检测到潜在循环依赖,建议引入中间层。”
这种“左移”的开发理念意味着,我们不需要等到应用启动抛出 BeanCurrentlyInCreationException 时才发现问题。虽然 AI 不能完全替代我们对 Spring 原理的理解,但它确实极大地提高了我们的开发效率。不过,作为核心开发者,我们依然需要深入理解底层机制,以便在 AI 给出建议时做出正确的判断。
构造器注入的陷阱:不可变性的代价
并非所有的循环依赖都会导致 Spring 崩溃,这主要取决于我们使用的依赖注入(DI)方式。其中,构造器注入 是最容易出现问题的,但这恰恰是现代 Java 开发(尤其是结合 Spring 3.0+ 和不可变设计理念)最推崇的方式。
让我们编写一段代码来复现这个经典的错误场景。假设我们有两个服务:INLINECODE500b382f(订单服务)和 INLINECODE799d8ed1(库存服务)。
示例 1:导致崩溃的循环依赖代码
package com.example.demo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class OrderService {
private final InventoryService inventoryService;
// 通过构造器注入 InventoryService
@Autowired
public OrderService(InventoryService inventoryService) {
this.inventoryService = inventoryService;
System.out.println("OrderService 正在初始化...");
}
public void createOrder() {
System.out.println("创建订单...需要检查库存");
}
}
package com.example.demo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class InventoryService {
private final OrderService orderService;
// 通过构造器注入 OrderService
@Autowired
public InventoryService(OrderService orderService) {
this.orderService = orderService;
System.out.println("InventoryService 正在初始化...");
}
public void checkStock() {
System.out.println("检查库存...需要关联订单");
}
}
当我们尝试运行这段代码时,Spring 容器会毫不犹豫地抛出 BeanCurrentlyInCreationException。核心原因在于:Java 的构造函数机制要求对象必须完全初始化后才能返回。在构造器注入模式下,Spring 必须先拿到依赖的 Bean 实例才能调用构造函数。在 A → B → A 的循环中,谁都无法先完成构造,这就是死局。
解决方案 1:使用 @Lazy 注解打破僵局(战术级)
既然无法同时满足双方的要求,那我们能不能让其中一方“妥协”一下?这就是我们要讲的第一个解决方案:使用 @Lazy 注解。
@Lazy 注解的核心思想是延迟初始化。我们可以告诉 Spring:“在创建当前 Bean 时,先不要急着去创建那个依赖的 Bean,给我一个代理对象占位,等我真正用到它的时候再去创建。”
代码修正示例:
package com.example.demo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
@Component
public class OrderService {
private final InventoryService inventoryService;
// 关键点:在构造器参数前加上 @Lazy
// 这告诉 Spring:在创建 OrderService 时,先不要去实例化 InventoryService
@Autowired
public OrderService(@Lazy InventoryService inventoryService) {
this.inventoryService = inventoryService;
System.out.println("OrderService 初始化完成(持有 InventoryService 的代理)");
}
public void processOrder() {
// 只有当这里真正调用 inventoryService 的方法时,InventoryService 才会被创建
inventoryService.checkStock();
}
}
工作原理深度解析:
- Spring 尝试创建 INLINECODE977b1594,遇到构造函数需要 INLINECODEcd193bbd。
- 因为有 INLINECODE3d5809b6 注解,Spring 不会去递归创建 INLINECODE371ece48,而是生成一个代理对象(通常是 JDK 动态代理)。
-
OrderService成功构造完成,Bean 创建闭环解除。 - Spring 继续流程,去创建和初始化
InventoryService(因为它也是容器管理的 Bean)。 - 当 INLINECODE3d37acc3 被调用并访问 INLINECODEd5679537 时,代理对象会将调用转发给真正的、已经初始化好的
InventoryService实例。
适用场景: 这种方式非常适合你必须使用构造器注入(例如为了保证不可变性 final 字段),但又确实存在循环依赖的情况。在2026年的微服务架构中,这种方式依然有效,但要注意代理对象带来的微小性能开销。
解决方案 2: Setter/字段注入与三级缓存(常规级)
如果在你的业务场景中,构造器注入并非强制的(即字段不需要声明为 final),那么最简单、最经典的解决方式就是改用 Setter 注入 或 字段注入。
Spring 框架在设计之初就为我们提供了针对这种场景的“三级缓存”机制,能够自动解决单例 Bean 的 Setter 注入循环依赖。
- 构造器注入:对象还没出生(实例化),就需要依赖对象。
- Setter/字段注入:对象可以先通过无参构造函数生出来(实例化),然后再通过 Setter 方法把依赖对象塞进去(注入)。
示例 2:使用 Setter 注入解决循环依赖
package com.example.demo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class OrderService {
private InventoryService inventoryService;
// 无参构造函数
public OrderService() {
System.out.println("OrderService 实例化");
}
// Setter 注入
@Autowired
public void setInventoryService(InventoryService inventoryService) {
System.out.println("OrderService 正在注入 InventoryService");
this.inventoryService = inventoryService;
}
}
Spring 容器在处理上述代码时,流程如下:
- 实例化 A(此时 A 中的 B 为 null)。
- 将 A 暴露在三级缓存中。
- 填充 A 的属性 B,发现 B 需要被创建。
- 实例化 B。
- 填充 B 的属性 A,从缓存中拿到了 A 的早期引用。
- B 完成初始化。
- A 拿到 B,完成初始化。
解决方案 3:事件驱动架构(战略级推荐)
虽然上述技术手段可以解决问题,但作为2026年的架构师,我们更推荐从设计上彻底消除循环依赖。循环依赖通常意味着职责划分不清。
事件驱动解耦:使用 Spring 的 ApplicationEventPublisher 发布事件,而不是直接调用依赖的方法。
示例 3:使用事件机制解耦(推荐做法)
// 定义事件
package com.example.demo.event;
import org.springframework.context.ApplicationEvent;
public class OrderCreatedEvent extends ApplicationEvent {
private String orderId;
public OrderCreatedEvent(Object source, String orderId) {
super(source);
this.orderId = orderId;
}
public String getOrderId() {
return orderId;
}
}
// 订单服务:只管发布事件
package com.example.demo;
import com.example.demo.event.OrderCreatedEvent;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
@Service
public class OrderService {
@Resource
private ApplicationEventPublisher eventPublisher;
public void createOrder(String orderId) {
System.out.println("订单 " + orderId + " 已创建");
// 不需要调用 InventoryService,只需发布事件
eventPublisher.publishEvent(new OrderCreatedEvent(this, orderId));
}
}
// 库存服务:监听事件
package com.example.demo;
import com.example.demo.event.OrderCreatedEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service;
@Service
public class InventoryService {
@EventListener
public void handleOrderCreated(OrderCreatedEvent event) {
System.out.println("收到订单创建事件,开始扣减库存:" + event.getOrderId());
}
}
这种架构不仅解决了依赖问题,还非常适合现代云原生环境,因为它天然支持异步处理和分布式扩展。
实战案例:生产环境中的性能与可观测性
在我们最近的一个高并发交易系统中,我们遇到了一个棘手的问题。为了确保数据一致性,我们最初在 INLINECODEba9d9145 和 INLINECODEd38f7704 之间使用了循环依赖,并通过 @Lazy 注解解决。然而,在上线后的压力测试中,我们发现由于频繁的动态代理调用,系统的延迟在 P99 也就是 99分位线有所上升。
排查过程:
我们利用了现代可观测性工具(如 OpenTelemetry 和 Grafana)对 JVM 进行了深度剖析。我们发现,虽然 @Lazy 解决了启动问题,但在运行时,每一次方法调用都经过了一次代理拦截,这在每秒百万级请求的场景下造成了不可忽视的 CPU 开销。
最终方案:
我们决定重构代码,引入了一个中间的 EventBus(基于 Kafka 或 Reactive Streams),彻底切断了双向依赖。
- TradeService 发出交易事件。
- RiskService 订阅事件,同步或异步进行风控检查。
- 结果:系统吞吐量提升了 20%,且代码结构清晰了许多。
这次经验告诉我们,虽然 @Lazy 和 Setter 注入是有效的战术工具,但在追求极致性能的现代应用中,拥抱响应式编程和事件驱动架构才是长久之计。
总结
在这篇文章中,我们像侦探一样,抽丝剥茧地分析了 Spring 中循环依赖的成因。我们了解到,这本质上是一个“先有鸡还是先有蛋”的问题。
在2026年的技术背景下,我们不仅要掌握传统的解决方案(INLINECODE1fe4956f、Setter 注入),更要学会利用 AI 工具辅助检测,以及采用事件驱动、响应式等现代架构范式来从根本上避免循环依赖。下次当你看到 INLINECODEe004629c 时,希望你能从容应对,知道这不仅是一个 Bug,更是一次重构架构的良机。