Skip to content

Bean 的循环依赖问题及三级缓存的解决机制

约 2949 字大约 10 分钟

ai随机问

2025-08-04

1. Bean 循环依赖是什么?

  1. A 类的属性依赖 B 类(A.has-a B);B 类的属性依赖 A 类(B.has-a A
  2. Spring 默认只解决 “单例 Bean” 的循环依赖,而原型 Bean(scope="prototype")的循环依赖无法解决

👇 俩问题,单例Bean的循环依赖是怎么解决的?为什么原型Bean的循环依赖无法解决?

2. 单例Bean的循环依赖怎么解决?

通过三级缓存(三个 Map)实现单例 Bean 的循环依赖

核心思路是:提前暴露 “未完全初始化” 的 Bean 实例,让依赖方先引用,避免死等

  1. 一级缓存:singletonObjects : 完全初始化完成 成品 Bean 缓存,是最终对外提供的 “可用 Bean”,其他 Bean 获取依赖时优先从这里查
  2. 二级缓存:earlySingletonObjects : 未完全初始化但已实例化 从三级缓存的工厂中获取实例,放入二级缓存
  3. 三级缓存:singletonFactories : 实例化后(还未注入属性) Spring 会将其包装成工厂对象放入三级缓存
  4. 解决循环依赖流程(依赖关系:A → B → A
    1. Spring 实例化 A,将 A 的工厂对象放入三级缓存
    2. A 需要注入 B,检查一/二/三级缓存没有 B,开始创建 B 的实例
    3. Spring 实例化 B,将 B 的工厂对象放入三级缓存
    4. B 需要注入 A,检查一/二/三级缓存,三级缓存中有 A,通过工厂获取 A 的早期实例,将 A 的早期实例从三级缓存移到二级缓存
    5. B 注入 A 的早期实例,完成自身初始化,B 放入一级缓存
    6. B 已在一级缓存中,A 直接注入 B,完成自身初始化,A 从二级缓存移到一级缓存

👇 这里面提到了 初始化、未完全初始化、实例化、未注入属性 都代表什么意思?

3. 初始化/实例化基本概念

  1. 实例化:调用类的构造方法,创建一个 “空对象”(仅分配内存,属性未赋值)
  2. 属性注入:给实例化后的对象设置依赖(如@Autowired标注的属性)
  3. 初始化:执行初始化方法(如@PostConstruct标注的方法、InitializingBean接口的afterPropertiesSet()
  4. 完全初始化后,Bean 被放入singletonObjects,供其他 Bean 使用**(放入一级缓存)**

👇 概念搞懂后,有一个新的问题,判断循环依赖的依据是否是:“B需要注入A时,发现A在三级缓存中”得出的结论?

4. 循环依赖的依据是否是发现实例在三级缓存中?

  1. Spring 通过一个专门的标记(singletonsCurrentlyInCreation集合)来记录 Bean 的创建状态
  2. Spring 在创建单例 Bean 时,会先将 Bean 的名称(或标识)加入singletonsCurrentlyInCreation集合,标记为 “正在创建中”
  3. 当 Bean 完全初始化并放入一级缓存后,再从该集合中移除
  4. 循环依赖的核心取决于被依赖的 Bean 是否处于 “正在创建中” 且形成了依赖闭环,而非单纯看三级缓存中是否有某个 Bean

👇 三级缓存 和 正在创建中 的关联关系?

5. 三级缓存和正在创建中关联关系

  1. “正在创建中” 的标记先于 “进入三级缓存”
    1. 在 Bean 开始实例化前,Spring 就会将其加入singletonsCurrentlyInCreation集合,标记为 “正在创建中”
    2. 在 Bean 完成实例化之后,Spring 会将其 “早期实例工厂” 放入三级缓存
    3. 只有先标记为 “正在创建中”,才会进入后续的实例化和三级缓存暴露流程
  2. 三级缓存中的 Bean,必然处于 “正在创建中”
  3. “正在创建中” 的 Bean,绝大多数会进入三级缓存,但有例外
  4. 例外情况:若手动关闭循环依赖支持,此时仍会被标记为 “正在创建中”,但不会进入三级缓存(因为allowCircularReferences=false,跳过了暴露工厂的步骤)

👇 到这里,三级缓存解决循环依赖告一段落,但只有单例Bean才能解决循环依赖,如何区分单例Bean和原型Bean?

6. 如何区分单例Bean和原型Bean?

  1. 单例 Bean(Singleton)与原型 Bean(Prototype)的核心区别

    维度单例 Bean(默认)原型 Bean(scope="prototype"
    实例数量整个 Spring 容器中只创建一个实例,全局共享。每次通过getBean()获取或被依赖注入时,都会创建新实例
    生命周期与 Spring 容器一致(容器启动时创建,容器关闭时销毁)。由开发者手动管理(Spring 创建后不再跟踪,不会自动销毁)。
    适用场景无状态 Bean(如 Service、Dao 层,无成员变量或成员变量不可变)。有状态 Bean(如命令对象、会话对象,需要为不同请求创建独立实例)。
    循环依赖处理支持(通过三级缓存解决)。不支持(会抛出BeanCurrentlyInCreationException)。
  2. 代码中指定作用域

    // 单例Bean(默认,可省略@Scope注解)
    @Service
    @Scope("singleton") // 等价于不写
    public class UserService { ... }
    
    // 原型Bean(每次获取都创建新实例)
    @Service
    @Scope("prototype")
    public class OrderCommand { ... }
  3. 验证实例是否为单例

    @SpringBootTest
    public class ScopeTest {
        @Autowired
        private ApplicationContext context;
    
        @Test
        public void testScope() {
            // 单例Bean:两次获取的是同一个实例
            UserService service1 = context.getBean(UserService.class);
            UserService service2 = context.getBean(UserService.class);
            System.out.println(service1 == service2); // 输出:true
    
            // 原型Bean:两次获取的是不同实例
            OrderCommand command1 = context.getBean(OrderCommand.class);
            OrderCommand command2 = context.getBean(OrderCommand.class);
            System.out.println(command1 == command2); // 输出:false
        }
    }

此时也解决了第一个标题中引出的第二个问题:为什么原型Bean的循环依赖无法解决?

  1. 原型 Bean 的作用域是prototype,即每次获取都会创建新实例(getBean()一次就 new 一个)。
  2. 当 A(原型)依赖 B(原型),B 又依赖 A 时,会触发 “无限创建新实例” 的循环(A1 依赖 B1,B1 依赖 A2,A2 依赖 B2...),导致 OOM。
  3. Spring 无法处理这种场景,直接抛出BeanCurrentlyInCreationException

👇 好,新的问题是,日常开发中默认是单例Bean,按照上面说的会通过三级缓存解决循环依赖,但还是会报循环依赖的错,为什么?

7. 三级缓存处理循环依赖范围

两个核心条件:

  1. 依赖注入方式为字段注入(Field Injection)或 setter 注入 :这两种注入方式发生在 Bean 实例化(调用构造器创建对象)之后,此时 Bean 已完成实例化,可通过三级缓存提前暴露 “早期实例”(未初始化完成的对象)供依赖方引用。
  2. 循环依赖的 Bean 均为单例:只有单例 Bean 会进入 Spring 的三级缓存机制,原型 Bean 或其他作用域的 Bean 无法通过缓存解决循环依赖。

单例 Bean 仍报循环依赖的常见原因:

  1. 循环依赖中使用了构造器注入
  2. 循环依赖涉及非单例 Bean(如原型、request 等作用域)
  3. 循环依赖中使用了final 字段注入 : final字段的注入发生在构造器执行阶段
  4. 循环依赖涉及代理对象(如 AOP 增强)且注入时机异常
    • 当循环依赖的 Bean 被 AOP 增强(如@Transactional@Async等)时,Spring 会先创建目标对象的早期实例,再生成代理对象
    • 此时 Bean 的实际类型是 “代理对象” 而非原始对象
    • 如果代理对象的创建时机与依赖注入顺序冲突,可能导致循环依赖无法处理
    • 循环依赖中,当需要注入A对象时,A的代理对象可能尚未生成(此时只有原始对象的早期实例在三级缓存中),导致注入的是 “原始对象” 而非 “代理对象”
  5. 未正确使用@Lazy注解处理构造器循环依赖

👇 既然说到了注入方式,那再回顾一下所有的注入方式以及和三级缓存的关系。

8. 注入方式和三级缓存

  1. 常见注入方式及循环依赖解决能力对比

    注入方式是否能通过三级缓存解决循环依赖核心原因
    构造器注入不能注入发生在实例化阶段(调用构造方法时),对象未实例化,无法暴露早期实例到三级缓存。
    @Autowired注入发生在实例化之后(属性注入阶段),对象已实例化并放入三级缓存,可暴露早期实例。
    @Resource不能(默认)按名字注入时不经过 getBean(),直接查找成熟 Bean,不触发三级缓存。
  2. 能解决循环依赖的注入方式实例化之后注入 在对象实例化后(已放入三级缓存)才注入依赖

  3. 不能解决循环依赖的注入方式构造器注入 构造器调用时对象尚未创建,依赖的 Bean 也无法获取当前 Bean 的引用

  4. @Resource默认不兼容三级缓存

    • 注入逻辑由 JDK 自带的 CommonAnnotationBeanPostProcessor 处理(早期版本),或 Spring 扩展的处理器处理
    • 其注入时默认不通过 getBean() 流程,而是直接从容器的 beanDefinitionMap 中查找 Bean 实例
    • 若未找到成熟 Bean(一级缓存中无),则不会触发三级缓存的查找,而是直接抛出 BeanCurrentlyInCreationException(循环依赖异常)

👇 好,那现在根据注入方式看来,@Autowired能解决循环依赖,构造器注入不能,但是Spring官方却推荐使用构造器注入,这是为什么?

9. 为什么Spring官方推荐使用构造器注入?

因为构造器注入在多数场景比 @Autowired 下更优

  1. 确保依赖不可变(Immutability)
    • 构造器注入的依赖可以用final修饰,一旦注入后就无法被修改,避免了运行时被意外篡改的风险,增强了对象的稳定性
    • @Autowired字段注入无法使用final修饰依赖(因为final字段必须在构造器中初始化)
  2. 强制依赖在对象创建时初始化,避免空指针
    • 构造器是对象创建的必经步骤,依赖必须在构造器调用时传入,否则对象无法实例化。这就强制保证了依赖在对象使用前一定被初始化
    • @Autowired字段注入是 “对象先创建,后注入依赖”:如果注入过程失败(比如依赖不存在),对象已经被实例化,但依赖为null
  3. 依赖关系更明确,可读性更强
  4. 无侵入性,脱离 Spring 也能正常工作
  5. 构造器注入的 Bean 在测试时更容易通过构造方法手动传入依赖
  6. 提前暴露循环依赖问题
    • 循环依赖本质上是代码设计的隐患(可能意味着职责耦合)
    • 构造器注入在循环依赖时会直接报错(启动时就抛出BeanCurrentlyInCreationException),强制开发者提前发现并解决循环依赖问题

👇 为什么@Lazy能解决循环依赖呢?

10. @Lazy解决循环依赖

  1. @Lazy的核心是 “延迟初始化依赖对象”
  2. 不直接创建其原始对象或代理对象,而是先创建一个临时代理(占位符)
  3. 直到第一次实际使用某对象时,才会真正初始化他的原始对象或代理对象
  4. 循环依赖是双向的,只需在其中一方的依赖注入上添加@Lazy即可打破循环