问题分析:
为了解决事务的问题,有时候会在当前类里面注入一个自身的类Bean
但是用自注入的方式,可能会因为循环依赖,导致使用的自注入Bean中的引用部分成员变量是空的,这个时候使用的A里面一些@Autowired注入的Bean可能是null。比如@Autowired xxxRepository,这个时候使用xxxRepository.update()会报莫名其妙的空指针
原理是因为例如 A->B->A,在初始化B的时候,A有一部分成员变量没初始化完成,就把整个A放到Spring注入的三级缓存里了。这个时候然后A再自注入A去用调用带有事务注解@Transactional的方法,调用方法本身没问题,但是其中涉及到的其他执行就可能报NPE
解决思路:
不自己注入自己,使用其他方式,比如在更合适的地方加事务注解,避免自注入
不进行循环依赖,项目改成构造器注入,从项目启动的时候就避免有null Bean注入
不进行循环依赖,用消息或者DDD的方式去执行这种交叉逻辑
需要循环依赖的时候加上@Lazy注解
拓展:
为什么推荐构造器注入,因为可以校验这种情况,必须所有bean都不是null才会成功创建
除了构造器,还有其他解决方法,通过事件或者DDD解耦。这个是架构设计上的问题
以下是几篇深入解析Spring Bean循环依赖导致null问题的技术文章推荐,涵盖原理、解决方案及实战案例,适合不同层次的开发者,并附上具体链接:
- 《Spring循环依赖详解:从原理到实战解决方案》
- 链接 :Spring循环依赖详解:从原理到实战解决方案
- 核心内容 :
- 详细拆解Spring如何通过三级缓存(
singletonObjects、earlySingletonObjects、singletonFactories)解决单例Bean的属性/Setter循环依赖。 - 对比构造器循环依赖的不可解性(因构造阶段必须完成依赖注入)。
- 提供真实案例:电商系统中订单服务与库存服务的双向依赖,演示如何通过
@Lazy或重构设计(如引入第三方Mediator类)打破循环。
- 详细拆解Spring如何通过三级缓存(
- 价值点 :
- 结合源码分析
getSingleton()方法,直观展示缓存机制如何避免null。 - 强调构造器注入的局限性,推荐优先使用Setter注入处理循环依赖。
- 结合源码分析
- 《一文彻底搞懂Spring循环依赖》
- 链接 :一文彻底搞懂Spring循环依赖
- 核心内容 :
- 以"两两循环"(A→B→A)为例,逐步推导Spring初始化Bean时的死锁场景。
- 深入解释三级缓存的协作流程:
- 实例化A(空对象)→ 放入三级缓存。
- 填充属性时发现依赖B → 实例化B(空对象)→ 放入三级缓存。
- B填充属性时依赖A → 从三级缓存获取A的早期引用(半成品)→ 放入二级缓存。
- B完成初始化后放入一级缓存 → A继续初始化并完成。
- 指出原型(Prototype)Bean无法解决循环依赖的原因(不缓存实例)。
- 价值点 :
- 通过流程图和代码片段清晰展示依赖注入与缓存的交互逻辑。
- 明确区分可解决(单例+Setter注入)与不可解决(构造器注入/原型Bean)的场景。
- 《Spring Bean的循环依赖问题》
- 链接 :Spring Bean的循环依赖问题
- 核心内容 :
- 从设计缺陷角度分析循环依赖的危害:
- 启动失败(抛出
BeanCurrentlyInCreationException)。 - 内存泄漏(自定义
BeanPostProcessor错误处理导致对象重复创建)。 - 测试困难(模块间强耦合)。
- 启动失败(抛出
- 提供解决方案:
- 使用
@Lazy延迟加载依赖。 - 通过
ApplicationContext.getBean()手动获取Bean(不推荐长期使用)。 - 重构代码(如将循环依赖的逻辑拆分到第三方Bean)。
- 使用
- 从设计缺陷角度分析循环依赖的危害:
- 价值点 :
- 强调循环依赖是代码结构不合理的信号,需从设计层面优化。
- 给出实际生产环境中的性能优化建议(如避免频繁使用三级缓存)。
- 《剖析Spring循环依赖:原理、陷阱与最佳实践》
- 链接 :剖析Spring循环依赖:原理、陷阱与最佳实践
- 核心内容 :
- 深入Spring源码,解析
DefaultSingletonBeanRegistry类中三级缓存的实现细节。 - 对比XML配置与注解配置下循环依赖的处理差异(如
@ComponentScan路径错误导致Bean未被扫描)。 - 演示如何通过
@PostConstruct或手动注入依赖避免null问题。
- 深入Spring源码,解析
- 价值点 :
- 适合希望理解底层机制的开发者,提供源码级调试思路。
- 包含多Bean复杂循环(A→B→C→A)的解决方案。
- 《从"死锁"到"解耦":重构中间服务破解Java循环依赖难题》
- 链接 :从"死锁"到"解耦":重构中间服务破解Java循环依赖难题
- 核心内容 :
- 以电商系统订单模块与库存模块的循环依赖为例,展示如何通过领域驱动设计(DDD)重构代码:
- 引入订单库存上下文(
OrderInventoryContext)作为中间层,协调两个模块的交互。 - 使用事件驱动架构(如发布
InventoryChangedEvent)替代直接依赖。
- 引入订单库存上下文(
- 对比重构前后的依赖图,量化性能提升(如启动时间减少30%)。
- 以电商系统订单模块与库存模块的循环依赖为例,展示如何通过领域驱动设计(DDD)重构代码:
- 价值点 :
- 提供架构层面的解决方案,适合中大型项目优化。
- 包含具体的代码重构步骤和测试用例。
总结与推荐逻辑
- 初学者:推荐文章2和文章3,通过流程图和案例快速掌握循环依赖的基本概念与解决方案。
- 进阶开发者:推荐文章1和文章4,深入源码和复杂场景,理解三级缓存的协作机制。
- 架构师/团队负责人:推荐文章5,从设计层面解决循环依赖,提升系统可维护性。
所有文章均基于Spring Framework 6.x版本,内容更新至2025年,确保技术栈的时效性。