咱们聊聊Spring循环依赖那点事儿:从“死锁”到“三级缓存”的奇妙之旅

最近看了点面试题,发现Spring循环依赖,一二三级缓存还是一个盲点,估计很多人也是一样吧,就专门查了资料了解了这部分内容,希望给在这部分内容茫然的同仁们一点点启发,先赞后看你必能学会👍💗~ ~ ~

你有没有写过这样的代码:两个类A和B,A里要用到B,B里又要用到A,结果Spring启动时“啪”地抛了个BeanCurrentlyInCreationException,告诉你“循环依赖了”?别慌,这事儿Spring其实早有预案——今天咱们就用最接地气的方式,把这个“死锁”怎么破、三级缓存怎么玩,掰开揉碎讲明白。

一、先举个“生活化”的例子:机器人组装厂的死锁危机

想象你在开个机器人组装厂(这就是Spring容器),专门生产各种机器人(Bean)。每个机器人得按流程造:先搭骨架(实例化,调构造函数)→ 装零件(填属性,比如依赖其他机器人)→ 测试出厂(初始化,调@PostConstruct等方法)→ 合格了进“成品仓库”(一级缓存),随时能领用。

某天接了两个订单:造A机器人和B机器人。

  • A的说明书:“我得装个B的核心零件才能干活!”(A依赖B)
  • B的说明书:“我得装个A的能源核心才能启动!”(B依赖A)

工人开工了:

  1. 先造A:搭好骨架(A的“裸体”对象),准备装零件时发现要B——B还没造呢!
  2. 转头造B:搭好骨架(B的“裸体”对象),准备装零件时发现要A——A也没造完呢!

得,A等B,B等A,俩机器人都卡在“等零件”这一步,工厂差点停工。这就是循环依赖:两个Bean互相指着对方说“你得先给我,我才完整”,结果谁都动不了。

二、Spring的“救场神器”:三级缓存是个啥?

厂长急中生智,搞了个“半成品暂存系统”——这就是Spring大名鼎鼎的三级缓存。简单说,就是给刚搭好骨架的机器人发张“预订券”,谁急着用,先领个“毛坯版”顶上,等正式零件造好再替换。

这个系统分三层(对应DefaultSingletonBeanRegistry类里的三个Map):

缓存层级 比喻说法 真实身份(类名) 存啥玩意儿?
一级缓存 成品仓库 singletonObjectsConcurrentHashMap 完全造好的机器人(成品Bean):实例化+装零件+测试全搞定,随时能领。
二级缓存 毛坯暂存处 earlySingletonObjectsHashMap 刚搭好骨架的“裸体”机器人(早期对象),或从三级缓存“兑换”来的毛坯(可能带“贴膜”=AOP代理)。
三级缓存 工厂仓库(预订券) singletonFactoriesHashMap “预订券”(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类),感受下三级缓存的“物理形态”:

// 一级缓存:成品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()里:

// 实例化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标注):

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; // 返回原始对象或代理对象  
}  

六、咋避免循环依赖?老司机的建议

  1. 优先用构造器注入“排雷”:构造器注入一报错,你就知道“这儿有循环依赖,得重构!”,倒逼你把代码解耦(比如引入中间层Service)。
  2. 实在绕不开,用Setter/字段注入:Spring的三级缓存只认这种“实例化后装零件”的注入方式。
  3. @Lazy注解“缓兵之计”:在构造器注入的某个依赖上加@Lazy,Spring会注入个“代理对象”(相当于“提货单”),等真用的时候再去领成品,打破死锁。
  4. 别用Prototype作用域:每次new一个对象,三级缓存根本帮不上忙,循环依赖必炸。

七、总结:三级缓存的本质

Spring的三级缓存(singletonFactoriesearlySingletonObjectssingletonObjects),说白了就是“用空间换时间”:提前暴露半成品(毛坯),让依赖方先用着,等正式零件造好再替换。核心是用ObjectFactory工厂“延迟生成早期引用”,顺便搞定AOP代理的坑。

不过话说回来,循环依赖能解决不代表应该出现——它往往是代码耦合太高的信号。理解了三级缓存的原理,下次遇到循环依赖,你不仅能知道“为啥报错”,还能笑着跟同事说:“来,咱用@Lazy或者重构一下,别让机器人组装厂再停工啦!”

(完)

posted @ 2025-12-14 15:19  佛祖让我来巡山  阅读(127)  评论(0)    收藏  举报

佛祖让我来巡山博客站 - 创建于 2018-08-15

开发工程师个人站,内容主要是网站开发方面的技术文章,大部分来自学习或工作,部分来源于网络,希望对大家有所帮助。

Bootstrap中文网