最近在研究spring为什么使用三级缓存而不是两级,发现很多文章博客写的内部有点看不懂,所以自己研究了几天,现在在这里记录一下研究的结果。
spring三级缓存解决的问题:
Spring 使用三级缓存 (而不是两级)来解决循环依赖,主要目的是兼容 AOP(动态代理)场景 ,同时保持 Bean 创建过程的语义一致性 和扩展性。
spring三级缓存存储内容:
一级缓存:普通 bean(成品),存放完全初始化好的 bean(可能已代理)
二级缓存:早期普通对象(半成品),已实例化但还没完成属性填充和初始化的原始对象
三级缓存:ObjectFactory(lambda 工厂),生成早期引用的工厂(通常是 getEarlyBeanReference 的 lambda)
spring Bean的生命周期:
实例化 -> 属性注入 -> 初始化 -> 使用 ->销毁
具体实现思路
假设现在领导给我布置了一个任务,研究一下spring为什么使用三级缓存而不是两级,那么我应该从哪里入手呢?很快我想到了一个方案。
第一种方案(二级缓存存储工厂,一级缓存存储对象):
二级缓存缓存工厂对应spring中的第三级缓存,一级缓存存储成品对象以及半成品对象.
这一种方案必须将spring的二级缓存和一级缓存混合在了一起,因为一级缓存就必须缓存从工厂中生成的半成品对象,以及完成了初始化的成品对象,我左思右想发现这一种方案存在巨大的隐患,就是一级缓存中混淆成品与半成品,区分不了,在getBean的时候拿到的可能是半成品对象,没有进行属性注入和初始化,再拿这个半成品对象去进行操作可能会报空指针异常
这种设计违违背了单一职责且这种存在安全隐患的设计是非常不合理的
那我要解决这个问题可以这么解决
1.在对象中添加标识,标识是成品对象还是半成品对象
2.再加一个临时map,缓存半成品bean
第一种翻案不符合单一原则,而第二种方案不就是现在的三级缓存吗。
所以第一种方案最终的结论就是不可行。
第二种方案(二级缓存存储半成品对象,一级缓存存储成品对象):
现在我又想到了一种方案,来继续分析验证下看可不可行.
这次我不要工厂了,我直接在bean实例化后将这个实力化后的bean放入二级缓存,在这个bean属性填充以及初始化后将这个bean放入一级缓存,这样总行了吧....
思考研究了半小时再对比spirng的三级缓存,我发现这种方案有两个问题
1.所有对象都提前缓存到二级缓存,不管有没有循环依赖
2.二级缓存的是原始对象, 在bean初始化后如果需要会生成代理对象,在bean初始化后最终一级缓存生成的是代理对象,但是在循环依赖注入的时候注入的是二级缓存的原始对象,这会造成注入的对象和最终缓存到一级缓存的对象不一致,从而导致代理失效。
第二种方案最终的结论就是不可行。
第三种方案(二级缓存存储半成品对象或者代理对象,一级缓存存储成品对象):
仔细思考后我对第二种方案进行了优化,我在bean实例化后调用getEarlyBeanReference获取早期bean,这个方法会判断是否需要代理bean,需要的还会返回代理bean,然后我们可以将bean缓存到二级缓存中,在bean初始化后再将初始化好的bean放到一级缓存中。
我发现我的这个优化版的方案在技术上竟然是可行的,然后再对比spring的三级缓存方案,
我发现我这个二级缓存的方案没有那么的优雅.
- 全部触发
getEarlyBeanReference
- 现实情况:一个中型项目通常有几千个 Bean,但真正存在循环依赖的往往只有个位数。
- 我的方案 :几千个 Bean 全部触发
getEarlyBeanReference()。即使最终返回的是原始对象,该方法内部仍需执行:- 遍历所有
SmartInstantiationAwareBeanPostProcessor - 计算 Advisor 匹配度(
@Transactional/@Async/自定义切面) - 反射检查代理类型(JDK/CGLIB)
- 遍历所有
2. 破坏生命周期契约,限制扩展能力
getEarlyBeanReference() 的语义是 "提前暴露早期引用" ,属于循环依赖场景的 **特殊旁路
- 架构语义耦合:把"决策逻辑"和"执行结果"混在一起**
第三种方案在技术上是可以行得通的,但是设计这种 企业级容器核心原因不是"跑通就可以了",
还要在性能、扩展性、生命周期严谨性 之间找到企业级平衡点,第三种方案 违背了按需触发原则,在 Spring 这种追求零开销的通用容器中,需用三级缓存的工厂模式延迟执行这种方案才是最优的.
现在我要把我的研究成功回复给领导了。