Spring 循环依赖,别光背三级缓存
先说结论
面试被问循环依赖,很多人张嘴就是“三级缓存”,然后背一遍三个 Map 的名字。但真在项目里遇到 BeanCurrentlyInCreationException 的时候,光知道三级缓存没用,得知道什么情况它能救、什么情况它救不了、救不了的时候该怎么改。
我遇到过的循环依赖,最后基本都是靠改代码解决的,不是靠 Spring 兜底。
Spring 能处理哪些
限定条件挺死的:单例 + 字段注入或 setter 注入。
A 的字段里 @Autowired 了 B,B 的字段里又 @Autowired 了 A,这种 Spring 能绕过去。原理就是 A 实例化完之后、属性还没填的时候,先把 A 的引用提前暴露出去,让 B 能拿到一个“半成品 A”把自己先建完,回头再给 A 填上 B。
构造器注入不行。因为构造器要求“创建 A 的瞬间就把 B 给我”,而 B 又要 A,两边都在等对方完整出生,Spring 连 A 的壳都还没造出来,没东西可以提前暴露,直接报错。Spring Boot 2.6 之后默认还禁止了循环依赖,不手动开 allow-circular-references 的话,字段注入的环也会直接启动失败。
三级缓存到底存了什么
一级 singletonObjects:完整成品,平时 getBean 拿的就是这个。
二级 earlySingletonObjects:半成品,实例化了但属性还没填。从三级缓存里取出来之后放这儿,下次再用就不用再走工厂了。
三级 singletonFactories:存的是 ObjectFactory,一个 Lambda。它的作用不是直接给对象,而是在需要的时候生成早期引用。为什么不能直接存半成品?因为如果这个 Bean 后面要被 AOP 代理,提前暴露的得是代理对象,不是原始对象。工厂模式让“生成代理”这件事延迟到真正被循环依赖引用的时候才发生,没被引用就不生成代理。
所以三级缓存的核心不是“三级”,是延迟决定要不要生成代理。二级缓存做不到这一点,因为放进去的时候代理还没生成。
我实际处理过的几种情况
第一种,构造器注入的环。 两个 Service 互相在构造器里要对方。改法要么把其中一个依赖改成 @Lazy,注入一个代理,真正用的时候才初始化;要么抽第三个小类把公共逻辑挪出去。我一般选后者,@Lazy 只是把问题藏起来了,设计上那个环还在。
第二种,自己注入自己。 之前写过一个类里 @Autowired 了自己,想用代理调方法。Spring 2.6 之后默认不让这么玩了。改成 AopContext.currentProxy() 或者把方法拆到别的 Bean 里。
第三种,@Async 和循环依赖撞上。 有个 Service 带 @Async 方法,又被另一个 Service 注入,启动就报错。原因是异步代理的创建时机和普通 Bean 不一样,三级缓存那套流程对代理对象的处理有坑。最后是把异步方法抽到了独立的组件里,不在有环的 Bean 上挂 @Async。
排查的时候看什么
报错信息一般会直接告诉你哪个 Bean 在创建时撞环了,比如 Requested bean is currently in creation: Is there an unresolvable circular reference?。
日志里搜 Eagerly caching bean 和 using early reference,能看到 Spring 有没有走提前暴露的路径。如果搜不到 using early reference 但报错了,基本就是构造器注入或者代理导致的,缓存流程根本没走到。
小结
循环依赖大部分时候是设计问题,不是配置问题。Spring 的三级缓存是兜底机制,不是鼓励你写环。我现在写新代码的原则很简单:构造器注入,不写环,有环就抽类。真遇到老代码改不动的,@Lazy 临时顶上,但记个 TODO 后面重构。
先记这些。
- 点赞
- 收藏
- 关注作者
评论(0)