Spring 是如何解决循环依赖的

发布于:2025-07-17 ⋅ 阅读:(14) ⋅ 点赞:(0)

一、什么是循环依赖

循环依赖(Circular Dependency) 指多个 Bean 在相互注入时形成闭环,导致依赖无法解析。例如:

@Component
public class A {
    @Autowired
    private B b;
}

@Component
public class B {
    @Autowired
    private A a;
}

在创建 Bean A 时发现需要 B,而 B 又需要 A,这就像鸡生蛋、蛋生鸡的问题。


二、通俗易懂的生活例子:两位厨师借锅

假设我们有两个厨师:

  • 厨师 A 做菜需要用厨师 B 的锅
  • 厨师 B 做菜又需要用厨师 A 的锅

常规方式:直接做菜

两人都等着对方先把锅拿出来,结果两人都饿死了 —— 死锁,循环依赖

Spring 的方式:先把锅提前摆出来

Spring 采取了一个折中的方式:

“你们先把锅拿出来(对象创建但还没做菜),虽然菜没做好(属性没填),但锅已经可以用了。”

所以:

  1. 厨师 A 先把锅放在桌上;
  2. 厨师 B 可以先用这个锅继续做菜;
  3. 然后大家再慢慢把菜做完。

这就是 Spring 用“三级缓存”提前暴露 bean 的原理。


三、Spring 的解决机制

3.1 可解决的范围

  • 可以解决 字段注入Setter 注入 的循环依赖;
  • 构造器注入 无法解决。

3.2 三级缓存机制

Spring 使用三级缓存管理单例 Bean 的生命周期:

缓存层级 名称 作用
一级缓存 singletonObjects 已完全初始化的 Bean
二级缓存 earlySingletonObjects 提前曝光但尚未初始化完成的 Bean
三级缓存 singletonFactories 用于生成早期 Bean 的工厂(ObjectFactory

通过这种方式,Spring 可以“在 Bean 初始化完成之前,就让别人能引用到我”,从而打破循环。


四、代码示例:字段注入成功解决循环依赖

@Component
public class ServiceA {
    @Autowired
    private ServiceB serviceB;

    public void print() {
        System.out.println("This is ServiceA");
    }
}

@Component
public class ServiceB {
    @Autowired
    private ServiceA serviceA;

    public void print() {
        System.out.println("This is ServiceB");
    }
}

启动成功,无异常

Spring 自动利用三级缓存,在 Bean 未完成初始化前就让彼此可以“提前引用”。


五、失败示例:构造函数注入无法解决

@Component
public class X {
    private final Y y;

    @Autowired
    public X(Y y) {
        this.y = y;
    }
}

@Component
public class Y {
    private final X x;

    @Autowired
    public Y(X x) {
        this.x = x;
    }
}

结果:

BeanCurrentlyInCreationException: Error creating bean with name 'x'

构造器注入要求必须先创建完整对象,Spring 无法“提前曝光”引用。


六、解决循环依赖的策略

推荐做法:

  • 避免循环设计:服务间职责清晰,减少相互依赖。
  • 使用 @Lazy 注解:推迟 Bean 的创建时机。
  • 采用事件驱动或观察者模式:避免强依赖耦合。

示例:使用 @Lazy 延迟注入

@Component
public class A {
    @Autowired
    @Lazy
    private B b;
}

七、总结

特性 支持循环依赖? 是否推荐?
字段注入 支持 推荐
Setter 注入 支持 推荐
构造器注入 不支持 慎用

Spring 使用三级缓存机制 提前曝光 Bean 引用,打破依赖死循环。但构造函数注入由于生命周期限制,不在其解决范围内。


八、Spring 解决循环依赖的完整执行流程(含源码路径)

当我们使用 @Autowired 注入依赖时,Spring 会执行如下流程(以字段注入为例):

8.1 Bean 创建流程图

               getBean("A")
                   │
         ┌─────────▼─────────┐
         │createBeanInstance │   (创建 Bean 实例 A)
         └─────────┬─────────┘
                   │
         ┌─────────▼────────────┐
         │add singletonFactory  │ (将 ObjectFactory 放入三级缓存)
         └─────────┬────────────┘
                   │
         ┌─────────▼────────────┐
         │populateBean()        │ (依赖注入,发现需要 B)
         └─────────┬────────────┘
                   │
         ┌─────────▼────────────┐
         │getBean("B")          │
         └─────────┬────────────┘
                   │
        如果 B 也依赖 A ——> 从三级缓存中取出 A 的 early reference 引用

8.2 涉及关键源码位置

步骤 类名 方法
创建 Bean AbstractAutowireCapableBeanFactory createBean()
添加三级缓存 DefaultSingletonBeanRegistry addSingletonFactory()
从三级缓存提取 DefaultSingletonBeanRegistry getSingleton()
判断循环依赖状态 DefaultSingletonBeanRegistry isSingletonCurrentlyInCreation()

九、Spring 中三级缓存的具体作用解析

9.1 singletonObjects(一层缓存)

作用:存放已经完成初始化的单例 Bean。

一旦 Bean 完成构造、属性注入、初始化方法等生命周期后,就会进入这个 Map 中。

Map<String, Object> singletonObjects;

9.2 earlySingletonObjects(二层缓存)

作用:存放提前曝光但尚未初始化完成的 Bean。

当某个 Bean 正在创建中,但已经被别的 Bean 所依赖,Spring 会从三级缓存生成一个对象(不是最终的成品),放入此处。

Map<String, Object> earlySingletonObjects;

9.3 singletonFactories(三层缓存)

作用:存放生成早期 Bean 引用的 ObjectFactory

通过它可以生成 earlySingletonObject 并填充到二级缓存,实现“懒加载”。

Map<String, ObjectFactory<?>> singletonFactories;

9.4 内部关键逻辑简化代码

// 如果一级缓存没有,尝试从二级缓存取
singletonObject = this.earlySingletonObjects.get(beanName);

// 如果二级缓存也没有,再从三级缓存中通过 ObjectFactory 提前创建
if (singletonObject == null && allowEarlyReference) {
    ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
    if (singletonFactory != null) {
        singletonObject = singletonFactory.getObject();
        this.earlySingletonObjects.put(beanName, singletonObject);
        this.singletonFactories.remove(beanName);
    }
}

十、调试技巧:如何在 IDE 中观察循环依赖过程

10.1 推荐断点设置点

  • AbstractAutowireCapableBeanFactory.createBean()
  • DefaultSingletonBeanRegistry.getSingleton()
  • populateBean() 中的依赖注入逻辑
  • addSingleton() 添加到一级缓存的地方

10.2 实际调试步骤

  1. 设置断点在 getSingleton()addSingletonFactory()
  2. 创建两个循环依赖 Bean 并启动 Spring Boot 项目。
  3. 观察 IDE 中对象在三级缓存中如何切换。
  4. 利用调试窗口查看:singletonObjectsearlySingletonObjectssingletonFactories 内容。

十一、特殊场景说明

11.1 @Scope(“prototype”) 不支持循环依赖

因为原型 Bean 每次都会新建对象,不会放入缓存,Spring 不会缓存其早期引用,所以也就无法提前暴露。

@Scope("prototype")
@Component
public class A {
    @Autowired
    private B b;
}

@Scope("prototype")
@Component
public class B {
    @Autowired
    private A a;
}

启动时会报错。


11.2 构造器注入 + @Lazy:也无法解决

即使你写了:

@Autowired
public A(@Lazy B b) { ... }

仍然 无法提前暴露构造器参数,所以不能解决循环依赖,构造器注入必须避免环依赖设计。


十二、小结与建议

场景 能否解决 原因
字段注入 支持 可通过三级缓存提前暴露
Setter 注入 支持 同上
构造器注入 不支持 无法提前暴露
原型作用域 不支持 不进入缓存机制
使用 @Lazy 字段注入 可辅助解决 推迟初始化打破闭环

最佳建议:设计上避免循环依赖。Spring 能帮你兜底,但不是让你滥用。

十三、补充

Spring 循环依赖解决流程

BeanA 创建阶段

步骤 1-3:创建 BeanA

  1. 创建 BeanA(1):Spring 调用 getBean("BeanA") 开始实例化。
  2. 实例化 BeanA(2):通过构造函数或反射 new BeanA() 得到对象,属性尚未赋值。
  3. 未初始化的 BeanA 加入三级缓存(3):此时把 ObjectFactory<BeanA> 放入 singletonFactories,BeanA 的引用可以被别人提前拿到(虽然属性未注入)。

BeanB 创建阶段

步骤 5-8:创建 BeanB 并注入 BeanA

  1. BeanA 依赖 BeanB(5):发现 BeanA 中注入了 BeanB,开始调用 getBean("BeanB")
  2. 实例化 BeanB(6):使用反射或构造函数创建 BeanB 对象(属性 A 还未注入)。
  3. BeanB 加入三级缓存(7):将 BeanB 的工厂也放入 singletonFactories
  4. BeanB 依赖 BeanA(9):Spring 在注入 BeanA 时发现它“正在创建”,进入循环依赖处理流程。

三级缓存介入解决循环依赖

步骤 9-11:从三级缓存提前获取 BeanA

  • 二级缓存为空,转查三级缓存:Spring 发现一级缓存(已初始化的 Bean)找不到 A,尝试从二级缓存 earlySingletonObjects 获取。
  • 从三级缓存中调用 ObjectFactory(9):将其返回的 BeanA 放入二级缓存中,返回该引用用于注入。
  1. 返回提前暴露的 BeanA(10)
  2. BeanA 正式加入二级缓存(11)

此时 BeanB 中已经注入了 BeanA,但 BeanA 还未初始化完成!这是关键!


BeanB 初始化完成

  1. BeanB 完成属性注入 + 初始化方法(12)
  2. BeanB 放入一级缓存(13):表示创建完成,可以安全使用。
  3. 返回 BeanB 给 BeanA(14)

回到 BeanA,完成剩余初始化

  1. 完成 BeanA 属性注入(15):此时 B 已完成并在一级缓存中,注入顺利。
  2. BeanA 放入一级缓存(16):生命周期完成。

总结三大缓存参与点

缓存 存什么 在哪用到 示例说明
三级缓存 singletonFactories 创建早期引用的工厂 让别人可以“先看到我” BeanA 提前暴露引用给 BeanB
二级缓存 earlySingletonObjects 实例化未初始化的 Bean 从三级缓存转入供别人使用 BeanA 被提前用来注入
一级缓存 singletonObjects 完全初始化好的 Bean 真正用于返回 BeanA 和 BeanB 最终归宿

整体理解小结

  • 为什么能解决? 因为 Spring 把正在创建中的 Bean 的“引用”提前暴露出来了。
  • 为什么构造器注入不行? 因为构造器阶段就需要完整 Bean,Spring 还来不及提前暴露。
  • 为什么要三级? 二级缓存只保存对象,三级缓存保存“生成引用的工厂”,能延迟加载且更灵活。
  • 实际作用? 避免 Bean 死循环,保证注入成功。

网站公告

今日签到

点亮在社区的每一天
去签到