咱们聊聊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类),感受下三级缓存的"物理形态":

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

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

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

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

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

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

(完)

相关推荐
蜂蜜黄油呀土豆4 天前
Spring 自动装配深度解析:@Autowired、@Resource 与自动注入实战指南
spring boot·spring·autowired·依赖注入·resource
L.EscaRC22 天前
深入解析SpringBoot中的循环依赖机制与解决方案
java·spring boot·spring·循环依赖
Kay_Liang1 个月前
Spring IOC核心原理与实战技巧
java·开发语言·spring boot·spring·ioc·依赖注入·控制反转
安冬的码畜日常1 个月前
【JUnit实战3_27】第十六章:用 JUnit 测试 Spring 应用:通过实战案例深入理解 IoC 原理
spring·观察者模式·设计模式·单元测试·ioc·依赖注入·junit5
吹晚风吧2 个月前
spring是如何解决循环依赖的(二级缓存不行吗)?
java·spring·循环依赖·三级缓存
十五年专注C++开发2 个月前
Fruit框架:C++依赖注入解决方案
开发语言·c++·依赖注入·fruit框架
一叶飘零_sweeeet2 个月前
从 “死锁“ 到 “解耦“:重构中间服务破解 Java 循环依赖难题
java·循环依赖
Xxtaoaooo3 个月前
Spring Boot 启动卡死:循环依赖与Bean初始化的深度分析
java·后端·依赖注入·三级缓存机制·spring boot循环依赖
佛祖让我来巡山5 个月前
【Spring三级缓存解密】如何优雅解决循环依赖难题
spring·循环依赖·三级缓存