人人都会AI编程

3.5 循环依赖处理机制:三级缓存解决方案与适用边界

更新时间:2026-07-10

在 Spring 的 IoC 容器中,Bean 的创建过程伴随着依赖的解析和注入。大多数情况下,依赖关系是单向无环的,容器能顺序完成实例化。但当两个或多个 Bean 相互持有对方的引用时——例如 A 依赖 BB 也依赖 A——就形成了 循环依赖

Spring 并没有回避这一问题,而是通过一套精巧的 三级缓存 机制,在特定条件下优雅地解决了大部分循环依赖的场景。理解这套机制,不仅能帮助你在问题出现时快速定位,更能避免在设计阶段引入无法解决的循环引用。

3.5.1 循环依赖的典型场景

考虑一个常见的业务服务之间的相互调用:

@Component
public class OrderService {
    private final UserService userService;

    public OrderService(UserService userService) {
        this.userService = userService;
    }
}

@Component
public class UserService {
    private final OrderService orderService;

    public UserService(OrderService orderService) {
        this.orderService = orderService;
    }
}

OrderService 需要 UserService,而 UserService 又需要 OrderService。这种通过构造器声明的强依赖,使得容器在创建任何一个 Bean 时都陷入困境:要创建 OrderService,必须先有 UserService 实例;而要创建 UserService,又必须先有 OrderService 实例。

Spring 是否能解决这类问题,取决于注入方式和 Bean 的作用域。

3.5.2 三级缓存的内部结构

Spring 在 DefaultSingletonBeanRegistry 中维护了三个核心缓存 Map,用于解决单例 Bean 的循环依赖:

| 缓存级别 | 名称 | 存储内容 |
| -------- | --------------------------- | ------------------------------------------------------------ |
| 一级缓存 | singletonObjects | 完全初始化好的单例对象(成品) |
| 二级缓存 | earlySingletonObjects | 已实例化但尚未填充属性的早期单例对象(半成品) |
| 三级缓存 | singletonFactories | 生成早期对象的工厂,通常是 ObjectFactory 回调,可提前暴露对象的代理 |

解决循环依赖的关键在于 提前暴露对象的引用——在对象创建的过程中,即使依赖尚未注入、初始化尚未完成,先将一个“半成品”引用暴露给其他正在创建的 Bean,从而打破等待的僵局。

3.5.3 循环依赖的解决流程(以 Setter 注入为例)

构造器注入的循环依赖默认无法解决(后续说明边界),但 Setter 注入或字段注入 可以借助三级缓存轻松处理。我们以字段注入为例,分析 Spring 内部的创建步骤:

@Component
public class OrderService {
    @Autowired
    private UserService userService;
}

@Component
public class UserService {
    @Autowired
    private OrderService orderService;
}

容器启动时,首先尝试创建 OrderService

  1. 实例化:调用 OrderService 的默认构造器,生成原始对象 orderService@1234
  2. 暴露早期对象:在属性填充之前,将 orderService@1234 的引用包装为 ObjectFactory 并存入三级缓存 singletonFactories。这个工厂可在需要时返回该原始对象(或其代理)。
  3. 属性填充:发现 OrderService 依赖 UserService,容器转而去获取 UserService 的 Bean。
  4. 嵌套创建 UserService
  • 实例化 UserService,得到原始对象 userService@5678
  • 暴露其工厂到三级缓存。
  • 属性填充时,发现 UserService 依赖 OrderService,容器再次去获取 OrderService 的 Bean。
  1. 从缓存中发现半成品:这次获取 OrderService 时,一级缓存还没有成品,但三级缓存中存在其工厂。容器调用工厂方法,得到 orderService@1234 的早期引用,并将其升级放入二级缓存 earlySingletonObjects,同时从三级缓存移除。
  2. 完成 UserService 创建userService@5678 获得了 orderService@1234 的引用,属性填充完毕,执行初始化回调,最终进入一级缓存 singletonObjects
  3. 回溯完成 OrderService 创建OrderService 获得 userService@5678 的成品引用,属性填充完毕,初始化,最终放入一级缓存。

整个过程结束时,两个 Bean 相互持有对方的引用,且最终都指向一级缓存中的完整对象。关键点在于“提前暴露”,允许处于创建过程中的 Bean 被其他 Bean 引用,从而打破循环

3.5.4 为什么需要三级缓存

你可能会问,为什么不直接用二级缓存存储早期对象,而要引入三级缓存的工厂模式?原因在于 AOP 代理

在 Spring 中,许多 Bean 最终不是原始对象,而是由 AOP 框架生成的代理对象。代理的生成时机通常在 Bean 初始化之后。但对于需要提前暴露的循环依赖,如果直接把原始对象存入二级缓存,其他 Bean 拿到的是一个未经代理的原始对象,而最终容器中存放的却是代理对象——这会导致注入的引用不一致。

三级缓存的 ObjectFactory 可以在获取早期引用时,根据情况提前执行部分后处理器(如 SmartInstantiationAwareBeanPostProcessor),从而在必要时刻直接返回代理对象,确保所有引用最终指向同一个代理实例。如果 Bean 不需要代理,则工厂直接返回原始实例。这就是三级缓存的核心价值:延迟代理生成并保证引用的唯一性

3.5.5 适用边界与限制

三级缓存虽然巧妙,但并非万能。它只在以下条件下生效:

1. 仅限单例作用域(Singleton)

Spring 只对单例 Bean 使用三级缓存,prototype 作用域的 Bean 不会存入缓存。因为原型 Bean 每次获取都要创建新实例,缓存失去意义,容器也无法为这种无池化的对象提供早期引用。如果两个原型 Bean 相互引用,容器会抛出 BeanCurrentlyInCreationException

2. 无法解决构造器注入的循环依赖

试想构造器注入场景:要创建 OrderService 必须先调用构造器传入 UserService,而 UserService 又要求 OrderService。此时连实例化都无法完成,更谈不上去暴露早期对象(早期对象的暴露发生在实例化之后、属性填充之前)。因此构造器循环依赖会直接导致 BeanCreationException 异常。

官方推荐的做法是:优先使用构造器注入以保持依赖不可变,如果确实出现相互引用的需求,应将其中一方的注入方式改为 Setter 或字段注入,或重构代码——因为循环依赖往往暗示着设计上的不合理性(如边界划分不清或职责过重)。

3. 与 @Async@Transactional 等代理增强的协同

当涉及 AOP 代理时,三级缓存的工厂会提前触发代理的生成。如果代理过程本身存在循环依赖,仍可能失败。特别是使用 @Async 注解的方法,它会创建一个完全独立的代理,若其依赖链中再次引用回原 Bean,可能引发异常。这类场景需要特别注意切面的顺序和设计。

4. 与延迟初始化的交互

@Lazy 注解也能打破循环依赖。被标注 @Lazy 的注入点不会在 Bean 创建时立即解析依赖,而是注入一个代理,直到第一次调用时才真正获取目标 Bean。这种方式可以弥补构造器循环依赖的问题,但增加了代理开销和调用端的感知,应慎用。

3.5.6 实际建议与避免策略

  • 重构优先:循环依赖通常暴露了模块间的耦合过深。可以尝试抽取第三个组件(如门面或中介者),让两个 Bean 都依赖它,从而消除直接相互引用。
  • 遵循清晰的分层:Service 层之间应避免双向依赖。如果业务复杂,可引入事件机制(ApplicationEvent)或异步消息解耦。
  • 使用 @Lazy 作为最后手段:在处理遗留代码或确实必要的少数场景下,对构造器注入的其中一个依赖标注 @Lazy,可使容器先注入代理,延迟真正对象的获取,但必须理解这掩盖了潜在的设计问题。
  • 关注启动日志:当看到 BeanCurrentlyInCreationException 时,应检查是否存在构造器循环依赖,而不是简单切换注入方式就草草了事。

Spring 的三级缓存方案体现了框架对真实开发痛点的深刻理解:在保证绝大多数正确设计流畅运行的同时,为不可避免的少数场景提供一条可控的逃生通道。但作为开发者,理解这些内部机制,更重要的目的是在设计阶段就规避不必要的循环,而非滥用容器的容错能力。