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

相关推荐
鬼火儿3 小时前
SpringBoot】Spring Boot 项目的打包配置
java·后端
cr7xin4 小时前
缓存三大问题及解决方案
redis·后端·缓存
间彧5 小时前
Kubernetes的Pod与Docker Compose中的服务在概念上有何异同?
后端
间彧5 小时前
从开发到生产,如何将Docker Compose项目平滑迁移到Kubernetes?
后端
间彧5 小时前
如何结合CI/CD流水线自动选择正确的Docker Compose配置?
后端
间彧5 小时前
在多环境(开发、测试、生产)下,如何管理不同的Docker Compose配置?
后端
间彧5 小时前
如何为Docker Compose中的服务配置健康检查,确保服务真正可用?
后端
间彧5 小时前
Docker Compose和Kubernetes在编排服务时有哪些核心区别?
后端
间彧5 小时前
如何在实际项目中集成Arthas Tunnel Server实现Kubernetes集群的远程诊断?
后端
brzhang6 小时前
读懂 MiniMax Agent 的设计逻辑,然后我复刻了一个MiniMax Agent
前端·后端·架构