Spring是如何解决循环依赖
ps:如果这篇帖子对于还在找工作和找实习的你有所帮助,可以关注我,给本贴点赞、评论、收藏并订阅专栏;同时不要吝啬您的花花
一、循环依赖基础认知
循环依赖是指两个或多个Bean之间形成相互依赖的闭环,比如Bean A依赖Bean B,Bean B同时依赖Bean A,若没有特殊处理,容器会陷入无限创建Bean的死循环,最终抛出创建异常。Spring针对循环依赖做了针对性适配,但并非所有循环依赖都能解决,核心仅支持单例Bean的setter/属性注入循环依赖。
1.1 循环依赖常见分类
- 构造器循环依赖:通过构造方法注入依赖,Bean实例化阶段就需要依赖对象,无法打破闭环,Spring无法解决
- setter/属性注入循环依赖:Bean先完成实例化,再通过set方法/字段注入依赖,Spring可通过三级缓存解决
- 原型作用域循环依赖:每次获取原型Bean都会创建新实例,无缓存机制,Spring无法解决
Spring解决循环依赖的核心思路:提前暴露未完全初始化的Bean早期引用,让依赖方先获取引用完成注入,后续再补全Bean的初始化逻辑,打破闭环死锁。
二、Spring解决循环依赖的核心:三级缓存机制
Spring容器在DefaultSingletonBeanRegistry类中维护了三级缓存(三个ConcurrentHashMap),分层存储不同生命周期阶段的Bean,通过缓存协同工作完成依赖注入,三级缓存各司其职、逐级降级获取Bean实例。
2.1 三级缓存详细说明
缓存级别 | 源码变量名 | 存储内容 | 核心作用 |
一级缓存(成品缓存) | singletonObjects | 完全初始化完毕、可直接使用的单例Bean | 存放最终成品Bean,供全局获取,保证单例唯一性 |
二级缓存(半成品缓存) | earlySingletonObjects | 已实例化、未完成初始化的早期Bean(原始对象/代理对象) | 缓存提前暴露的早期引用,避免重复创建早期Bean,解决普通循环依赖 |
三级缓存(工厂缓存) | singletonFactories | Bean工厂对象(ObjectFactory),延迟生成早期Bean/代理对象 | 解决AOP代理场景下的循环依赖,保证代理对象唯一性,避免原始对象与代理对象不一致 |
三、循环依赖解决全流程(以A依赖B、B依赖A为例)
以下是Spring创建单例Bean A、B并解决循环依赖的完整步骤,贴合源码执行逻辑,清晰体现三级缓存的流转过程:
步骤1:容器启动,开始创建Bean A
调用getSingleton(A)尝试从一级缓存获取A,此时A未创建,进入doCreateBean方法。先通过构造器实例化A(仅分配内存,属性未赋值、初始化未执行),得到A的原始半成品对象。
步骤2:提前暴露A的工厂对象到三级缓存
实例化完成后,Spring会将A的ObjectFactory工厂存入三级缓存singletonFactories,标记A已进入实例化阶段,允许其他Bean提前获取A的引用。此时A仅完成实例化,未进行属性填充和初始化。
步骤3:填充A的属性,触发Bean B的创建
执行属性填充逻辑,发现A依赖B,转而调用getSingleton(B)创建B。重复步骤1-2:实例化B,将B的工厂对象存入三级缓存,随后填充B的属性,发现B依赖A。
步骤4:B从三级缓存获取A的早期引用
B填充属性需要A,调用getSingleton(A):先查一级缓存无A,再查二级缓存无A,最后从三级缓存获取A的工厂对象,调用getObject()生成A的早期引用(普通场景为原始对象,AOP场景为代理对象)。
生成后,将A的早期引用移入二级缓存earlySingletonObjects,并删除三级缓存中A的工厂对象,避免重复生成。B成功获取A的早期引用,完成自身属性填充和初始化,最终存入一级缓存。
核心答疑:A、B互相依赖,为何B不进二级缓存?看似双向依赖,但依赖获取的时机完全不同,这是判断是否进二级缓存的唯一标准:✅ B获取A的时机:A仅完成实例化、还在属性填充阶段,属于未初始化的半成品,没有成品A可用,必须提取A的早期引用,因此A需要存入二级缓存过渡;❌ A获取B的时机:A回头找B时,B已经完成属性填充+初始化全流程,是直接存入一级缓存的成品Bean,A无需调用B的半成品引用,自然不需要把B放入二级缓存。简言之:二级缓存只救“自身没完工,就被别人强行索要引用”的Bean,B是顺利完工的成品,无需二级缓存兜底。
步骤5:A完成属性填充和初始化
B创建完成后,A顺利获取B的成品对象,完成自身属性填充、初始化方法执行等后续逻辑。随后将A从二级缓存移入一级缓存,删除三级缓存中残留数据,A最终成为成品Bean。
步骤6:闭环完成
此时A和B均存入一级缓存,相互持有对方的合法引用,循环依赖闭环成功打破,容器正常启动。
二级缓存准入铁律:谁先被“中途截胡”,谁才进二级缓存。循环依赖中,只有先创建、先被对方提前依赖的半成品Bean(如本例A)需要二级缓存;后创建、顺利完工的Bean(如本例B)直接晋级一级缓存,全程不接触二级。
四、关键设计:为什么必须用三级缓存?
很多开发者疑惑二级缓存能否解决循环依赖,答案是:普通无AOP的循环依赖用二级缓存即可,但AOP代理场景必须用三级缓存。
- 若只用二级缓存:AOP代理会在Bean初始化后生成,循环依赖时B获取的是A的原始对象,而容器最终存放的是A的代理对象,导致对象不一致,破坏单例原则
- 三级缓存的ObjectFactory实现延迟代理:只有发生循环依赖时,才通过工厂生成代理对象并放入二级缓存;无循环依赖时,代理对象在初始化后生成,保证逻辑一致性
五、Spring无法解决的循环依赖场景
以下场景即便开启缓存机制,Spring仍无法处理,会直接抛出BeanCurrentlyInCreationException异常:
- 构造器注入的循环依赖:实例化阶段就需要依赖对象,无法提前暴露引用,彻底无法解决
- 原型(prototype)作用域循环依赖:原型Bean无缓存,每次获取都会创建新实例,无法形成缓存闭环
- @DependsOn强制循环依赖:注解强制指定Bean创建顺序,形成死锁,容器直接校验失败
- 多线程并发创建单例Bean:并发场景下缓存读写冲突,可能导致早期引用异常
六、总结
Spring解决循环依赖的核心是三级缓存+提前暴露早期引用,仅适配单例Bean的setter/属性注入场景,通过分层缓存实现Bean生命周期的解耦,既保证了单例唯一性,又兼容AOP代理逻辑。日常开发中,应尽量避免构造器循环依赖、原型循环依赖等违规场景,减少容器启动风险。
ps:如果这篇帖子对于还在找工作和找实习的你有所帮助,可以关注我,给本贴点赞、评论、收藏并订阅专栏;同时不要吝啬您的花花
本专栏聚焦Spring全生态体系,从IoC/AOP核心原理入手,覆盖Spring Boot自动配置、事务管理、Web开发等实战内容。拆解循环依赖、动态代理等高频面试难点,助力开发者从入门到精通,打通单体到微服务的技术链路,解决企业级开发痛点,提升架构设计与问题排查能力,成为Java后端进阶的必备技术专栏。