循环依赖与三级缓存: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 的底层原理有更深刻的理解,也希望你在面对循环依赖时,不再"慌的一批"。

相关推荐
却尘6 小时前
从53个漏洞到5个:我们用Distroless把容器安全"减"出来了
后端·自动化运维·devops
BingoGo6 小时前
PHP 中的命名艺术 实用指南
后端·php
骑着bug的coder6 小时前
第1讲:入门篇——把MySQL当成Excel来学
后端·mysql
SimonKing6 小时前
Spring Boot全局异常处理的背后的故事
java·后端·程序员
骑着bug的coder6 小时前
线上503了?聊聊Feign熔断降级这点事
后端
初级程序员Kyle6 小时前
开始改变第六天 MySQL(1)
后端
MeowRain6 小时前
JVM分代回收
后端
程序员蜗牛6 小时前
拒绝重复造轮子!SpringBoot 内置的20个高效官方工具类详解
后端
白衣鸽子6 小时前
ListUtils:Java列表操作的瑞士军刀
后端·开源·设计