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 天前
jev-ultrafast 深度解析:7 秒订机票的浏览器 Agent 是如何炼成的
前端·后端·agent
子兮曰4 天前
Jev 爆发一周:7 秒 Agent 背后的 System One 生态与三场争议
前端·后端·ai编程
爱勇宝4 天前
ZCode 开源 24 小时:一份没有历史的账本,回答不了"有没有偷代码"
前端·后端·chatglm (智谱)
胡写代码4 天前
别再前后端各写一套表单校验了
java·后端
大勇前进4 天前
原生 PHP 还是 Laravel?小项目到底要不要上框架
后端
yuzhi_liu4 天前
我用 LangGraph4j 实现 Multi-Agent Supervisor
后端
alsmile4 天前
Node-RED 之外,国产规则引擎的新方案:基于标准语法,Go 先行实现
后端·开源·go
大白804 天前
PHP 内存溢出排查思路:看懂报错日志,精准定位问题
后端
二月龙4 天前
PHP 接口返回统一响应封装,让前后端对接更省心
后端
盖伦发发4 天前
软件工程SOLID 五大设计原则
后端·软件工程