spring框架里有循环依赖问题,讲解

整理优化后的专业面试回答

1. 什么是循环依赖

循环依赖指两个或多个Bean之间形成相互依赖的闭环,典型场景是Bean A依赖Bean B,同时Bean B也依赖Bean A。

Spring单例Bean的创建遵循「实例化 → 属性填充 → 初始化」的完整生命周期。正常创建A时,容器发现A依赖B,就会暂停A的创建,转而去实例化B;实例化B时又发现B依赖A,此时A和B都处于创建中、互相等待对方完成,最终导致Bean无法正常创建,形成死锁。

2. Spring解决循环依赖的核心:三级缓存+提前暴露

Spring通过三级缓存机制解决单例场景下的循环依赖,核心逻辑是「提前暴露半成品Bean」:
在Bean完成实例化、但还没进行属性填充和初始化时,就将这个“半成品”的引用提前暴露到缓存中。其他依赖它的Bean可以先拿到这个引用完成自身的创建流程,等后续目标Bean完成全部初始化后,依赖方持有的引用也会对应补全属性,最终都成为完整对象。

3. 为什么用三级缓存,而不是二级缓存

核心原因是保证AOP代理场景下,Bean引用的一致性。

先明确三级缓存的分工:

  • 一级缓存 singletonObjects:存放完全初始化完成的「成品Bean」
  • 二级缓存 earlySingletonObjects:存放已实例化、但未完成属性填充和初始化的「早期半成品Bean」
  • 三级缓存 singletonFactories:存放ObjectFactory工厂对象,用于生成早期Bean的引用

如果只有二级缓存,提前暴露出去的是原始Bean对象的引用。但如果该Bean需要通过AOP生成代理对象,那么Bean初始化完成后,容器中最终存在的是代理对象;而那些提前拿到原始Bean引用的依赖方,手里持有的依然是原始对象,和容器最终的代理对象不一致,出现引用错误。

三级缓存的设计解决了这个问题:存入缓存的不是Bean实例本身,而是一个ObjectFactory工厂。当其他Bean需要获取该Bean的早期引用时,会调用工厂的getObject()方法——这个方法会判断当前Bean是否需要AOP代理:需要代理就生成并返回早期代理对象,不需要代理就直接返回原始Bean对象。
这样就能保证所有依赖方拿到的引用,和容器最终完成初始化后的Bean是同一个对象,既解决了循环依赖,又兼容了AOP代理的逻辑。

4. 三级缓存能解决所有循环依赖吗

不能,三级缓存仅对单例模式下的setter注入/字段注入有效,无法解决构造器注入的循环依赖。

原因:
提前暴露的前提是Bean已经完成「实例化」。对于setter/字段注入,Bean可以先通过无参构造器实例化,再后续通过setter方法或反射注入依赖,因此有机会提前暴露半成品。
但构造器注入要求在实例化阶段就必须传入依赖的Bean,此时目标Bean还没完成实例化,无法放入三级缓存提前暴露,因此三级缓存完全无法介入,最终容器会抛出BeanCurrentlyInCreationException异常。

对应的解决办法:

  1. 将构造器注入改为setter注入或字段注入,适配三级缓存的提前暴露机制;
  2. 给其中一方的依赖添加@Lazy注解,实现延迟加载:容器启动时先为依赖生成代理对象,真正调用时才完成目标Bean的初始化,打破循环依赖的闭环。

posted @ 2026-09-14 22:50  堭鍙銤  阅读(10)  评论(0)    收藏  举报