循环依赖与三级缓存:Spring 如何优雅地解决“鸡生蛋”问题?

循环依赖与三级缓存:Spring 如何优雅地解决"鸡生蛋"问题?

作者:天天摸鱼的java工程师

时间:2025-10-31

标签:Spring、循环依赖、三级缓存、Bean 生命周期、源码解析


一、前言:循环依赖的"哲学问题"

在日常开发中,我们经常听到"循环依赖"这个词。初学者会问: "不是说依赖就是注入吗?那为什么会循环?"

举个现实生活中的例子:

A 需要 B,B 也需要 A。

比如:

  • 订单服务需要用户服务(查用户下的订单)
  • 用户服务又需要订单服务(查用户最近一单)

这是不是很常见?

是的,非常常见。

但问题来了:Spring 在启动时如何实例化这种互相引用的 Bean?

这就引出了今天的主角:循环依赖与三级缓存


二、什么是循环依赖?

简单来说:

kotlin 复制代码
class A {
    @Autowired
    private B b;
}

class B {
    @Autowired
    private A a;
}

你觉得 Spring 能处理这种互相注入的情况吗?

答案是:能(有条件)!

前提:必须是单例 + 非构造器注入

如果你用的是构造器注入:

css 复制代码
@Component
class A {
    private final B b;
    A(B b) { this.b = b; }
}

@Component
class B {
    private final A a;
    B(A a) { this.a = a; }
}

对不起,Spring 会报错:

Requested bean is currently in creation: Is there an unresolvable circular reference?


三、三级缓存机制:Spring 的"解耦神器"

Spring 为了解决单例 Bean 的循环依赖问题,引入了 三级缓存机制

1. 三级缓存是什么?

在 Spring 的 DefaultSingletonBeanRegistry 中,有这么三个 Map:

typescript 复制代码
// 一级缓存:存放完全初始化好的单例 Bean
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>();

// 二级缓存:提前暴露的 Bean(未完成依赖注入)
private final Map<String, Object> earlySingletonObjects = new HashMap<>();

// 三级缓存:存放 BeanFactory,用于创建早期 Bean 的代理对象
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>();

2. 流程图一览(文字版)

以下是 Spring 创建 Bean 时的大致流程:

css 复制代码
1. 创建 A → 标记 A 正在创建
2. 实例化 A(构造方法)
3. 将 A 的 ObjectFactory 放入三级缓存
4. 注入属性时发现需要 B
5. 创建 B → 标记 B 正在创建
6. 实例化 B → 注入属性时发现需要 A
7. 从缓存中获取 A:
   - 一级没有 → 二级没有 → 三级有!
   - 调用 ObjectFactory 创建早期 A → 放入二级缓存
8. B 注入成功 → 初始化完成 → 放入一级缓存
9. 返回 B,继续完成 A 的注入
10. A 初始化完成 → 放入一级缓存

就这样,一对互相依赖的 Bean 被"优雅"地创建出来了。


四、实战演练:用户服务与订单服务的循环依赖

我们来看一个业务中真实可能发生的场景:

typescript 复制代码
@Service
public class UserService {
    @Autowired
    private OrderService orderService;
    
    public void getUserInfo() {
        orderService.getOrderByUser();
    }
}

@Service
public class OrderService {
    @Autowired
    private UserService userService;

    public void getOrderByUser() {
        userService.getUserInfo();
    }
}

你可能会说:这不是死循环吗?

是的,如果你在方法调用中不加控制,运行时会栈溢出。

但 Spring 启动时能否处理这种结构?能!

因为它们是单例的,且是 字段注入,Spring 会通过三级缓存机制,提前暴露对象引用,避免死锁。


五、源码分析:Spring 是怎么做到的?

关键方法在 AbstractAutowireCapableBeanFactory#createBean

typescript 复制代码
protected Object createBean(...) {
    // 省略部分代码
    Object bean = resolveBeforeInstantiation(beanName, mbdToUse);
    if (bean != null) {
        return bean;
    }

    return doCreateBean(beanName, mbdToUse, args);
}

doCreateBean 中:

scss 复制代码
// 提前暴露 bean 的 ObjectFactory
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));

这就是三级缓存核心代码。Spring 先把 ObjectFactory 放入三级缓存(singletonFactories),以便后续注入依赖时可以"拿到还没完全初始化"的 Bean。

当另一个 Bean 需要注入这个 Bean 时,会尝试从三级缓存中拿出来,提前使用。


六、循环依赖的限制与最佳实践

✅ Spring 支持的情况:

  • 单例 Bean
  • 非构造器注入(字段或 setter)

❌ 不支持的情况:

  • 原型作用域 Bean(prototype)
  • 构造器注入循环依赖(Spring 无法提前暴露)

✅ 最佳实践建议:

  1. 避免循环依赖是最好的解决方式

    • 通常是设计不合理
    • 拆分出中间服务 / 拆分职责
  2. 构造器注入优先,但要避免循环

    • 构造器注入更符合"不可变性"原则
  3. 使用 @Lazy 打破循环

    • 延迟加载其中一个 Bean:
less 复制代码
@Autowired
@Lazy
private UserService userService;

七、总结

循环依赖并不是洪水猛兽,而是设计中常见的"鸡生蛋"问题。Spring 为了解决它,设计了非常巧妙的三级缓存机制。

但话说回来:

最好的循环依赖解决方式,永远是优雅的架构设计。

希望这篇文章能让你对 Spring 的底层原理有更深刻的理解,也希望你在面对循环依赖时,不再"慌的一批"。

相关推荐
ChinaRainbowSea几秒前
13. Spring AI 的观测性
java·人工智能·后端·spring·flask·ai编程
-大头.2 分钟前
SpringBoot 全面深度解析:从原理到实践,从入门到专家
java·spring boot·后端
charlie11451419125 分钟前
使用 Poetry + VS Code 创建你的第一个 Flask 工程
开发语言·笔记·后端·python·学习·flask·教程
aiopencode25 分钟前
iOS 上架 App Store 全流程技术解读 应用构建、签名体系与发布通道的标准化方案
后端
Rexi30 分钟前
go如何写单元测试2
后端
Rexi30 分钟前
go如何写单元测试1
后端
Harry技术1 小时前
Spring Boot 4.0 发布总结:新特性、依赖变更与升级指南
spring boot·后端
武子康1 小时前
大数据-159 Apache Kylin Cube 实战:Hive 装载与预计算加速(含 Cuboid/实时 OLAP,Kylin 4.x)
大数据·后端·apache kylin
疯狂的程序猴1 小时前
Mac 抓包软件怎么选?从 HTTPS 调试、TCP 数据流分析到多工具协同的完整抓包方案
后端
BingoGo2 小时前
使用 PHP 和 Raylib 也可以开发贪吃蛇游戏
后端·php