侧边栏壁纸
博主头像
牧云

怀璧慎显,博识谨言。

  • 累计撰写 198 篇文章
  • 累计创建 19 个标签
  • 累计收到 8 条评论

目 录CONTENT

文章目录

Spring循环依赖:三级缓存与@Lazy

秋之牧云
2026-05-14 / 0 评论 / 0 点赞 / 1 阅读 / 0 字

在Spring应用开发中,循环依赖(Circular Dependency) 是一个既常见又令人头疼的问题。特别是在大型项目迭代或多人协作开发时,经常会出现启动报错 BeanCurrentlyInCreationException

很多开发者知道Spring能解决循环依赖,但往往不清楚为什么能解决什么情况下不能解决以及如何优雅地处理。本文将深入剖析Spring循环依赖的底层原理,特别是著名的“三级缓存”机制,并给出实战中的解决方案。

一、 什么是循环依赖?

简单来说,就是两个或多个Bean之间相互持有对方的引用,形成闭环。

常见场景

  1. 两者循环:A依赖B,B依赖A。
  2. 三者循环:A依赖B,B依赖C,C依赖A。
  3. 自我依赖:A依赖A(较少见,通常由配置错误导致)。
@Component
public class ServiceA {
    @Autowired
    private ServiceB serviceB; // A依赖B
}

@Component
public class ServiceB {
    @Autowired
    private ServiceA serviceA; // B依赖A
}

二、 Spring能解决所有循环依赖吗?

答案是:不能。

Spring解决循环依赖是有严格前提条件的:

  1. 必须是单例(Singleton):原型(Prototype)作用域的Bean无法解决,因为每次获取都会创建新实例,导致无限递归 。
  2. 必须是非构造器注入:如果是纯构造器注入,Spring无法解决,会直接抛出异常 。

为什么构造器注入无法解决?

构造器注入要求在Bean实例化阶段就必须提供依赖对象。此时Bean尚未创建完成,无法提前暴露引用,导致“先有鸡还是先有蛋”的死锁问题 。

三、 核心揭秘:三级缓存机制

Spring之所以能解决单例Bean的Setter/字段注入循环依赖,核心在于其内部的三级缓存机制。这三级缓存定义在 DefaultSingletonBeanRegistry 类中 。

1. 三级缓存的结构

缓存级别变量名类型作用
一级缓存singletonObjectsMap<String, Object>存放完全初始化好的成品Bean 。
二级缓存earlySingletonObjectsMap<String, Object>存放早期曝光的Bean实例(半成品),用于解决循环依赖时的引用共享 。
三级缓存singletonFactoriesMap<String, ObjectFactory<?>>存放Bean工厂对象,用于在需要时动态生成早期引用(特别是代理对象) 。

2. 解决流程详解

假设 A 依赖 B,B 依赖 A:

  1. 创建 Bean A
    • Spring 实例化 A(调用构造器),此时 A 是一个原始对象。
    • 将 A 的 ObjectFactory 放入三级缓存
    • 开始填充 A 的属性,发现依赖 B。
  2. 创建 Bean B
    • Spring 尝试获取 B,发现不存在,于是实例化 B。
    • 将 B 的 ObjectFactory 放入三级缓存
    • 开始填充 B 的属性,发现依赖 A。
  3. 获取早期 A
    • B 请求获取 A。Spring 依次查找:
      • 一级缓存:无。
      • 二级缓存:无。
      • 三级缓存:找到 A 的工厂。
    • 调用工厂的 getObject() 方法。如果 A 需要 AOP 代理,此处会生成代理对象;否则返回原始对象 。
    • 将生成的早期引用放入二级缓存,并从三级缓存移除 。
    • B 获得 A 的引用,完成初始化,放入一级缓存
  4. 完成 Bean A
    • A 从一级缓存获取已完成的 B。
    • A 完成剩余初始化,放入一级缓存

3. 为什么需要三级缓存?只用二级不行吗?

这是面试中的高频考点。如果 Bean 没有 AOP 代理,二级缓存确实足够。但 Spring 引入三级缓存的核心目的是为了处理 AOP 代理

  • 原则:Spring AOP 代理通常在 Bean 初始化完成后(postProcessAfterInitialization)生成。
  • 冲突:循环依赖需要在初始化前就获取引用。
  • 解决:三级缓存中的 ObjectFactory 允许 Spring 在真正需要早期引用时,才去判断是否要创建代理。如果直接放在二级缓存,就无法区分是返回原始对象还是代理对象,可能导致最终注入的 Bean 版本不一致(即报错中提到的 "injected into other beans in its raw version... but has eventually been wrapped") 。

四、 Spring 无法解决的场景及对策

尽管三级缓存很强大,但以下场景仍会报错:

  1. 构造器循环依赖
    • 现象:启动直接报错。
    • 解决:使用 @Lazy 注解 。
  2. 多例(Prototype)循环依赖
    • 现象:每次请求都创建新对象,陷入死循环。
    • 解决:重构代码,避免多例间的循环依赖,或改为单例 。
  3. @Async/@Transaction 导致的代理问题
    • 现象:某些特定后置处理器(如 AsyncAnnotationBeanPostProcessor)未正确实现 getEarlyBeanReference,导致早期引用不是代理对象,而最终Bean是代理对象,引发版本不一致异常 。
    • 解决:使用 @Lazy 或重构依赖关系。

五、 实战建议:如何优雅地处理循环依赖?

虽然 Spring 能解决部分循环依赖,但循环依赖本身往往是设计不良的信号。以下是推荐的处理方案:

1. 首选:重构代码(最佳实践)

循环依赖通常意味着两个类耦合度过高。

  • 提取第三方服务:将 A 和 B 共同依赖的逻辑抽取到新的 Service C 中,让 A 和 B 都依赖 C 。
  • 接口解耦:通过接口隔离变化,减少直接依赖。

2. 次选:使用 @Lazy 注解

当无法重构时,@Lazy 是最简单的救命稻草。

  • 用法:在构造器参数或字段上添加 @Lazy
  • 原理:注入的是一个代理对象,只有在首次调用方法时才会真正去容器中获取目标 Bean,从而打破初始化时的死锁 。
@Service
public class ServiceA {
    private final ServiceB serviceB;

    // 加上 @Lazy,Spring 注入 ServiceB 的代理,避免启动时死锁
    public ServiceA(@Lazy ServiceB serviceB) {
        this.serviceB = serviceB;
    }
}

3. 其他技巧

  • Setter 注入替代构造器注入:对于非强依赖,可以使用 Setter 注入,利用 Spring 的三级缓存自动解决 。
  • **使用 **ObjectProvider:Spring 4.3+ 提供的延迟查找机制,效果类似 @Lazy,但更语义化 。

六、 总结

  1. Spring 通过三级缓存巧妙解决了单例、Setter/字段注入的循环依赖问题。
  2. 三级缓存的核心价值在于延迟生成代理对象,确保循环依赖中引用的 Bean 与最终容器中的 Bean 一致。
  3. 构造器注入多例 Bean的循环依赖无法自动解决,需借助 @Lazy 或代码重构。
  4. 治本之策是优化系统设计,降低耦合,避免循环依赖的产生。

理解这一机制,不仅能帮你排查启动报错,更能让你深入理解 Spring IoC 容器的设计哲学。

0

评论区