发布时间:2026/7/30 8:17:27
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/7/30 8:17:27

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

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

2026/7/30 8:17:27

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

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

2026/7/30 8:12:27

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

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

2026/7/30 10:17:34

RAG与MCP技术解析:大模型外部数据集成与工具调用实战指南

1. 先搞清楚 MCP 和 RAG 到底解决什么问题 如果你正在处理大模型&#xff08;LLM&#xff09;如何连接外部数据或工具的问题&#xff0c;大概率已经听过 RAG&#xff08;检索增强生成&#xff09;和最近出现的 MCP&#xff08;模型上下文协议&#xff09;。这两个方案都不是为了…

2026/7/30 10:17:34

Spring Boot拦截HTTP请求与设置请求头实战指南

1. Spring Boot拦截HTTP请求设置请求头的核心场景 在Web开发中&#xff0c;经常需要统一处理HTTP请求和响应。比如在微服务架构中&#xff0c;我们可能需要在所有请求中添加认证头&#xff08;如JWT Token&#xff09;&#xff0c;或者记录请求日志、修改响应内容。Spring Boot…

2026/7/30 10:17:34

Python包管理进阶:使用pip从Git仓库直接安装与配置指南

1. 为什么需要从Git仓库直接安装Python包&#xff1f; 在Python开发中&#xff0c;我们最熟悉的包安装方式无疑是 pip install package-name 。这个命令会从PyPI&#xff08;Python Package Index&#xff09;这个官方的软件仓库中&#xff0c;拉取对应名称的包及其依赖&…

2026/7/30 10:17:34

安卓模拟器安装Magisk:安全沙盒中实现系统级Root与模块测试

1. 项目概述&#xff1a;为什么要在模拟器里折腾Magisk&#xff1f;如果你和我一样&#xff0c;是个喜欢在安卓世界里“搞机”的玩家&#xff0c;那么对Magisk一定不陌生。它早已超越了单纯的Root工具&#xff0c;成为了一个强大的模块化框架&#xff0c;能让我们在不破坏系统完…

2026/7/30 10:17:34

百度网盘提取码智能获取:5秒破解资源锁定的终极指南

百度网盘提取码智能获取&#xff1a;5秒破解资源锁定的终极指南 【免费下载链接】baidupankey 在线查询网盘提取码&#xff08;维护中 rm repo&#xff09; 项目地址: https://gitcode.com/gh_mirrors/ba/baidupankey 还在为百度网盘加密资源而烦恼吗&#xff1f;每次打…

2026/7/30 10:12:33

蓝牙模块开发实战:从选型到量产,避坑指南与优化策略

1. 项目概述&#xff1a;从“能连上”到“用得稳”的蓝牙开发之旅 提起蓝牙模块&#xff0c;很多刚接触嵌入式或物联网开发的朋友第一反应可能就是“简单”——不就是两个设备配对、传点数据嘛。但真当你上手把一个蓝牙模块集成到自己的项目里&#xff0c;比如做个无线传感器数…

2026/7/29 22:32:30

PDF合并与动态水印的工程化方案:2026国内免费工具实测对比

一、背景与测试方案 在实际项目交付中&#xff0c;PDF文件合并与版权保护水印的叠加是一个高频但容易被低估的技术需求。典型的处理链路涉及&#xff1a;多源PDF的文件流合并、页面级水印渲染&#xff08;含透明度混合与图层叠加&#xff09;、输出文件体积控制。看似简单的操作…

2026/7/30 0:01:39

[GESP202606 四级] 扫雷

B4557 [GESP202606 四级] 扫雷 https://www.luogu.com.cn/problem/B4557 中国计算机学会&#xff08;CCF&#xff09;2026年6月C四级讲解——扫雷 https://www.bilibili.com/video/BV1MCMg6AEXR/ B4557 [GESP202606 四级] 扫雷 https://www.bilibili.com/video/BV1ZKTj6ZEVh/ 2…

2026/7/30 0:01:39

Windows驱动存储终极清理工具:DriverStoreExplorer完全指南

Windows驱动存储终极清理工具&#xff1a;DriverStoreExplorer完全指南 【免费下载链接】DriverStoreExplorer Driver Store Explorer 项目地址: https://gitcode.com/gh_mirrors/dr/DriverStoreExplorer 您是否曾因Windows系统盘空间不足而烦恼&#xff1f;是否遇到过设…

2026/7/29 13:12:43

3个高效策略:快速掌握Axure中文界面配置

3个高效策略&#xff1a;快速掌握Axure中文界面配置 【免费下载链接】axure-cn Chinese language file for Axure RP. Axure RP 简体中文语言包。支持 Axure 11、10、9。不定期更新。 项目地址: https://gitcode.com/gh_mirrors/ax/axure-cn 还在为Axure RP的英文界面感…