用生活中的例子初步理解Spring如何解决循环依赖

用生活中的例子初步理解Spring如何解决循环依赖 一、什么是循环依赖当多个 Bean 互相依赖时ComponentpublicclassA{AutowiredprivateBb;}ComponentpublicclassB{AutowiredprivateAa;}A 的实例化需要 BB 的实例化又需要 A形成循环依赖就像“先有鸡还是先有蛋”一样。二、Spring 默认使用三级缓存解决循环依赖Spring 通过三级缓存三个 Map来处理单例 Bean 的循环依赖一级缓存singletonObjects存储已经完全初始化的 Bean包括没有循环依赖的普通 Bean。二级缓存earlySingletonObjects存储已实例化但尚未完成属性注入和初始化的 Bean 的早期引用半成品。当需要提前暴露时会将早期引用从三级缓存移到此处。三级缓存singletonFactories存储ObjectFactory对象工厂用于生成 Bean 的早期引用。它允许在 Bean 未完全初始化时先暴露一个工厂以便其他依赖方获取引用可能是原始对象或代理对象。工作流程简化实例化 A → 将 A 的ObjectFactory放入三级缓存。填充 A 的属性发现需要 B → 去缓存找 B。实例化 B → 将 B 的ObjectFactory放入三级缓存。填充 B 的属性发现需要 A → 从三级缓存获取 A 的工厂生成 A 的早期引用如果 A 需要代理此时可能生成代理对象→ 将早期引用放入二级缓存并删除三级缓存中的 A 工厂。B 获得 A 的早期引用即使 A 还不完整完成 B 的创建 → B 进入一级缓存。回到 A 的注入将 B完整对象注入 A → A 完成初始化 → A 也进入一级缓存。通过这种机制循环依赖得以解开。注意三级缓存的核心是工厂而非早期引用本身。这种设计允许 Spring 在 Bean 未完全初始化时提前暴露一个可被引用的“占位对象”。三、浅显易懂的理解方式叠纸箱比喻比喻是为了帮助理解与真实实现细节不完全等价但思路相通。叠一个四个面互相依赖的纸箱每个面依赖前一个面完成立起四个面都未完成折叠→ 相当于 Spring 的二级缓存对象已实例化但还不完整。将其中一个面弯曲成斜面→ 这个斜面虽然没完成全部折叠但已经能与其它面连接 → 相当于 Spring 的三级缓存提供的“早期引用”一个可被引用的临时状态。利用斜面打破循环依次完成所有面的折叠 → 最终四个面都折叠完成 → 相当于进入一级缓存。当 Spring 遇到循环依赖时它先创建一个不完整的 Bean早期引用如同“弯一个斜面”让其他 Bean 先引用这个不完整的实例从而逐步完成整个对象的初始化。四、构造注入导致的循环依赖构造注入要求在构造时传入所有依赖无法像 setter 或字段注入那样先实例化再赋值。因此 Spring 的三级缓存无法解决构造注入的循环依赖会抛出BeanCurrentlyInCreationException。解决方案在循环依赖链上的某个构造参数上添加Lazy注解让 Spring 为其生成一个代理对象延迟实际 Bean 的创建。ComponentpublicclassA{privatefinalBb;AutowiredpublicA(LazyBb){// 这里使用 Lazythis.bb;}}ComponentpublicclassB{privatefinalCc;AutowiredpublicB(Cc){this.cc;}}ComponentpublicclassC{privatefinalDd;AutowiredpublicC(Dd){this.dd;}}ComponentpublicclassD{privatefinalAa;AutowiredpublicD(Aa){this.aa;}}原理用叠箱子理解原本 A 依赖 BB 依赖 CC 依赖 DD 依赖 A。在 A 的构造参数 B 上加LazyA 创建时不需要真实的 B只需要一个“板子代理”放在 B 的位置。于是 A 可以顺利实例化并放入一级缓存。之后 B、C、D 依次创建时都能从一级缓存拿到已完成的 A从而解开循环。最终当真正调用 B 的方法时代理会触发 B 的真实初始化此时所有依赖都已就绪。注意Lazy加在入口依赖上比如 A→B 的这一边而不是加在 Bean 的类上。五、多例模式循环依赖为什么无法解决单例 BeanSpring 通过三级缓存暂存不完整的对象允许打破循环。多例 BeanScope(prototype)每次获取都会创建全新实例且 Spring不缓存多例 Bean。当 A、B、C、D 都是多例且互相依赖时获取 A 会触发无限递归A → B → C → D → A → B → C → D → A → ...最终导致StackOverflowError或BeanCurrentlyInCreationException。因此Spring 无法解决多例 Bean 的循环依赖应避免这种设计。总结Spring 通过三级缓存巧妙解决了单例 Bean 的字段/setter 注入循环依赖构造注入需借助Lazy多例模式则无法解决需从设计上避免。