Spring IoC容器解决循环依赖的三级缓存机制详解

Spring IoC容器通过三级缓存机制巧妙地解决了单例Bean之间的循环依赖问题,这一设计是Spring框架的核心机制之一。下面我将从原理、实现到应用场景全面解析这一机制。

一、循环依赖的产生与问题本质

循环依赖是指两个或多个Bean相互持有对方的引用,形成依赖闭环。例如:

less 复制代码
@Component
public class A {
    @Autowired
    private B b;
}

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

这种相互依赖会导致传统初始化流程陷入死锁:创建A需要先初始化B,而创建B又需要先初始化A。

二、三级缓存的核心架构

Spring在DefaultSingletonBeanRegistry类中定义了三级缓存结构:

  1. 一级缓存(singletonObjects)​

    • 类型:ConcurrentHashMap<String, Object>
    • 作用:存储完全初始化完成的单例Bean,是最终可用的成品对象
  2. 二级缓存(earlySingletonObjects)​

    • 类型:HashMap<String, Object>
    • 作用:存储已实例化但未初始化的Bean早期引用(半成品对象),用于解决普通循环依赖
  3. 三级缓存(singletonFactories)​

    • 类型:HashMap<String, ObjectFactory<?>>
    • 作用:存储Bean的ObjectFactory,用于生成代理对象或原始对象,是解决AOP代理循环依赖的关键

三、解决循环依赖的完整流程

以A→B→A的循环依赖为例,Spring容器处理流程如下:

  1. 开始创建A

    • 标记A为"创建中"(加入singletonsCurrentlyInCreation集合)

    • 通过反射调用A的构造器创建实例(此时A是原始对象,未注入属性)

    • 将A的ObjectFactory放入三级缓存:

      scss 复制代码
      addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));

      这个工厂对象能在必要时生成A的早期引用(原始对象或代理对象)

  2. 处理A的依赖B

    • 发现A需要注入B,触发B的创建流程
    • 同样地:标记B为创建中,实例化B,将B的工厂放入三级缓存
  3. 处理B的依赖A

    • 发现B需要注入A,此时:

      • 从一级缓存查找A → 不存在

      • 从二级缓存查找A → 不存在

      • 从三级缓存获取A的ObjectFactory并调用getObject()

        ini 复制代码
        ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
        if (singletonFactory != null) {
            earlySingleton = singletonFactory.getObject();
            this.earlySingletonObjects.put(beanName, earlySingleton);
            this.singletonFactories.remove(beanName);
        }
      • 将生成的A早期引用存入二级缓存,并从三级缓存移除

  4. 完成B的初始化

    • 将A的早期引用注入B
    • 执行B的初始化回调(@PostConstruct等)
    • 将完全初始化的B放入一级缓存
  5. 完成A的初始化

    • 将B的成品对象注入A
    • 执行A的初始化回调
    • 将A的成品对象从二级缓存提升到一级缓存

四、三级缓存的关键设计意义

  1. 解决普通循环依赖

    二级缓存存储的早期对象允许相互依赖的Bean在未完成初始化时就能被引用,打破循环僵局

  2. 支持AOP代理

    三级缓存的ObjectFactory通过getEarlyBeanReference()方法:

    • 对普通Bean:直接返回原始对象

    • 对需要AOP代理的Bean:返回代理对象

      这确保最终注入的是同一个代理实例,避免出现原始对象与代理对象混用的情况

  3. 性能优化

    • 通过缓存层级减少重复创建
    • 通过对象工厂延迟代理对象的生成时机
    • 使用不同粒度的锁控制并发访问

五、机制的限制条件

  1. 仅适用于单例Bean

    原型(prototype)作用域的Bean每次请求都创建新实例,无法通过缓存解决循环依赖

  2. 不支持构造器循环依赖

    构造器注入必须在实例化前完成依赖解析,此时Bean尚未创建,无法存入缓存

  3. Spring Boot 2.6+默认关闭循环引用

    可通过spring.main.allow-circular-references=true重新启用

六、替代解决方案

当三级缓存无法解决问题时,可考虑:

  1. ​@Lazy延迟注入

    less 复制代码
    @Component
    public class A {
        @Lazy
        @Autowired
        private B b; // 实际注入的是代理对象
    }

    首次调用b的方法时才触发真实初始化

  2. 重构代码结构

    • 提取公共逻辑到第三个Bean
    • 使用接口解耦
    • 将部分依赖改为方法参数
  3. 改用Setter注入

    Setter注入允许先创建实例再注入依赖,比构造器注入更灵活

七、典型应用场景

  1. 领域模型互引用

    如用户-角色关系:用户拥有角色集合,角色又关联用户列表

  2. 服务层交叉调用

    如订单服务需要用户服务查用户,用户服务需要订单服务查历史订单

  3. AOP代理链

    如事务管理器的AOP代理相互依赖

Spring的三级缓存机制通过"提前暴露引用"的设计思想,在保持单例约束的同时,优雅地解决了循环依赖这一经典难题,体现了框架设计的精妙之处。

相关推荐
间彧4 小时前
Spring IoC详解与应用实战
后端
junnhwan4 小时前
【苍穹外卖笔记】Day04--套餐管理模块
java·数据库·spring boot·后端·苍穹外卖·crud
间彧4 小时前
Java NPE异常详解
后端
无责任此方_修行中4 小时前
我的两次 Vibe Coding 经历,一次天堂,一次地狱
后端·node.js·vibecoding
想想就想想4 小时前
深度分页介绍及优化建议:从原理到实战的全链路解决方案
后端
程序员清风4 小时前
Dubbo RPCContext存储一些通用数据,这个用手动清除吗?
java·后端·面试
南瓜小米粥、4 小时前
从可插拔拦截器出发:自定义、注入 Spring Boot、到生效路径的完整实践(Demo 版)
java·spring boot·后端
Huangmiemei9114 小时前
Spring Boot项目的常用依赖有哪些?
java·spring boot·后端
天天摸鱼的java工程师4 小时前
接口联调总卡壳?先问自己:真的搞清楚 HTTP 的 Header 和 Body 了吗?
java·后端