Spring 三级缓存
一、从生活中的循环依赖说起
想象这样一个场景:你去公司上班需要工牌,但领取工牌需要先登记工位号,而工位号的分配又需要你提供工牌信息。这种"先有鸡还是先有蛋"的困境,在软件开发中就是典型的循环依赖问题。
在Spring框架中,当两个Bean互相依赖时:
java
体验AI代码助手
代码解读
复制代码
@Service public class ServiceA { @Autowired private ServiceB serviceB; } @Service public class ServiceB { @Autowired private ServiceA serviceA; }
Spring是如何解决这个看似无解的问题的呢?答案就藏在三级缓存的巧妙设计中。
二、三级缓存结构全景图
先来看一张Spring容器内部的三级缓存结构示意图:
Bean生命周期
初始化
实例化
填充属性
Spring容器
存放
存放
存放
1.实例化后注册
2.提前暴露
3.最终成品
一级缓存
singletonObjects
三级缓存
singletonFactories
ObjectFactory
二级缓存
earlySingletonObjects
早期对象
完整Bean
- 一级缓存(成品库):
singletonObjects,存放完全初始化好的Bean。 - 二级缓存(半成品库):
earlySingletonObjects,存放提前暴露的原始对象。 - 三级缓存(对象工厂库):
singletonFactories,存放生成对象的工厂。
三、循环依赖解决全流程演示
让我们通过一个具体案例,看看Spring是如何玩转这三个缓存的:
场景设定
ServiceA 依赖 ServiceB,ServiceB 又依赖 ServiceA
伪代码简化版创建流程
- 开始创建ServiceA
- 实例化ServiceA(在堆中开辟内存空间)
- 将ServiceA的ObjectFactory放入三级缓存
- 填充ServiceA的属性(发现需要ServiceB)
- 开始创建ServiceB
- 实例化ServiceB
- 将ServiceB的ObjectFactory放入三级缓存
- 填充ServiceB的属性(发现需要ServiceA)
- 从三级缓存获取ServiceA的ObjectFactory
- ObjectFactory.getObject() → 得到ServiceA的早期引用
- 将ServiceA早期引用放入二级缓存,清除三级缓存
- ServiceB完成属性注入,初始化后放入一级缓存
- ServiceA继续完成属性注入和初始化,最终放入一级缓存
关键步骤图解
ServiceAServiceB三级缓存二级缓存一级缓存注册ObjectFactory需要注入查询A的工厂返回ObjectFactory执行getObject()转移A的早期引用完成初始化完成初始化ServiceAServiceB三级缓存二级缓存一级缓存
四、为什么要用三级缓存?
1. 看似多余的三级缓存
很多同学会有疑问:为什么需要三级缓存而不是两级?我们通过对比实验来说明:
实验场景:使用AOP代理的Bean。
java
体验AI代码助手
代码解读
复制代码
@Service public class ServiceA { @Autowired private ServiceB serviceB; @Transactional // 需要生成代理 public void method() {} }
两级缓存方案的问题:
- 第一次从缓存获取时直接创建代理对象。
- 如果后续初始化过程中Bean被修改,会导致代理对象与原始对象状态不一致。
三级缓存的优势:
- 延迟代理对象的生成时机。
- 保证最终放入容器的对象是完整的代理对象。
2. 各级缓存的职责划分
| 缓存级别 | 存储内容 | 生命周期 | 作用 |
|---|---|---|---|
| 一级缓存 | 完整的Bean实例 | 整个应用运行期间 | 提供最终可用的Bean |
| 二级缓存 | 原始对象的早期引用 | 创建到初始化完成前 | 解决循环依赖 |
| 三级缓存 | 生成对象的ObjectFactory | 实例化后到放入二级前 | 支持AOP等需要后置处理的情况 |
五、经典问题解答
Q1:构造器注入为何无法解决循环依赖?
java
体验AI代码助手
代码解读
复制代码
// 构造器注入示例 @Service public class ServiceA { private final ServiceB serviceB; public ServiceA(ServiceB serviceB) { this.serviceB = serviceB; } }
当使用构造器注入时,对象在实例化阶段就需要完成依赖注入,此时:
- ServiceA实例化需要ServiceB。
- ServiceB实例化又需要ServiceA。
- 两者都无法完成实例化,导致死循环。
Q2:三级缓存与设计模式
这里运用了多种设计模式:
- 工厂模式:通过ObjectFactory延迟对象创建。
- 外观模式:AbstractBeanFactory统一处理缓存。
- 代理模式:处理AOP等需要生成代理的情况。
六、最佳实践与避坑指南
-
避免循环依赖(即使Spring能解决)
- 使用
@Autowired而非构造器注入。 - 定期运行
mvn dependency:analyze检查依赖。 - 重构代码,引入中间层打破循环。
- 使用
-
调试技巧
java
体验AI代码助手
代码解读
复制代码
// 查看缓存状态 DefaultSingletonBeanRegistry registry = (DefaultSingletonBeanRegistry)context.getAutowireCapableBeanFactory(); System.out.println("一级缓存:" + registry.getSingletonNames()); -
性能优化
- 合理设置Bean的作用域。
- 避免过度使用@Autowired。
- 及时清理不需要的Bean。
七、总结与思考
Spring 的三级缓存机制通过 提前暴露半成品对象 的巧妙设计,解决了循环依赖的难题。 三级缓存设计体现了几个精妙之处:
- 空间换时间:通过缓存提升性能。
- 关注点分离:每级缓存职责单一。
- 延迟加载:ObjectFactory的灵活运用。
最后:三级缓存也并不能解决所有的循环依赖问题,但了解其机制原理,能帮我更好的解决类似的抽象问题。
作者:四七伵
链接:https://juejin.cn/post/7477883001449005091
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
更多推荐




所有评论(0)