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);它们的协作流程可以类比为生产线:
- 当Bean开始创建时,首先在三级缓存注册ObjectFactory
- 遇到依赖注入时,会依次检查各级缓存
- 最终完成初始化的Bean会被提升到一级缓存
3. 完整解决流程的步骤拆解
让我们通过OrderService和UserService的循环依赖案例,看看三级缓存如何运作:
3.1 实例化阶段
- 开始创建OrderService
- 通过反射调用构造函数实例化对象(此时属性都是null)
- 将原始对象包装成ObjectFactory放入三级缓存
- 开始属性注入,发现需要UserService
3.2 依赖解决阶段
- 开始创建UserService
- 同样实例化后放入三级缓存
- 注入属性时发现需要OrderService
- 从三级缓存获取OrderService的ObjectFactory
- 执行getObject()获取早期引用(可能经过AOP处理)
- 将OrderService的引用注入UserService
- 完成UserService的初始化,移入一级缓存
3.3 最终初始化
- 回到OrderService的属性注入
- 此时可以直接从一级缓存获取完整的UserService
- 完成OrderService的初始化
- 移入一级缓存,清除下级缓存中的相关条目
这个过程中,二级缓存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时采用层级检查策略:
- 先查一级缓存singletonObjects(完全初始化的Bean)
- 再查二级缓存earlySingletonObjects(早期引用)
- 最后查三级缓存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提供了解决方案,但过度依赖这个特性可能意味着设计需要优化。