当前位置: 首页 > news >正文

【问题】容器部署场景Spring Bean偶尔循环依赖问题

问题描述

在本地开发中不会发生循环依赖问题,但是在容器场景下,制作成镜像启动后异常出现Bean的循环依赖。

问题原因

开发者在代码中使用构造函数注入来引用依赖的 Bean,这种方式可能导致循环依赖问题。虽然 Spring 框架具备循环依赖的处理机制,但它仅适用于通过 @Resource 或 @Autowired 注解进行的 Setter 方法注入或字段注入。如果开发者使用构造函数注入,当Bean的初始化未发生循环依赖,则启动没问题,对应日常开发中不会有循环依赖问题,但是在一些Docker容器场景,则会偶发抛出循环依赖异常。

深入剖析原理

在这里插入图片描述

进一步深入分析为什么本地没有循环依赖问题,但是容器里或服务器里偶发会出现循环依赖问题,问题的根源在于 Spring Boot 在扫描和实例化 Bean 时的顺序并非固定,而是可能受到 Jar 包中文件列表顺序 的影响。

  1. Jar 包文件列表的顺序
  • 在 Java 中,Jar 文件实际上是一个压缩包,内部包含了许多类文件和资源文件。当应用程序运行时,可能需要遍历这些文件。例如,Spring Boot 在启动时需要扫描特定路径下的类和资源,以识别需要创建的 Bean。
  • 在 Java 中,可以使用 JarFile.entries() 方法获取 Jar 包中的所有条目。然而,需要注意的是:JarFile.entries() 返回的并非是一个稳定有序的列表。根据 Java 官方文档,entries() 返回一个 Enumeration,但并未保证返回的顺序
  • 不同的平台或工具在打包 Jar 文件时,可能导致文件列表的顺序不同。例如,在 Windows 和 Linux 上生成的 Jar 文件,即使内容完全相同,但内部文件的排列顺序可能不同。
  1. Spring Boot 的资源匹配逻辑
    Spring Boot 使用 PathMatchingResourcePatternResolver 类来处理资源的匹配和加载。其中,doFindPathMatchingJarResources() 方法负责在 Jar 文件中查找与指定模式匹配的资源。
protected Set<Resource> doFindPathMatchingJarResources(Resource rootDirResource, String subPattern) throws IOException {...JarFile jarFile = ((JarURLConnection) rootDirURL.openConnection()).getJarFile();Enumeration<JarEntry> entries = jarFile.entries();while (entries.hasMoreElements()) {JarEntry entry = entries.nextElement();String entryPath = entry.getName();// 资源匹配逻辑if (matcher.match(subPattern, entryPath)) {// 处理匹配的资源}}...
}

如上所示,jarFile.entries() 返回的文件列表顺序不定。这意味着,Spring Boot 在扫描资源并识别 Bean 时,可能因为文件顺序的不同而导致 Bean 的加载顺序发生变化。

  1. Bean 加载顺序对循环依赖的影响
    在大多数情况下,Bean 的加载顺序并不会影响应用程序的启动。然而,当存在循环依赖时,Bean 的加载顺序可能决定了 Spring 能否成功地解决这种依赖关系。

举个例子,假设存在两个 Bean:BeanA 和 BeanB,它们互相依赖。如果 BeanA 先被加载,Spring 可能能够通过代理或其他机制解决依赖。然而,如果 BeanB 先被加载,可能就会导致无法解决的循环依赖,进而抛出异常。

由于 Jar 包中文件列表的顺序不定,导致 Bean 的加载顺序在不同的环境或不同的启动中可能有所不同,这解释了为什么循环依赖问题会 偶尔 发生。

参考链接

解决方案

方案1:干掉循环依赖

最根本的解决方案是 重新设计 Bean 的依赖关系,避免循环依赖的出现。

开发规范:默认禁用构造器注入的循环依赖
PS:在升级SpringBoot3.0后,对应Spring6.0 开始,默认情况下不再允许通过构造器注入的方式解决循环依赖。如果两个 Bean 之间通过构造器注入存在循环依赖,Spring 将会直接抛出 BeanCurrentlyInCreationException,而不再试图通过懒加载代理等方式来解决这个问题。

使用 @DependsOn 注解

Spring 提供了 @DependsOn 注解,允许我们显式地指定 Bean 的加载顺序。

@Component
@DependsOn("beanB")
public class BeanA {// ...
}@Component
public class BeanB {// ...
}

通过这种方式,可以确保 BeanB 在 BeanA 之前被初始化。然而,需要谨慎使用该注解,避免引入新的依赖问题。

代码中定义某个Bean延迟加载
@Component
public class BeanA {private final BeanB beanB;public BeanA(@Lazy BeanB beanB) {this.beanB = beanB;}// ...
}

方案3:启用延迟加载

spring.main.lazy-initialization=true 是 Spring Boot 应用中的一个配置选项,它用于启用 延迟初始化功能。
在 Spring Boot 2.2 及以上版本中,lazy-initialization 的默认值是 false。这意味着默认情况下,Spring Boot 应用中的所有 Bean 都是在应用启动时立即初始化的,而不是在第一次使用时才进行初始化。

延迟初始化 (Lazy Initialization) 的概念

在 Spring 应用中,默认情况下,所有的 @Bean 和组件(如 @Component, @Service, @Repository 等)在应用启动时都会被立即创建和初始化。这意味着在应用程序启动时,所有这些 Bean 都会被加载到 Spring 应用上下文中,无论它们何时在应用的生命周期中被使用。

启用延迟初始化 (lazy initialization) 后,**只有在第一次需要使用某个 Bean 的时候,该 Bean 才会被创建和初始化。**这可以加快应用启动的速度,尤其是在有许多不需要立即初始化的 Bean 时。

延迟初始化的优缺点

优点

  • 减少应用启动时间:对于大型应用来说,减少不必要的 Bean 初始化可以显著提高启动速度。
  • 资源节约:只有在需要时才会创建 Bean,节省了内存和 CPU 资源。

缺点

  • 潜在的延迟:由于 Bean 在第一次使用时才会被创建,这可能导致在应用运行过程中首次调用某个服务时出现轻微的延迟。
  • 调试复杂度:延迟初始化可能会导致某些问题(如配置错误、Bean 的依赖问题)直到运行时才暴露出来,这可能增加调试的复杂性。

注意事项

  • 延迟初始化适用于不需要立即加载的服务和组件,但对于关键服务(如启动时需要立即使用的 Bean),你可能希望保持默认的非延迟加载方式。
  • 延迟初始化可以通过在特定 Bean 上使用 @Lazy(false) 来排除那些需要立即初始化的 Bean。
启用延迟初始化的场景

开发环境:加快开发过程中应用的启动速度,减少等待时间。
测试环境:在单元测试时,只加载特定的 Bean,而不是所有的 Bean,减少测试的开销。
微服务:在某些微服务架构中,可能希望某些 Bean 仅在真正需要时才加载,以节省资源。

如何启用延迟初始化

可以在 application.properties 或 application.yml 中配置 spring.main.lazy-initialization=true 来启用延迟初始化。
application.properties 示例

spring.main.lazy-initialization=true

application.yml 示例

spring:main:lazy-initialization: true

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • 71、docker网络
  • 白骑士的Matlab教学高级篇 3.1 高级编程技术
  • 聊聊场景及场景测试
  • [Meachines] [Medium] Haircut Curl命令注入+TRP00F自动化权限提升+Screen4.5.0权限提升
  • C语言类型转换的问题
  • 数据结构----队列
  • RabbitMq消息队列(缓存加速)
  • 登录过程记录
  • 讲解 狼人杀中的买单双是什么意思
  • php 在app中唤起微信app进行支付,并处理回调通知
  • mysql误删数据恢复记录
  • 判断 I2C 总线通信异常原因的方法2
  • HarmonyOS WebView
  • 学习STM32(6)-- STM32单片机ADCDAC的应用
  • NFS文件共享
  • Android 初级面试者拾遗(前台界面篇)之 Activity 和 Fragment
  • CAP理论的例子讲解
  • Eureka 2.0 开源流产,真的对你影响很大吗?
  • Java 最常见的 200+ 面试题:面试必备
  • Java知识点总结(JavaIO-打印流)
  • JS 面试题总结
  • Laravel5.4 Queues队列学习
  • Mithril.js 入门介绍
  • Python十分钟制作属于你自己的个性logo
  • Redux系列x:源码分析
  • SQL 难点解决:记录的引用
  • thinkphp5.1 easywechat4 微信第三方开放平台
  • Yeoman_Bower_Grunt
  • 包装类对象
  • 漫谈开发设计中的一些“原则”及“设计哲学”
  • 前端之Sass/Scss实战笔记
  • 如何实现 font-size 的响应式
  • 蚂蚁金服CTO程立:真正的技术革命才刚刚开始
  • ​【经验分享】微机原理、指令判断、判断指令是否正确判断指令是否正确​
  • ​【已解决】npm install​卡主不动的情况
  • #java学习笔记(面向对象)----(未完结)
  • $.ajax()
  • (ibm)Java 语言的 XPath API
  • (二刷)代码随想录第15天|层序遍历 226.翻转二叉树 101.对称二叉树2
  • (附源码)ssm教材管理系统 毕业设计 011229
  • (附源码)基于SpringBoot和Vue的厨到家服务平台的设计与实现 毕业设计 063133
  • (六)什么是Vite——热更新时vite、webpack做了什么
  • (论文阅读32/100)Flowing convnets for human pose estimation in videos
  • (一)、python程序--模拟电脑鼠走迷宫
  • (转)真正的中国天气api接口xml,json(求加精) ...
  • .NET : 在VS2008中计算代码度量值
  • .net core 6 集成 elasticsearch 并 使用分词器
  • .net core webapi 部署iis_一键部署VS插件:让.NET开发者更幸福
  • .NET Core引入性能分析引导优化
  • .NET 快速重构概要1
  • .NET/C# 的字符串暂存池
  • .net6 webapi log4net完整配置使用流程
  • .NET企业级应用架构设计系列之应用服务器
  • .pub是什么文件_Rust 模块和文件 - 「译」
  • [ web基础篇 ] Burp Suite 爆破 Basic 认证密码