SpringBoot循环依赖:原理、配置与重构策略详解

发布时间:2026/9/15 7:33:43

SpringBoot循环依赖:原理、配置与重构策略详解 1. 项目缘起一个看似简单却暗藏玄机的配置最近在做一个SpringBoot项目为了快速实现一个功能我在两个Service类里互相注入了对方。本地启动时一切正常但项目一打包部署到测试环境启动日志里就赫然出现了一行刺眼的警告“There is a circular dependency between beans”。虽然应用最终还是跑起来了但这个警告像一根刺时刻提醒着我架构设计上存在瑕疵。更让我警惕的是在一些对启动性能和内存有严格要求的场景下这种循环依赖可能会导致不可预知的问题比如Bean创建顺序混乱、甚至启动失败。于是我开始研究如何在SpringBoot中“允许”循环依赖。注意这里的“允许”是带引号的。Spring框架本身从设计上就不鼓励循环依赖因为它破坏了依赖注入DI所倡导的单向依赖原则是代码“坏味道”的一种体现。SpringBoot作为Spring的“脚手架”其默认配置是严格禁止循环依赖的目的就是为了在项目初期就暴露这种设计问题。所以我们首先要明确一个核心观点开启循环依赖支持本质上是一种妥协和临时方案绝非最佳实践。它的正确使用场景应该是在重构遗留代码、进行短期技术债务处理或者在某些特定框架集成如某些旧版第三方SDK强制要求时作为一个过渡手段。那么SpringBoot是如何控制这个开关的呢答案就在其自动配置的核心机制里。当我们使用SpringBootApplication注解启动应用时它会创建一个AnnotationConfigApplicationContext。在这个上下文的创建过程中会调用AbstractAutowireCapableBeanFactory的setAllowCircularReferences方法。而SpringBoot通过SpringApplication的运行时环境默认为我们设置了这个值为false。我们的任务就是找到并修改这个开关。2. 循环依赖的三种类型与Spring的解决机制在动手修改配置之前我们必须理解我们面对的是什么。循环依赖并非只有一种形式Spring能处理的其实只是其中一种特定情况。理解这一点能避免我们错误地认为开启了开关就万事大吉。2.1 构造器循环依赖Constructor Circular Dependency这是最严重、Spring完全无法解决的一种类型。假设有两个类A和BComponent public class A { private final B b; public A(B b) { // A的构造器需要B this.b b; } } Component public class B { private final A a; public B(A a) { // B的构造器需要A this.a a; } }这种情况下Spring IoC容器在启动时就会直接抛出BeanCurrentlyInCreationException。原因很简单要创建A必须先有B的实例但要创建B又必须先有A的实例。这是一个死锁即使将allowCircularReferences设置为true也无济于事。构造器注入的循环依赖是无解的必须通过代码重构来打破循环。2.2 属性Setter/字段循环依赖这是我们最常见也是Spring在开启支持后能够自动处理的情况。它指的是通过Autowired注解在字段上或者通过setter方法进行注入。Component public class ServiceA { Autowired private ServiceB serviceB; // 字段注入 } Component public class ServiceB { Autowired private ServiceA serviceA; // 字段注入 }Spring解决这种依赖的机制非常巧妙它采用了一种“提前暴露”的解决方案。我们以单例Singleton作用域的Bean为例看看Spring的Bean创建流程实例化InstantiateSpring首先调用构造器创建ServiceA的原始对象。此时对象内的serviceB字段还是null。注意这个对象虽然还未填充属性即“半成品”但已经被创建出来了。提前暴露引用Expose Early ReferenceSpring将这个“半成品”的ServiceA对象引用放入一个叫做“早期暴露对象缓存”earlySingletonObjects的Map中。此时其他Bean就可以引用到这个ServiceA对象了尽管它还不完整。属性填充Populate PropertiesSpring开始为ServiceA注入属性。它发现需要注入ServiceB于是去容器中查找ServiceB。创建ServiceB容器开始创建ServiceB同样经过实例化、提前暴露引用。为ServiceB注入属性当Spring要为ServiceB注入ServiceA时它不会再去从头创建ServiceA而是直接从“早期暴露对象缓存”中拿到那个ServiceA的半成品引用注入给ServiceB。至此ServiceB创建完成。完成ServiceA的创建ServiceB创建完成后被注入到ServiceA中ServiceA的属性填充完成成为一个完整的Bean。这个过程的关键在于**“提前暴露引用”** 和“三级缓存”机制。三级缓存是Spring解决循环依赖的核心数据结构一级缓存singletonObjects存放完全初始化好的单例Bean。二级缓存earlySingletonObjects存放提前暴露的、尚未完成属性填充的“半成品”Bean。三级缓存singletonFactories存放创建Bean的工厂对象ObjectFactory用于在需要时生成提前暴露的引用。当allowCircularReferences设置为false时Spring不会将Bean工厂放入三级缓存也就无法提供早期引用因此在遇到属性循环依赖时会直接报错。2.3 原型Prototype作用域的循环依赖对于作用域为prototype的BeanSpring是完全不支持循环依赖的无论是否是属性注入。因为原型Bean每次请求都会创建一个新的实例Spring无法像处理单例Bean那样通过缓存一个“半成品”引用来解决循环问题。如果存在原型Bean的循环依赖Spring会直接抛出BeanCurrentlyInCreationException。重要提示即使你通过配置开启了循环依赖支持它也只能解决单例作用域下的属性/Setter注入循环依赖。对于构造器注入和原型Bean的循环依赖配置开关是无效的。3. 配置允许循环依赖的四种方法及其原理理解了Spring的处理机制我们就可以来看如何打开这个开关了。方法有多种适用于不同的场景和SpringBoot版本。3.1 方法一在application.properties或application.yml中配置推荐这是最直接、最“SpringBoot”的方式。对于.properties文件spring.main.allow-circular-referencestrue对于.yml文件spring: main: allow-circular-references: true原理剖析这个配置项是在SpringBoot 2.6.0版本中引入的。当你设置这个属性时SpringBoot的SpringApplication在运行时会读取它并在创建ApplicationContext之前通过SpringApplication#setAllowCircularReferences方法将标志位传递到底层的AbstractAutowireCapableBeanFactory。这是最干净、声明式的配置方式与SpringBoot的配置哲学一致。3.2 方法二通过SpringApplicationAPI编程式设置如果你需要在代码中动态控制或者你的启动类有特殊的初始化逻辑可以使用这种方法。SpringBootApplication public class MyApplication { public static void main(String[] args) { SpringApplication app new SpringApplication(MyApplication.class); // 关键设置允许循环引用 app.setAllowCircularReferences(true); app.run(args); } }原理与适用场景这种方式在SpringApplication实例化后、上下文刷新前设置标志位。它比配置文件更早生效适合需要根据某些条件如环境变量、外部配置中心的值来决定是否开启循环依赖的场景。但通常来说静态配置更易于维护。3.3 方法三自定义BeanFactoryPostProcessor较底层慎用这是一种更底层、更灵活但也更复杂的方式。你可以通过实现BeanFactoryPostProcessor接口在Bean工厂标准初始化之后、任何Bean实例化之前来修改工厂的内部配置。Component public class CircularDependencyBeanFactoryPostProcessor implements BeanFactoryPostProcessor { Override public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { if (beanFactory instanceof DefaultListableBeanFactory) { DefaultListableBeanFactory listableBeanFactory (DefaultListableBeanFactory) beanFactory; // 直接设置底层Bean工厂的允许循环引用属性 listableBeanFactory.setAllowCircularReferences(true); } } }原理与风险BeanFactoryPostProcessor是Spring容器扩展机制的一部分它允许你读取和修改Bean的定义BeanDefinition甚至直接操作BeanFactory。这里我们直接获取到DefaultListableBeanFactory并设置其属性。这种方法风险较高因为它干预了容器的核心初始化流程如果使用不当例如在错误的时机修改了其他属性可能会导致难以排查的启动问题。除非你有非常特殊的定制化需求例如只对某个特定的Bean定义开启循环依赖否则不建议使用。3.4 方法四升级或降级SpringBoot版本历史版本兼容性在SpringBoot 2.6.0之前并没有spring.main.allow-circular-references这个配置项。在2.6.0版本中Spring团队为了推动更好的代码实践默认将此值改为了false。这就是为什么很多项目在升级到2.6.x版本后突然开始报循环依赖警告的原因。SpringBoot 2.6.0之前默认是允许循环依赖的true。SpringBoot 2.6.0及之后默认禁止循环依赖false。因此如果你的项目是一个遗留项目暂时无法处理大量的循环依赖一个临时的应对方案是将SpringBoot版本降级到2.5.x或更早。但这绝对是一个下策因为你会因此错过新版本的性能提升、安全补丁和新特性。正确的做法是将其视为一个升级后的必改项逐步重构代码消除循环依赖然后再将配置显式设为false或升级版本。4. 开启循环依赖后的副作用与长期重构策略打开了这个开关警告消失了世界清净了。但这只是把问题从台前转移到了幕后。我们必须清醒地认识到允许循环依赖带来的潜在风险。4.1 可能引发的副作用启动顺序的不确定性增强虽然Spring解决了依赖注入但Bean的PostConstruct初始化方法、InitializingBean接口的afterPropertiesSet方法其执行顺序在循环依赖存在时会变得难以预测。如果A和B的初始化方法互相依赖对方的某些状态可能会导致初始化失败或状态不一致。内存泄漏风险由于Bean之间持有彼此的强引用在复杂的循环依赖链中可能会影响垃圾回收GC。特别是当你想替换或重新加载某个Bean时可能会因为引用无法释放而导致内存泄漏或类加载器泄漏。单元测试困难循环依赖使得Bean之间的耦合度极高难以进行隔离测试。你无法轻松地模拟Mock其中一个Bean来测试另一个因为它们紧密地绑在一起。代码理解与维护成本剧增循环依赖模糊了模块之间的边界和职责。新接手项目的开发者很难理清数据流和控制流任何修改都可能产生意想不到的连锁反应使得代码变得脆弱。4.2 如何识别和重构循环依赖允许循环依赖应该是暂时的。长期目标必须是重构代码消除它们。以下是一些实用的重构策略按推荐程度排序策略一使用Setter/方法注入替代字段注入治标不治本虽然Setter注入也能被Spring处理但它只是将问题从字段层面转移到了方法层面并没有降低耦合度。它更多是作为一种重构的中间步骤为引入接口做准备。策略二提取公共逻辑到第三个类引入中介者这是最常用且有效的方案。如果A和B互相调用是因为它们有共同的业务逻辑那么就把这部分逻辑抽离出来形成一个独立的ServiceC。让A和B都依赖于C从而将A-B的循环依赖拆解成A-C和B-C的两个单向依赖。// 重构前 Service public class OrderService { Autowired private UserService userService; public void processOrder(Long userId) { User user userService.getUser(userId); // ... 订单逻辑其中调用了userService的某些方法 userService.updateUserOrderStats(user); // 反过来调用UserService } } Service public class UserService { Autowired private OrderService orderService; public void updateUserOrderStats(User user) { // ... 用户统计逻辑其中需要查询订单信息 ListOrder orders orderService.getOrdersByUser(user.getId()); // 反过来调用OrderService } } // 重构后提取统计逻辑到新服务 Service public class OrderService { Autowired private UserService userService; Autowired private StatisticsService statsService; // 引入新服务 public void processOrder(Long userId) { User user userService.getUser(userId); // ... 订单逻辑 statsService.updateOrderStatsForUser(user); // 调用中介服务 } } Service public class UserService { Autowired private StatisticsService statsService; // 引入新服务 // ... 移除了对OrderService的依赖 } Service public class StatisticsService { // 专门负责订单和用户的统计逻辑 public void updateOrderStatsForUser(User user) { // 这里可以整合原先分散在两个Service中的统计逻辑 } }策略三使用事件驱动Event-Driven解耦如果A和B的互相调用并非即时必需而是“做完某事后通知对方”那么可以使用Spring的事件机制。A发布一个事件B监听这个事件并做出响应。这样A完全不需要知道B的存在。// A发布事件 Service public class ServiceA { Autowired private ApplicationEventPublisher publisher; public void doSomething() { // ... 业务逻辑 publisher.publishEvent(new SomethingDoneEvent(this, data)); } } // B监听事件 Service public class ServiceB { EventListener public void handleSomethingDone(SomethingDoneEvent event) { // 处理事件执行相应逻辑 } }策略四使用Lazy注解进行延迟注入临时解决方案在注入点添加Lazy注解告诉Spring延迟初始化被注入的Bean或者至少延迟注入代理。这可以解决一部分因启动顺序导致的循环依赖问题但并没有消除依赖关系本身只是把问题推迟到了第一次使用时。Service public class ServiceA { Autowired Lazy // 延迟注入ServiceB private ServiceB serviceB; }使用Lazy的注意事项它创建的是一个代理对象。如果ServiceB不是接口Spring会使用CGLIB创建子类代理。这可能会影响final方法、private方法并且在某些调试场景下会增加复杂性。策略五面向接口编程结合Primary或Qualifier如果循环依赖是因为依赖于具体实现可以考虑提取接口。让ServiceA依赖于IServiceB接口而ServiceB实现这个接口。同时ServiceB也可以依赖于IServiceA接口。这样虽然依赖关系还在但通过接口进行了解耦为未来替换实现或引入动态代理如AOP提供了可能。在存在多个实现时配合Primary指定首选Bean或Qualifier按名称注入来明确注入目标。5. 实战排查当允许循环依赖后依然报错怎么办即使你配置了spring.main.allow-circular-referencestrue有时依然会遇到启动错误。这时候就需要进行系统性的排查。5.1 排查步骤流程图文字描述确认错误类型首先看异常栈信息。是BeanCurrentlyInCreationException还是其他错误检查Bean的作用域如果错误信息里涉及到的Bean是prototype那么立即可以确定循环依赖不支持原型Bean必须重构代码。检查注入方式查看报错Bean的依赖注入方式。如果是构造器注入即类只有一个构造器并且参数需要被注入那么循环依赖是无解的必须改为属性/Setter注入或重构。验证配置是否生效在应用启动的早期添加一个调试断点或打印日志检查AbstractAutowireCapableBeanFactory的allowCircularReferences属性是否为true。有时配置可能因为优先级问题被覆盖。检查是否存在间接循环依赖循环依赖可能不是简单的A-B-A而是更长的链比如A-B-C-A。Spring处理长链循环依赖的能力是有限的尤其是在结合了Async、Transactional等AOP代理时可能会因为代理创建顺序问题而失败。检查是否有BeanPostProcessor介入一些自定义的或第三方库的BeanPostProcessor可能会在Bean创建过程中进行额外处理如果它们对Bean的创建顺序有严格要求可能会与循环依赖解决机制冲突。使用Spring Boot Actuator的/beans端点如果应用能部分启动可以通过Actuator查看所有Bean的依赖关系图直观地定位循环链。5.2 常见疑难场景分析场景一结合Async或Transactional当一个Bean被Async或Transactional注解时Spring会为其创建AOP代理。代理的创建时机可能与原始Bean的创建时机不同这可能会干扰到基于“早期暴露引用”的循环依赖解决机制。一个典型的错误是Bean with name ‘xxx‘ has been injected into other beans […] in its raw version as part of a circular reference。解决方案尝试对循环依赖链上的Bean都使用接口并确保Async或Transactional注解在接口方法上。或者将异步或事务操作抽取到另一个独立的Bean中打破循环链。场景二多模块项目中的配置类循环依赖如果使用Configuration类并且这些配置类之间通过Bean方法互相引用也可能形成循环依赖。虽然配置类本身是单例但Bean方法的调用顺序在循环依赖下可能出错。解决方案将相关的Bean定义合并到同一个配置类中或者使用DependsOn注解显式指定Bean的创建顺序但这是一种脆弱的解决方案。场景三使用ObjectProvider进行延迟查找推荐模式这是Spring官方推荐的一种避免循环依赖和降低耦合度的方式。ObjectProvider允许你延迟获取Bean它本身不参与循环依赖的解决而是绕过了问题。Service public class ServiceA { // 不直接注入ServiceB而是注入ObjectProvider Autowired private ObjectProviderServiceB serviceBProvider; public void doWork() { // 在需要的时候才获取Bean ServiceB serviceB serviceBProvider.getIfAvailable(); if (serviceB ! null) { serviceB.someMethod(); } } }使用ObjectProviderServiceA在启动时就不再强依赖ServiceB的完整实例从而打破了循环。这是一种比Lazy更灵活、意图更明确的方式。允许循环依赖是一个便捷的逃生舱口但绝不是舒适的长期居所。每一次打开这个开关都应该伴随着一个明确的TODO注释和重构计划。通过理解其原理、掌握配置方法、并积极运用重构策略我们才能逐步构建出更清晰、更健壮、更易于维护的SpringBoot应用架构。最理想的状态是你的应用在spring.main.allow-circular-referencesfalse的严格模式下依然能够顺利启动。
延伸阅读

更多相关文章

2026/9/15 7:33:43

快稳铷原子钟突破铷钟启动时长痛点,国产铷原子钟,特种铷原子钟

在数字化浪潮席卷全球的今天,时频同步已成为支撑通信、电力、国防、科研等关键领域稳定运行的核心基石。从6G基站的纳秒级协同,到智能电网的故障精准定位,再到北斗导航的车道级精度保障,每一个场景都对时间频率的准确度、稳定度提…

2026/9/11 1:27:17

液压挖掘机工作装置设计:从四连杆机构原理到UG三维建模实践

记得大三那年,我第一次在工程机械实训基地见到真正的液压挖掘机。看着它流畅地完成挖掘、抬臂、回转、卸料整套动作,我被那种力量与精度的结合深深震撼——但更让我好奇的是,那个看似笨重的钢铁巨兽,究竟是如何通过液压油缸和连杆…

2026/9/12 15:31:46

Selenium iframe切换与WebDriver上下文管理实战指南

1. 项目概述&#xff1a;当你的Selenium脚本在iframe里“迷路”了如果你用Selenium做过网页自动化&#xff0c;特别是处理过那些带登录弹窗、富文本编辑器或者第三方支付页面的网站&#xff0c;那你肯定对<iframe>这个HTML元素又爱又恨。爱的是它能让复杂功能模块化&…

2026/9/15 7:31:39

用Obsidian搭建LLM知识库:双链+原子笔记+MOC实操指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/15 7:31:39

Vue3+Echarts新能源大屏实战:动态数据流与地理可视化

简介&#xff1a;本资源是一套基于 Vue.js 与 ECharts 深度集成的新能源业务数据可视化大屏源码范例&#xff0c;面向前端开发者、数据可视化工程师及中高级 Vue 实践者&#xff0c;解决新能源场景下电站发电量、能源消耗、设备状态等多维指标实时展示与交互分析难题。压缩包共…

2026/9/15 7:31:39

基于SnowNLP与LSTM的新闻情感分析系统实践

1. 项目概述&#xff1a;基于SnowNLP的新闻情感分析系统这个Python项目实现了一个端到端的新闻情感分析预测系统&#xff0c;核心采用SnowNLP库进行中文文本情感值计算&#xff0c;结合深度学习技术提升分析准确率。我在金融舆情监控场景中实际应用过类似方案&#xff0c;相比传…

2026/9/15 7:31:39

Hibernate 核心原理与架构详解

Hibernate 核心原理与架构详解 定位&#xff1a;Hibernate 架构分层、启动引导、持久化流程、代理原理、事务连接与类型系统 适用版本&#xff1a;Hibernate ORM 6.x&#xff08;Jakarta Persistence 3.1&#xff09; 目录 整体架构启动引导持久化流程代理生成原理连接与事务集…

2026/9/15 7:31:39

微信服务号网页扫码登录接入全流程:个体户资质与OAuth2闭环

微信服务号网页扫码登录接入全流程&#xff1a;个体户资质与OAuth2闭环对于面向国内用户的独立开发者而言&#xff0c;微信生态是转化率最高、用户摩擦力最小的账号与支付底座。 然而&#xff0c;在对接微信生态的过程中&#xff0c;90% 的独立开发者会在资质与账号体系上陷入迷…

2026/9/15 7:26:39

豆包 LeetCode 78. 子集 Rust实现

LeetCode 78. 子集 Rust实现 数组元素互不相同&#xff0c;返回全部幂集子集。提供回溯DFS、迭代增量、位运算三种写法 回溯 DFS&#xff08;推荐&#xff0c;rust标准题解&#xff09; rust impl Solution { pub fn subsets(nums: Vec) -> Vec<Vec> { let mut res V…

2026/9/15 4:54:30

拯救者Y7000黑屏故障排查与维修实战指南

1. 项目概述&#xff1a;一台黑屏的拯救者Y7000&#xff0c;到底卡在哪一步&#xff1f; 联想拯救者Y7000系列笔记本&#xff0c;从2018年第一代搭载i5-8300H开始&#xff0c;到后来的i7-9750H、i7-10750H、i5-11400H&#xff0c;再到2023年款的R7-7840HS&#xff0c;它始终是学…

2026/9/15 0:01:16

AI英语单词APP开发:自适应学习算法与移动端优化实践

1. 项目概述 作为一名在移动应用开发领域摸爬滚打多年的老手&#xff0c;我最近完成了一个AI英语单词APP的开发项目。这个项目将传统单词记忆方法与现代AI技术相结合&#xff0c;打造了一款能够智能适应不同用户学习习惯的英语学习工具。 市面上大多数单词APP都存在一个通病&a…

2026/9/15 0:01:16

Flutter与OpenHarmony结合开发手语学习APP实战

1. 项目背景与核心价值作为一名同时接触过Flutter和OpenHarmony的开发者&#xff0c;最近我完成了一个基于Flutter for OpenHarmony的手语学习APP实战项目。这个项目最大的特点在于实现了跨平台框架与国产操作系统深度结合的创新实践——用Flutter开发的应用能完美运行在OpenHa…

2026/9/15 0:01:16

六个月成为机器人工程师:从ROS2到SLAM的实战路径

1. 六个月的紧迫感从哪来&#xff1a;先搞清楚你要成为哪种机器人工程师说实话&#xff0c;六个月的期限并不是一个宽松的时间线。市面上任何一本正经的机器人学教材都超过五百页&#xff0c;ROS2的官方文档可以翻到你怀疑人生&#xff0c;再加上ABB、KUKA这些工业机器人厂家动…

2026/9/14 11:59:31

USB Type-C PCB布局分区设计:电源、高速信号与PD协议全攻略

做硬件这行&#xff0c;Type-C接口算是典型的“看着简单&#xff0c;做起来全坑”的东西。光引脚就24个&#xff0c;高低速信号、电源、控制线全部塞在一个小小的连接器里&#xff0c;如果PCB布局不做规划&#xff0c;打样回来基本就是“插上没反应”、“高速掉线”、“静电一打…

2026/9/14 13:53:59

系统编程学习原型如何补齐稳定性边界

系统编程学习原型如何补齐稳定性边界预算有限时&#xff0c;我先优化明显多余的复制&#xff0c;而不是猜测性地换容器。用借用传递只读数据通常就能减少分配&#xff1a; fn parse(line: &str) -> Result<Item, Error> { /* ... */ }用基准确认热点确实在分配&am…

2026/9/14 11:22:57

雨花区哪家财务公司代理记账比较好?

在雨花区&#xff0c;企业处理财税事务常常面临诸多挑战&#xff0c;选择一家靠谱的财务公司至关重要。湖南巨勤财务管理咨询有限公司就是本地正规实体财税服务机构&#xff0c;深耕本地工商财税行业多年&#xff0c;熟悉当地工商局、税务局最新政策与申报流程。主营公司注册、…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
咨询二维码