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

相关推荐
Victor3569 小时前
https://editor.csdn.net/md/?articleId=139321571&spm=1011.2415.3001.9698
后端
Victor3569 小时前
Hibernate(89)如何在压力测试中使用Hibernate?
后端
灰子学技术11 小时前
go response.Body.close()导致连接异常处理
开发语言·后端·golang
Gogo81612 小时前
BigInt 与 Number 的爱恨情仇,为何大佬都劝你“能用 Number 就别用 BigInt”?
后端
fuquxiaoguang12 小时前
深入浅出:使用MDC构建SpringBoot全链路请求追踪系统
java·spring boot·后端·调用链分析
毕设源码_廖学姐13 小时前
计算机毕业设计springboot招聘系统网站 基于SpringBoot的在线人才对接平台 SpringBoot驱动的智能求职与招聘服务网
spring boot·后端·课程设计
野犬寒鸦14 小时前
从零起步学习并发编程 || 第六章:ReentrantLock与synchronized 的辨析及运用
java·服务器·数据库·后端·学习·算法
逍遥德15 小时前
如何学编程之01.理论篇.如何通过阅读代码来提高自己的编程能力?
前端·后端·程序人生·重构·软件构建·代码规范
MX_935915 小时前
Spring的bean工厂后处理器和Bean后处理器
java·后端·spring
程序员泠零澪回家种桔子16 小时前
Spring AI框架全方位详解
java·人工智能·后端·spring·ai·架构