Java七大热门技术框架源码解析 | 完结25章
获课:yinheit.xyz/5699/
Spring循环依赖三级缓存源码解剖:为何用早期引用解决Bean依赖?
Spring框架处理循环依赖的三级缓存机制是其IoC容器最精妙的设计之一。本文将深入剖析三级缓存的工作机制,重点解析"早期引用"如何破解循环依赖这一经典难题,揭示Spring在保证单例完整性与解决依赖循环之间的精妙平衡。通过逐层解剖
DefaultSingletonBeanRegistry的核心实现,理解Spring如何在不破坏Bean生命周期的情况下,优雅地处理各种复杂的依赖场景。
一、循环依赖的本质与Spring的应对挑战
1.1 循环依赖的三种形态
循环依赖在Spring应用中表现为三种典型模式:
- 构造器循环依赖:A的构造器需要B,B的构造器又需要A
- 属性循环依赖:A依赖B的属性,B又依赖A的属性
- 方法循环依赖:A的初始化方法调用B,B又回调A的方法
其中构造器循环依赖无法通过三级缓存解决,因为对象尚未创建完成时无法放入缓存,这也是Spring官方文档明确建议避免构造器注入循环依赖的根本原因。
1.2 解决循环依赖的核心矛盾
Spring需要同时保证两个看似冲突的原则:
- 单例完整性:完全初始化后的Bean才能被使用
- 依赖可满足:Bean创建过程中需要能获取依赖对象
早期引用(Early Reference)正是解决这一矛盾的关键创新,它允许半成品Bean被临时暴露用于依赖注入,待全部初始化完成后再替换为完整Bean。
二、三级缓存机制深度解析
2.1 缓存层级结构与职责划分
DefaultSingletonBeanRegistry中定义了三级缓存的核心结构:
一级缓存(singletonObjects):
- 存储完全初始化好的成品Bean
- ConcurrentHashMap实现,线程安全
- 最终获取Bean的入口
二级缓存(earlySingletonObjects):
- 存储已实例化但未初始化的半成品Bean
- HashMap实现(非线程安全)
- 解决循环依赖的临时过渡区
三级缓存(singletonFactories):
- 存储Bean的ObjectFactory
- 可以生成Bean的早期引用
- 最关键的循环依赖处理层
2.2 Bean创建过程与缓存流转
典型Bean生命周期在缓存中的状态变迁:
- 开始创建:标记beanName为"创建中"状态
- 实例化:通过反射调用构造器创建原始对象
- 放入三级缓存:添加ObjectFactory到singletonFactories
- 属性填充:从一级/二级/三级缓存获取依赖Bean
- 初始化:执行Aware接口、BeanPostProcessor等
- 移入一级缓存:从三级缓存移除,成品放入singletonObjects
2.3 早期引用的生成时机
当BeanA依赖BeanB,而BeanB又依赖BeanA时:
- BeanA实例化后,其ObjectFactory被放入三级缓存
- 填充BeanA属性时发现需要BeanB,开始创建BeanB
- BeanB填充属性时从三级缓存获取BeanA的ObjectFactory
- ObjectFactory.getObject()返回BeanA的早期引用(可能是代理对象)
- BeanB完成初始化后,BeanA继续完成剩余初始化
这个过程中,BeanA的早期引用允许BeanB先完成创建,打破了循环等待的死结。
三、设计决策的深层考量
3.1 为何需要三级而非两级缓存
常见的疑问是:为何不直接使用二级缓存?三级结构带来了三个关键优势:
- 代理对象一致性:保证AOP代理的Bean在整个容器中只有一个版本
- 延迟决策能力:ObjectFactory可以等到真正需要时再决定返回原始对象还是代理
- 生命周期完整性:避免半成品Bean被直接暴露给用户代码
在Spring 5.2之前的版本中,如果没有AOP代理,理论上可以简化为两级缓存。但为了架构统一性和扩展性,Spring始终保持三级结构。
3.2 早期引用与代理对象的协同
当存在AOP切面时,
SmartInstantiationAwareBeanPostProcessor会介入早期引用的生成:
- getEarlyBeanReference方法被调用
- 后处理器决定返回原始对象或包装代理
- 保证最终放入一级缓存的对象与早期引用类型一致
这个机制确保了即使用了AOP,循环依赖也能正常工作,且代理对象只生成一次。
3.3 与Bean生命周期的完美融合
三级缓存设计与Spring的标准Bean生命周期无缝衔接:
- 实例化阶段:对象创建后立即放入三级缓存
- 属性填充:依赖解析可能触发早期引用获取
- 初始化阶段:在所有依赖满足后执行
- 销毁阶段:从各级缓存中统一清理
这种设计确保了即使在循环依赖场景下,Bean的生命周期回调(如InitializingBean、@PostConstruct)仍能按正确顺序执行。
四、典型场景与特殊案例
4.1 属性注入的成功案例
处理流程:
- ServiceA实例化后,其ObjectFactory放入三级缓存
- 注入ServiceB时触发ServiceB创建
- ServiceB注入ServiceA时从三级缓存获取早期引用
- 双方都完成属性注入和初始化
4.2 构造器注入的失败案例
失败原因:
- 构造器执行前对象尚未创建,无法放入缓存
- 导致StackOverflowError递归调用
4.3 原型Bean的限制
原型(prototype)作用域的Bean无法使用三级缓存:
- 每次获取都应该是全新实例
- Spring直接抛出BeanCurrentlyInCreationException
- 设计上应避免原型Bean的循环依赖
五、实现细节与性能考量
5.1 并发安全的设计
三级缓存在并发环境下的保护措施:
- singletonObjects:使用ConcurrentHashMap
- 创建标记:synchronized加锁检查"正在创建"集合
- 双检锁模式:避免重复创建单例Bean
- 内存可见性:所有缓存字段声明为volatile
5.2 缓存访问的性能影响
各级缓存的访问频率统计:
- 一级缓存:命中率>99%(大多数Bean无循环依赖)
- 二级缓存:仅循环依赖场景使用
- 三级缓存:临时存储,生命周期短
Spring通过这种分层设计,确保在无循环依赖的常规路径上几乎没有任何性能损耗。
5.3 与其它容器特性的交互
三级缓存与Spring其它特性的协作:
- 延迟初始化:不影响缓存机制,首次真实访问才触发
- depends-on:强制依赖顺序,可能避免某些循环
- FactoryBean:特殊处理,实际存储的是getObject()结果
- BeanPostProcessor:可能修改最终放入缓存的对象
六、最佳实践与避坑指南
6.1 推荐的依赖模式
- 优先使用setter注入:更友好支持循环依赖
- 避免构造器循环:通过代码结构调整消除
- 接口解耦:依赖抽象而非具体实现
- 分层清晰:领域层与技术实现层分离
6.2 常见问题排查
当出现循环依赖问题时:
- 检查是否构造器注入导致的无法解决类型
- 确认Bean作用域是否为prototype
- 排查AOP代理是否改变了Bean类型
- 分析Bean初始化顺序是否有矛盾
6.3 设计模式替代方案
对于复杂依赖场景,可考虑:
- 事件驱动:ApplicationEvent解耦直接依赖
- 懒加载:@Lazy延迟某些依赖的初始化
- 方法注入:Lookup方法动态获取依赖
- 面向切面:通过AOP实现横切关注点
Spring的三级缓存机制展现了框架设计者在理论严谨性与实践灵活性之间的精妙平衡。早期引用的引入不仅解决了循环依赖的技术难题,更体现了"不完备对象也可临时使用"的务实哲学。理解这一机制有助于开发者编写更健壮的Spring应用,在享受依赖注入便利的同时,避免陷入复杂的依赖网。记住,三级缓存是Spring提供的安全网,而非鼓励随意设计循环依赖的通行证,良好的架构设计始终应该优先于框架的补救能力。