最近看了点面试题,发现Spring循环依赖,一二三级缓存还是一个盲点,估计很多人也是一样吧,就专门查了资料了解了这部分内容,希望给在这部分内容茫然的同仁们一点点启发,先赞后看你必能学会👍💗~ ~ ~
你有没有写过这样的代码:两个类A和B,A里要用到B,B里又要用到A,结果Spring启动时"啪"地抛了个BeanCurrentlyInCreationException,告诉你"循环依赖了"?别慌,这事儿Spring其实早有预案------今天咱们就用最接地气的方式,把这个"死锁"怎么破、三级缓存怎么玩,掰开揉碎讲明白。
一、先举个"生活化"的例子:机器人组装厂的死锁危机
想象你在开个机器人组装厂(这就是Spring容器),专门生产各种机器人(Bean)。每个机器人得按流程造:先搭骨架(实例化,调构造函数)→ 装零件(填属性,比如依赖其他机器人)→ 测试出厂(初始化,调@PostConstruct等方法)→ 合格了进"成品仓库"(一级缓存),随时能领用。
某天接了两个订单:造A机器人和B机器人。
- A的说明书:"我得装个B的核心零件才能干活!"(A依赖B)
- B的说明书:"我得装个A的能源核心才能启动!"(B依赖A)
工人开工了:
- 先造A:搭好骨架(A的"裸体"对象),准备装零件时发现要B------B还没造呢!
- 转头造B:搭好骨架(B的"裸体"对象),准备装零件时发现要A------A也没造完呢!
得,A等B,B等A,俩机器人都卡在"等零件"这一步,工厂差点停工。这就是循环依赖:两个Bean互相指着对方说"你得先给我,我才完整",结果谁都动不了。
二、Spring的"救场神器":三级缓存是个啥?
厂长急中生智,搞了个"半成品暂存系统"------这就是Spring大名鼎鼎的三级缓存。简单说,就是给刚搭好骨架的机器人发张"预订券",谁急着用,先领个"毛坯版"顶上,等正式零件造好再替换。
这个系统分三层(对应DefaultSingletonBeanRegistry类里的三个Map):
| 缓存层级 | 比喻说法 | 真实身份(类名) | 存啥玩意儿? |
|---|---|---|---|
| 一级缓存 | 成品仓库 | singletonObjects(ConcurrentHashMap) |
完全造好的机器人(成品Bean):实例化+装零件+测试全搞定,随时能领。 |
| 二级缓存 | 毛坯暂存处 | earlySingletonObjects(HashMap) |
刚搭好骨架的"裸体"机器人(早期对象),或从三级缓存"兑换"来的毛坯(可能带"贴膜"=AOP代理)。 |
| 三级缓存 | 工厂仓库(预订券) | singletonFactories(HashMap) |
"预订券"(ObjectFactory工厂对象):凭券能现场领个毛坯机器人(含贴膜逻辑)。 |
三、三级缓存咋破解死锁?一步步看流程(附"流程图")
还是用A→B→A的例子,咱们跟着工人师傅走一遍:
1. 造A(实例化)→ 发"预订券"进三级缓存 → 装零件时发现要B
↓
2. 造B(实例化)→ 发"预订券"进三级缓存 → 装零件时发现要A
↓
3. B找A:成品库(一级)无→毛坯暂存处(二级)无→工厂仓库(三级)找到A的"预订券"
↓
4. 拿A的券"兑换":工厂现场给A的毛坯(裸体骨架,要代理就贴膜)→ 毛坯进二级缓存,券从三级缓存删掉
↓
5. 把A的毛坯当零件装给B → B装完测试 → 送进成品库(一级缓存)
↓
6. 回头给A装零件:去成品库领B → A装完测试 → 送进成品库(一级缓存)
结果:A和B都造好了!死锁解开,靠的就是"先领毛坯顶上,再补零件"的思路。
四、关键原理:为啥三级缓存这么设计?
1. 为啥构造器注入会"死锁"?
如果用构造器注入(比如A的构造函数必须传B,B的构造函数必须传A),那问题就大了:造A得先有B,造B得先有A------俩机器人连骨架都没搭起来(实例化都没完成),哪来的"预订券"进三级缓存?这不就死锁了吗?所以构造器注入的循环依赖,Spring直接摆烂:抛异常!
2. 为啥需要三级缓存,两级不行吗?
假设只有"成品库"(一级)和"毛坯暂存处"(二级):
- 造A时,得先把A的毛坯放进二级缓存(不然B找A时找不到),但毛坯要不要用AOP代理(比如加日志、事务)?
- 如果A本来不需要代理,提前放毛坯没问题;但如果A需要代理,放原始毛坯就错了(应该用代理对象)。
三级缓存的聪明之处在于:用"预订券"(ObjectFactory)延迟生成毛坯 。只有真的发生循环依赖(比如B急着要A),才调用ObjectFactory.getObject()生成毛坯(顺便判断要不要代理),生成后放进二级缓存。这样既避免了"提前代理"的浪费,又保证了代理的正确性。
五、源码瞅一眼:三级缓存的真实面目
光说不练假把式,咱们看段Spring源码(DefaultSingletonBeanRegistry类),感受下三级缓存的"物理形态":
java
// 一级缓存:成品Bean(key: bean名, value: 成品Bean)
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
// 二级缓存:早期Bean(毛坯,key: bean名, value: 原始对象或代理对象)
private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);
// 三级缓存:ObjectFactory工厂(key: bean名, value: 生成早期引用的工厂)
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
关键方法:提前暴露"预订券"
在Bean实例化后(调完构造函数),Spring会把ObjectFactory放进三级缓存,代码在AbstractAutowireCapableBeanFactory.doCreateBean()里:
java
// 实例化Bean后,暴露早期引用工厂到三级缓存
boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences && isSingletonCurrentlyInCreation(beanName));
if (earlySingletonExposure) {
// 把ObjectFactory放进三级缓存,工厂逻辑是调用getEarlyBeanReference生成早期引用
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
}
// addSingletonFactory方法:往三级缓存塞ObjectFactory
protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
synchronized (this.singletonObjects) {
if (!this.singletonObjects.containsKey(beanName)) {
this.singletonFactories.put(beanName, singletonFactory); // 三级缓存存工厂
this.earlySingletonObjects.remove(beanName); // 清二级缓存(防止重复)
this.registeredSingletons.add(beanName);
}
}
}
getEarlyBeanReference:判断是否要"贴膜"(AOP代理)
这个方法是生成早期引用的核心,会检查Bean是否需要AOP代理(比如被@Transactional标注):
java
protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
Object exposedObject = bean;
// 遍历所有BeanPostProcessor,处理早期引用(比如AOP代理)
if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
for (BeanPostProcessor bp : getBeanPostProcessors()) {
if (bp instanceof SmartInstantiationAwareBeanPostProcessor) {
SmartInstantiationAwareBeanPostProcessor ibp = (SmartInstantiationAwareBeanPostProcessor) bp;
exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName); // AOP代理在这儿生成
}
}
}
return exposedObject; // 返回原始对象或代理对象
}
六、咋避免循环依赖?老司机的建议
- 优先用构造器注入"排雷":构造器注入一报错,你就知道"这儿有循环依赖,得重构!",倒逼你把代码解耦(比如引入中间层Service)。
- 实在绕不开,用Setter/字段注入:Spring的三级缓存只认这种"实例化后装零件"的注入方式。
- 加
@Lazy注解"缓兵之计" :在构造器注入的某个依赖上加@Lazy,Spring会注入个"代理对象"(相当于"提货单"),等真用的时候再去领成品,打破死锁。 - 别用Prototype作用域:每次new一个对象,三级缓存根本帮不上忙,循环依赖必炸。
七、总结:三级缓存的本质
Spring的三级缓存(singletonFactories→earlySingletonObjects→singletonObjects),说白了就是"用空间换时间 ":提前暴露半成品(毛坯),让依赖方先用着,等正式零件造好再替换。核心是用ObjectFactory工厂"延迟生成早期引用",顺便搞定AOP代理的坑。
不过话说回来,循环依赖能解决不代表应该出现------它往往是代码耦合太高的信号。理解了三级缓存的原理,下次遇到循环依赖,你不仅能知道"为啥报错",还能笑着跟同事说:"来,咱用@Lazy或者重构一下,别让机器人组装厂再停工啦!"
(完)