死锁式造人:A 要 B,B 要 A
循环依赖的经典现场:OrderService 注入 UserService,UserService 又注入 OrderService。创建 A 发现缺 B,创建 B 发现缺 A——按直觉就是死锁。但 Spring 单例默认能转起来,靠的是提前曝光:A 刚实例化完(构造器跑完、属性还是空的),就把自己的引用先亮出来给 B 用。B 拿到的是「半成品 A」的引用——但引用是同一个内存地址,等 A 后续初始化完成,B 手里的引用指向的对象就自动「长成」了成品。变的是对象内部状态,不是引用本身,所以安全。
三个缓存,各司其职
一级 singletonObjects 成品池:初始化完成的单例
二级 earlySingletonObjects 半成品池:提前曝光的早期引用
三级 singletonFactories 工厂池:能产出早期引用的 ObjectFactory取值按一、二、三级依次找。流程串起来:A 实例化后先把自己的工厂放进三级缓存 → 填充属性发现要 B → 创建 B → B 要 A → 从三级缓存找到 A 的工厂,调用它拿到早期引用,升级进二级缓存(三级删除)→ B 完成,进一级 → A 继续完成,进一级。注意关键细节:三级存的不是对象,是工厂——答案就埋在这。
为什么是三级,两级不够吗
二级缓存放早期引用,看似够用。但 Spring 有条设计原则:代理应该在初始化之后才生成(生命周期第 7 步)。若用二级缓存,就得在实例化后立刻生成代理放进去——所有 Bean 都被迫提前 AOP,哪怕绝大多数根本没有循环依赖。三级缓存的 ObjectFactory 是个延迟开关:只有真的发生循环依赖、别的 Bean 来要早期引用时,工厂才被调用(getEarlyBeanReference),此时才决定要不要提前生成代理;没有循环依赖,工厂永远不被调用,代理按正常流程在初始化后生成。三级缓存的本质:把「提前生成代理」这个特例的代价,推迟到确有必要时才付。
边界:三种救不了
其一,构造器注入——还没实例化就要依赖对方,根本没有「实例化后提前曝光」的机会,直接 BeanCurrentlyInCreationException;其二,原型 Bean——不进缓存,无曝光机制;其三,@Async 等在初始化后才生成代理的后置处理器——循环依赖提前曝光的引用与最终代理不是同一个,启动时直接报错拦截。另外要知道官方态度:Spring Boot 2.6 起默认禁止循环依赖,开启要显式配置 allow-circular-references=true——循环依赖是设计问题,不该靠框架无限兜底(第 12 篇细聊怎么改)。
一句话总结:三级缓存 = 成品池 + 半成品池 + 延迟生成代理的工厂池。提前曝光解决「鸡生蛋」,工厂延迟解决「代理时机」——两层设计各管一件事,缺一不可。下一篇讲贯穿生命周期与循环依赖的主角:BeanPostProcessor,Spring 魔法的实施者。
评论 (0)