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

相关推荐
程序员cxuan19 分钟前
Claude Code 为了封禁中国用户,竟然在代码里下毒
人工智能·后端·程序员
郡杰25 分钟前
Git基础与开发平台搭建
git·后端
ServBay42 分钟前
MCP (Model Context Protocol) 深度指南,协议原理、Python 实现与生产级安全实践
后端·ai编程·mcp
道友可好1 小时前
Claude Code 泄露源码里的 89 个秘密
前端·人工智能·后端
用户8356290780512 小时前
使用 Python 保护和取消保护 Excel 工作表和工作簿
后端·python
Java面试题总结2 小时前
CubeSandbox 实战:从零部署到快照/克隆/回滚全体验
后端
Csvn2 小时前
Python 开发技巧:函数式编程与 lambda 进阶
后端
考虑考虑2 小时前
nginx配置ssl
运维·后端·nginx
uhakadotcom2 小时前
Python 中,`None` 不等于 `False`导致的问题
后端·面试·github
SamDeepThinking2 小时前
面向对象真正管理的,不是对象,而是依赖
java·后端·程序员