十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

《Java 100 天进阶之路》第80篇:Spring循环依赖解决(2026版)

《Java 100 天进阶之路》第80篇:Spring循环依赖解决(2026版) 第80篇Spring循环依赖解决2026版系列导航《Java 100 天进阶之路》完整目录 |⬅️ 上一篇第79篇Spring源码阅读技巧 |➡️ 下一篇第81篇Spring事件机制待发布️ 本文阅读地图3 分钟速览第79篇搞定了Spring源码阅读方法论本篇深入Spring循环依赖解决。BeanCurrentlyInCreationException是Spring开发者最常遇到的启动异常之一——两个Bean互相依赖项目起不来。很多人背熟了“三级缓存”四个字但一问“为什么需要三级而不是二级”“构造器注入为什么不行”就卡壳了。搞懂循环依赖就是搞懂了Spring IoC容器最精妙的设计之一模块核心问题一句话回答循环依赖是什么两个Bean互相依赖会怎样A依赖B、B依赖A形成“先有鸡还是先有蛋”的死锁三级缓存是什么Spring靠什么解决循环依赖三个Map——一级存成品、二级存半成品、三级存工厂为什么能解决三级缓存的原理是什么提前暴露半成品——对象实例化后立即暴露引用不等初始化完成为什么需要三级二级缓存不够吗三级缓存专为AOP而生——代理对象需要在循环依赖时提前创建什么情况解决不了Spring能解决所有循环依赖吗构造器注入、原型Bean、多例模式——这三种不行Lazy怎么用不想改代码结构怎么办用Lazy延迟加载让Spring注入代理对象面试最爱问高频考点有哪些见文末 小节一、核心知识点1. 什么是循环依赖循环依赖是指两个或多个Bean之间互相持有对方的引用形成闭环依赖关系。最典型的场景是ComponentpublicclassA{AutowiredprivateBb;// A依赖B}ComponentpublicclassB{AutowiredprivateAa;// B依赖A形成循环}如果不做特殊处理Spring在创建A时需要注入B创建B时需要注入A陷入无限递归最终抛出BeanCurrentlyInCreationException异常。本质理解循环依赖就是**“先有鸡还是先有蛋”**的问题——A的创建依赖BB的创建依赖A谁都没法先完成。2. 循环依赖的产生时机循环依赖发生在Bean生命周期的属性填充populateBean阶段阶段说明① 实例化调用构造方法创建对象分配内存②属性填充注入依赖的其他Bean——循环依赖发生在此③ 初始化执行自定义初始化逻辑④ 完成放入一级缓存关键认知实例化和属性填充是两个独立的阶段——对象实例化后、属性填充前已经是一个“合法对象”有内存地址、可被引用只是属性还是空的。这是Spring能解决循环依赖的根本前提。二、通俗讲解1分钟开心学把循环依赖想象成“两个人互相等对方先开口”Bean A我要等B先说话我才说。Bean B我要等A先说话我才说。结果两个人就这么干等着谁都不开口——死锁。Spring的三级缓存就是“中间人”一级缓存singletonObjects已经聊完天、握过手的正式朋友成品Bean。二级缓存earlySingletonObjects刚见面、还没深聊的半熟人半成品Bean。三级缓存singletonFactories名片盒——里面放着每个人的联系方式ObjectFactory需要的时候随时打电话叫人。解决过程A来了先发一张名片放进三级缓存名片盒。A要认识B但B还没来于是去创建B。B来了也发一张名片放进三级缓存。B要认识A去三级缓存翻A的名片打电话把A叫来提前暴露。B认识了A半成品B完成创建变成正式朋友进入一级缓存。A继续完成创建也进入一级缓存。关键洞察提前暴露Early Exposure是核心——不等Bean完全初始化先把“半成品”拿出来用。三、三级缓存详解3.1 三级缓存的定义Spring在DefaultSingletonBeanRegistry类中定义了三个缓存缓存级别缓存名称存储内容状态一级缓存singletonObjects完全初始化完成的成品Bean可用状态二级缓存earlySingletonObjects提前暴露的半成品Bean已实例化未完成属性填充和初始化半成品三级缓存singletonFactoriesObjectFactory对象工厂——仅在调用getObject()时才会创建Bean实例工厂互斥规则三个缓存是互斥的同一个BeanName不会同时存在于多个缓存中。3.2 三级缓存的分工缓存职责关键特性一级缓存全局唯一对外提供可用的单例Bean用户最终获取的Bean均来自这里二级缓存缓存已通过三级工厂生成的早期对象避免重复创建提升性能防止重复调用ObjectFactory.getObject()三级缓存存放ObjectFactory封装getEarlyBeanReference早期代理创建逻辑无循环依赖时工厂永久不执行仅发生循环依赖时才调用getObject()四、三级缓存解决循环依赖的完整流程以A依赖BB依赖A的经典场景为例4.1 关键步骤详解步骤操作缓存变化①实例化A无②A的工厂放入三级缓存三级缓存A的ObjectFactory③A填充属性发现依赖B触发B的创建④实例化B无⑤B的工厂放入三级缓存三级缓存A的工厂 B的工厂⑥B填充属性发现依赖A从三级缓存取A的工厂→生成A的早期对象⑦A的早期对象放入二级缓存二级缓存A半成品⑧B拿到A完成创建一级缓存B成品⑨A拿到B完成创建一级缓存A成品 B成品五、核心源码解析5.1 getSingleton() —— 三级缓存的核心入口DefaultSingletonBeanRegistry.getSingleton()是解决循环依赖的核心方法NullablepublicObjectgetSingleton(StringbeanName,booleanallowEarlyReference){// 第一步从一级缓存获取成品BeanObjectsingletonObjectthis.singletonObjects.get(beanName);// 如果一级缓存没有且当前Bean正在创建中循环依赖的核心判断条件if(singletonObjectnullisSingletonCurrentlyInCreation(beanName)){// 第二步从二级缓存获取提前暴露的半成品BeansingletonObjectthis.earlySingletonObjects.get(beanName);// 如果二级缓存也没有且允许提前引用if(singletonObjectnullallowEarlyReference){synchronized(this.singletonObjects){// 双重检查singletonObjectthis.singletonObjects.get(beanName);if(singletonObjectnull){singletonObjectthis.earlySingletonObjects.get(beanName);if(singletonObjectnull){// 第三步从三级缓存获取ObjectFactoryObjectFactory?singletonFactorythis.singletonFactories.get(beanName);if(singletonFactory!null){// 调用工厂创建早期对象singletonObjectsingletonFactory.getObject();// 升级到二级缓存this.earlySingletonObjects.put(beanName,singletonObject);// 从三级缓存移除this.singletonFactories.remove(beanName);}}}}}}returnsingletonObject;}核心逻辑一级→二级→三级逐级查找。三级缓存找到了就“升级”到二级缓存避免重复创建。5.2 为什么需要三级缓存AOP场景很多人问二级缓存不就能解决循环依赖了吗为什么需要三级关键在AOP代理。如果没有三级缓存只有二级在循环依赖发生时直接暴露原始对象到二级缓存——但如果这个Bean需要AOP代理比如有Transactional代理对象必须在初始化后才创建。三级缓存的精妙设计场景处理方式无循环依赖代理在postProcessAfterInitialization中统一创建有循环依赖代理提前通过三级缓存工厂创建后置处理器不再重复生成三级缓存存的不是Bean实例而是**ObjectFactory**——一个函数式接口只有在getObject()被调用时才会真正创建对象。getEarlyBeanReference()方法会检查是否需要AOP代理如果需要就返回代理对象否则返回原始对象。一句话总结三级缓存的存在主要是为了解决AOP代理在循环依赖场景下的提前创建问题。六、什么情况下Spring无法解决循环依赖6.1 三种无法解决的场景场景原因解决方案构造器注入构造器在实例化阶段执行此时对象尚未创建无法提前暴露引用改用字段/Setter注入或使用Lazy原型PrototypeBean每次获取都新建不进入缓存改用单例或手动管理多例模式下的Setter注入每次创建新实例无限递归导致OOM避免在多例Bean之间循环依赖6.2 构造器注入为什么不行构造器注入的本质问题在于JVM对象创建的两个阶段不可颠倒阶段说明阶段一内存分配JVM在堆内存中开辟空间所有属性赋默认值null——半成品阶段二初始化赋值执行构造方法、成员变量赋值——成品构造器注入将两个阶段原子绑定——必须在构造期间完成依赖注入此时对象还没有分配内存地址无法被外部引用。而Setter/字段注入允许先完成实例化阶段一暴露半成品引用再完成属性填充阶段二。记忆口诀构造器注入“边盖房子边装修”房子没盖好别人进不来Setter注入“先盖好毛坯再装修”毛坯房就能让人进去等了。七、实战解决方案7.1 方案一改用字段/Setter注入推荐问题代码ComponentpublicclassA{privatefinalBb;// ❌ 构造器注入publicA(Bb){this.bb;}}ComponentpublicclassB{privatefinalAa;// ❌ 构造器注入publicB(Aa){this.aa;}}解决方案ComponentpublicclassA{AutowiredprivateBb;// ✅ 字段注入}ComponentpublicclassB{AutowiredprivateAa;// ✅ 字段注入}7.2 方案二使用Lazy延迟加载如果出于不可变性的考虑必须使用构造器注入可以用Lazy打破循环ComponentpublicclassA{privatefinalBb;publicA(LazyBb){// ✅ B被延迟加载this.bb;}}ComponentpublicclassB{privatefinalAa;publicB(Aa){this.aa;}}原理Lazy会让Spring注入一个代理对象只有在第一次使用时才真正创建目标Bean。7.3 方案三重新设计最彻底如果循环依赖频繁出现说明模块边界可能不清晰。考虑提取公共依赖到第三个类使用回调接口或事件发布解耦重新审视职责划分八、避坑要点错误/误区后果正确做法以为Lazy能解决所有循环依赖代理对象使用时可能NPELazy只适用于“非必须”的依赖在原型Bean之间循环依赖启动后无限递归导致OOM避免在多例Bean之间互相引用以为Spring能解决所有循环依赖构造器注入启动失败构造器注入遇到循环依赖时用Lazy在PostConstruct中调用依赖的Bean可能拿到未初始化的半成品在PostConstruct中谨慎操作九、面试高频考点Q1Spring如何解决循环依赖Spring通过三级缓存机制解决单例Bean的循环依赖。三级缓存分别是一级缓存singletonObjects存放成品Bean、二级缓存earlySingletonObjects存放提前暴露的半成品Bean、三级缓存singletonFactories存放ObjectFactory对象工厂。核心原理是提前暴露——Bean实例化后立即将ObjectFactory放入三级缓存当发生循环依赖时其他Bean可以从三级缓存获取ObjectFactory并调用getObject()得到早期引用从而打破循环。Q2为什么需要三级缓存二级缓存不够吗二级缓存只能解决普通对象的循环依赖。但如果Bean需要AOP代理如Transactional代理对象在初始化后才创建。三级缓存存的ObjectFactory可以在循环依赖发生时提前决定是返回原始对象还是代理对象。如果没有三级缓存AOP代理在循环依赖场景下无法提前创建。Q3为什么构造器注入的循环依赖无法解决构造器注入在实例化阶段就必须完成依赖注入而此时对象还没有分配内存地址无法被外部引用。Setter/字段注入允许先实例化分配内存、暴露引用再填充属性。简单说构造器注入“边盖房子边装修”房子没盖好别人进不来Setter注入“先盖好毛坯再装修”毛坯房就能让人进去等了。Q4Spring能解决所有类型的循环依赖吗不能。Spring的三级缓存只能解决单例Bean Setter/字段注入的循环依赖。以下场景无法解决①构造器注入②原型PrototypeBean③多例模式下的Setter注入。Q5Lazy如何解决循环依赖Lazy让Spring在注入依赖时不立即创建目标Bean而是注入一个代理对象。当第一次使用该依赖时代理对象才会触发真正的Bean创建。这样打破了“互相等待对方先创建”的死锁。 面试官追问陷阱加分题追问1“三级缓存中存的是ObjectFactory那它什么时候会执行getObject()”只有发生循环依赖时才会执行。如果Bean没有发生循环依赖三级缓存中的ObjectFactory永远不会被调用Bean会在正常流程中完成初始化后直接放入一级缓存。当其他Bean在创建过程中发现依赖当前Bean且当前Bean还在创建中isSingletonCurrentlyInCreation返回true才会从三级缓存获取ObjectFactory并执行getObject()。追问2“Spring默认是开启循环依赖支持的那能不能关闭关闭了会怎样”能。Spring提供了一个开关allowCircularReferences默认为true。可以通过设置AbstractRefreshableApplicationContext.setAllowCircularReferences(false)关闭。关闭后任何循环依赖都会直接抛出BeanCurrentlyInCreationException。生产环境不建议关闭——除非你100%确定项目中没有循环依赖。但大多数Spring Boot项目都有循环依赖尤其是事务和AOP相关关闭会导致启动失败。追问3“实际开发中是应该让Spring解决循环依赖还是主动避免”应该主动避免。三级缓存只是“救火”机制不是“设计范式”。循环依赖本质上说明模块边界不清晰、职责划分不合理——A和B互相依赖说明它们应该被合并或提取公共部分。主动避免的方式①重新设计提取公共接口②使用Lazy延迟加载非核心依赖③通过事件发布解耦。把循环依赖当成代码坏味道而不是让Spring帮你兜底。十、练习题简答题Spring的三级缓存分别是什么各自的作用是什么代码题写一个构造器注入导致循环依赖的示例然后用Lazy解决它。分析题某项目启动时报BeanCurrentlyInCreationException日志显示AService和BService互相依赖。两个Bean都使用了构造器注入。请给出两种解决方案并说明各自的优缺点。 你的学习进度当前第80篇 / 共108篇 ·进阶篇Spring全家桶第73~82篇✅ 已完成基础篇44篇 第45~80篇 正在学第80篇⏳ 待学习第81~108篇 完整目录 学习指南 | 订阅本专栏不错过每一篇 下一篇文章预告下一篇《第81篇Spring事件机制》内容简介Spring事件驱动模型——ApplicationEvent、ApplicationListener、EventListener注解、异步事件、事务绑定事件。Spring全家桶专题持续推进拿下事件机制《Java 100 天进阶之路 | 从入门到上岗就业》每天一篇建议收藏 关注一起100天拿offer 点击关注我更新后第一时间收到推送
返回列表