循环依赖与三级缓存: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 无法提前暴露)
✅ 最佳实践建议:
- 
避免循环依赖是最好的解决方式 - 通常是设计不合理
- 拆分出中间服务 / 拆分职责
 
- 
构造器注入优先,但要避免循环 - 构造器注入更符合"不可变性"原则
 
- 
使用 @Lazy打破循环- 延迟加载其中一个 Bean:
 
            
            
              less
              
              
            
          
          @Autowired
@Lazy
private UserService userService;七、总结
循环依赖并不是洪水猛兽,而是设计中常见的"鸡生蛋"问题。Spring 为了解决它,设计了非常巧妙的三级缓存机制。
但话说回来:
最好的循环依赖解决方式,永远是优雅的架构设计。
希望这篇文章能让你对 Spring 的底层原理有更深刻的理解,也希望你在面对循环依赖时,不再"慌的一批"。