Spring三级缓存机制解析:循环依赖的优雅解决方案
2026/8/3 12:12:35 网站建设 项目流程

1. 循环依赖的本质与Spring的困境

在Spring框架中,循环依赖指的是两个或多个Bean相互依赖对方完成初始化的情况。比如Bean A的构造需要注入Bean B,而Bean B的构造又需要注入Bean A。这种"鸡生蛋蛋生鸡"的问题在传统依赖注入模式下会导致无限递归,最终抛出BeanCurrentlyInCreationException。

Spring早期版本(2.x之前)对这种场景束手无策,开发者只能通过重构代码来打破循环引用。但随着业务系统复杂度提升,完全避免循环依赖变得越来越困难。比如在领域驱动设计中,订单(Order)需要关联客户(Customer),而客户又需要维护自己的订单列表,这种双向关联在领域模型中非常自然。

关键点:循环依赖本身不是编码错误,而是对象关系的一种合理表达。Spring需要提供机制来支持这种合理需求。

2. 三级缓存的设计架构解析

Spring通过三级缓存机制优雅地解决了这个问题。这三个缓存分别存储在DefaultSingletonBeanRegistry类中:

// 一级缓存:存放完全初始化好的Bean private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256); // 二级缓存:存放早期暴露的Bean(已实例化但未初始化) private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16); // 三级缓存:存放Bean工厂对象(用于处理AOP代理) private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);

它们的协作流程可以类比为生产线:

  1. 当Bean开始创建时,首先在三级缓存注册ObjectFactory
  2. 遇到依赖注入时,会依次检查各级缓存
  3. 最终完成初始化的Bean会被提升到一级缓存

3. 完整解决流程的步骤拆解

让我们通过OrderService和UserService的循环依赖案例,看看三级缓存如何运作:

3.1 实例化阶段

  1. 开始创建OrderService
  2. 通过反射调用构造函数实例化对象(此时属性都是null)
  3. 将原始对象包装成ObjectFactory放入三级缓存
  4. 开始属性注入,发现需要UserService

3.2 依赖解决阶段

  1. 开始创建UserService
  2. 同样实例化后放入三级缓存
  3. 注入属性时发现需要OrderService
  4. 从三级缓存获取OrderService的ObjectFactory
  5. 执行getObject()获取早期引用(可能经过AOP处理)
  6. 将OrderService的引用注入UserService
  7. 完成UserService的初始化,移入一级缓存

3.3 最终初始化

  1. 回到OrderService的属性注入
  2. 此时可以直接从一级缓存获取完整的UserService
  3. 完成OrderService的初始化
  4. 移入一级缓存,清除下级缓存中的相关条目

这个过程中,二级缓存earlySingletonObjects主要起到性能优化作用,避免重复执行ObjectFactory的getObject()方法。

4. AOP代理的特殊处理机制

当涉及AOP代理时,情况会变得更复杂。假设OrderService需要被JDK动态代理:

// 在ObjectFactory中处理的逻辑 protected Object getEarlyBeanReference(String beanName, Object bean) { // 如果需要代理,在这里创建代理对象 if (!earlyProxyReferences.contains(beanName) && isSingletonCurrentlyInCreation(beanName)) { return wrapIfNecessary(bean, beanName); } return bean; }

三级缓存存放ObjectFactory而非直接存原始对象,就是为了在获取早期引用时有机会介入AOP处理。这种设计保证了:

  • 最终注入的总是代理对象
  • 避免重复创建代理
  • 保持代理对象的单例特性

5. 典型问题排查与解决方案

5.1 构造器注入失效问题

三级缓存只能解决属性注入(setter注入)的循环依赖。如果使用构造器注入,在实例化阶段就需要完整依赖,此时缓存机制无法介入。解决方案:

  • 改为属性注入
  • 使用@Lazy延迟加载
  • 重构代码消除循环

5.2 原型Bean的循环依赖

原型(prototype)作用域的Bean无法使用三级缓存,因为:

  • Spring不缓存原型Bean的实例
  • 每次注入都需要创建新对象 解决方案只能是重构代码结构。

5.3 自我依赖检测

Spring会通过inCreationCheckExclusions集合检测自我依赖:

// AbstractBeanFactory if (isPrototypeCurrentlyInCreation(beanName)) { throw new BeanCurrentlyInCreationException(beanName); }

6. 性能优化与实现细节

6.1 缓存访问顺序优化

Spring在获取Bean时采用层级检查策略:

  1. 先查一级缓存singletonObjects(完全初始化的Bean)
  2. 再查二级缓存earlySingletonObjects(早期引用)
  3. 最后查三级缓存singletonFactories(工厂对象)

这种设计减少了同步锁的竞争,因为一级缓存是ConcurrentHashMap,而二级缓存只是HashMap。

6.2 并发控制机制

使用singletonsCurrentlyInCreation集合记录正在创建的Bean:

// 使用ThreadLocal避免并发问题 private final ThreadLocal<Object> prototypesCurrentlyInCreation = new NamedThreadLocal<>("Prototype beans currently in creation");

6.3 内存占用控制

三级缓存不会无限增长,因为:

  • 完成初始化的Bean会立即从下级缓存移除
  • 正常情况下缓存数量≈最大依赖深度
  • 采用软引用/弱引用策略防止内存泄漏

在实际项目中,我曾遇到过一个深度达7层的循环依赖链。通过分析发现是领域模型设计不合理导致的,最终通过引入DTO打破了部分循环。这也提醒我们:虽然Spring提供了解决方案,但过度依赖这个特性可能意味着设计需要优化。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询