技术分享 | SpringBoot 流式输出时,正常输出后为何突然报错?

项目背景

  1. 一个 SpringBoot 项目同时使用了 Tomcat 的过滤器和 Spring 的拦截器,一些线程变量在过滤器中初始化并在拦截器中使用。
  2. 该项目需要调用大语言模型进行流式输出。
  3. 项目中,笔者使用 SpringBoot 的 ResponseEntity<StreamingResponseBody> 将流式输出返回前端。

问题出现

问题出现在上述第 3 点:正常输出一段内容后,后台突然报错,而报错内容由拦截器产生

笔者仔细查看了报错日志,发现只是拦截器的问题:执行时由于某些线程变量不存在而报错。但是,这些线程变量已经在过滤器中初始化了。

那么问题来了:为什么这个接口明明可以正常通过过滤器和拦截器,并开始正常输出,却又突然在拦截器中报错呢?

场景重现

Filter

@Slf4j
@Component
@Order(1)
public class MyFilter implements Filter {@Overridepublic void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {// 要继续处理请求,必须添加 filterChain.doFilter()log.info("doFilter method is running..., thread: {}, dispatcherType: {}", Thread.currentThread(), servletRequest.getDispatcherType()); filterChain.doFilter(servletRequest,servletResponse);} 
}

Interceptor

@Slf4j
public class MyInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull Object handler) throws Exception {log.info("preHandle method is running..., thread: {}, dispatcherType: {}", Thread.currentThread(), request.getDispatcherType());if (DispatcherType.ASYNC == request.getDispatcherType()) {log.info("preHandle dispatcherType={}", request.getDispatcherType());}return true;}@Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {log.info("postHandle method is running..., thread: {}", Thread.currentThread());}      @Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {log.info("afterCompletion method is running..., thread: {}", Thread.currentThread());} 
}

WebMvcConfigurer

@Configuration
public class WebAppConfigurer implements WebMvcConfigurer {@Beanpublic MyInterceptor myInterceptor() {return new MyInterceptor();}@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(myInterceptor()).addPathPatterns("/**");}@Overridepublic void configureAsyncSupport(AsyncSupportConfigurer configurer) {configurer.setDefaultTimeout(120_000L);configurer.registerCallableInterceptors();configurer.registerDeferredResultInterceptors();ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();executor.setCorePoolSize(5);executor.setMaxPoolSize(10);executor.setQueueCapacity(100);executor.setThreadNamePrefix("web-async-");executor.initialize();configurer.setTaskExecutor(executor);}
}

Controller

@Slf4j
@RestController
@RequestMapping("/test-stream")
public class TestStreamController {@ApiOperation("流式输出示例")@PostMapping(value = "/example", produces = MediaType.TEXT_EVENT_STREAM_VALUE)public ResponseEntity<StreamingResponseBody> example() {log.info("Stream method is running, thread: {}", Thread.currentThread());return  ResponseEntity.status(HttpStatus.OK).contentType(new MediaType(MediaType.TEXT_EVENT_STREAM, StandardCharsets.UTF_8)).body(outputStream -> {log.info("Internal stream method is running, thread: {}", Thread.currentThread());try (outputStream) {String msg = "To be or not to be!";outputStream.write(msg.getBytes(StandardCharsets.UTF_8));outputStream.flush();}});}
}

根据以下运行日志,我们可以看到拦截器的 preHandle 确实执行了两次,并且此次调用过程共有 3 个线程(io-14000-exec-1web-async-1io-14000-exec-2)参与了工作。

2024-05-06 07:35:27.362  INFO 209108 --- [io-14000-exec-1] o.a.c.c.C.[.[localhost].[/java-study]    : Initializing Spring DispatcherServlet 'dispatcherServlet'
2024-05-06 07:35:27.362  INFO 209108 --- [io-14000-exec-1] o.s.web.servlet.DispatcherServlet        : Initializing Servlet 'dispatcherServlet'
2024-05-06 07:35:27.365  INFO 209108 --- [io-14000-exec-1] o.s.web.servlet.DispatcherServlet        : Completed initialization in 3 ms
2024-05-06 07:35:27.402  INFO 209108 --- [io-14000-exec-1] com.peng.java.study.web.config.MyFilter  : doFilter method is running..., thread: Thread[http-nio-14000-exec-1,5,main], dispatcherType: REQUEST
2024-05-06 07:35:28.107  INFO 209108 --- [io-14000-exec-1] c.p.java.study.web.config.MyInterceptor  : preHandle method is running..., thread: Thread[http-nio-14000-exec-1,5,main], dispatcherType: REQUEST
2024-05-06 07:35:28.121  INFO 209108 --- [io-14000-exec-1] c.p.j.s.w.r.test.TestStreamController    : Stream method is running, thread: Thread[http-nio-14000-exec-1,5,main]
2024-05-06 07:35:28.152  INFO 209108 --- [    web-async-1] c.p.j.s.w.r.test.TestStreamController    : Internal stream method is running, thread: Thread[web-async-1,5,main]
2024-05-06 07:35:28.167  INFO 209108 --- [io-14000-exec-2] c.p.java.study.web.config.MyInterceptor  : preHandle method is running..., thread: Thread[http-nio-14000-exec-2,5,main], dispatcherType: ASYNC
2024-05-06 07:35:28.167  INFO 209108 --- [io-14000-exec-2] c.p.java.study.web.config.MyInterceptor  : preHandle dispatcherType=ASYNC
2024-05-06 07:35:28.174  INFO 209108 --- [io-14000-exec-2] c.p.java.study.web.config.MyInterceptor  : postHandle method is running..., thread: Thread[http-nio-14000-exec-2,5,main]
2024-05-06 07:35:28.183  INFO 209108 --- [io-14000-exec-2] c.p.java.study.web.config.MyInterceptor  : afterCompletion method is running..., thread: Thread[http-nio-14000-exec-2,5,main]

问题分析

1. 方法调用流程的差异

众所周知,SpringBoot 的普通输出接口调用流程图如图 1 所示。

图1-SpringBoot 普通输出调用流程图
(图1-SpringBoot 普通输出调用流程图)

结合日志,我们可以简单画出流式输出接口对应的流程图(图 2)。

图2-SpringBoot 流式输出调用流程图

(图2-SpringBoot 流式输出调用流程图)

2. 线程的差异

普通接口的执行时序图如图 3 所示。

图3-普通接口的时序图

(图3-普通接口的时序图)

而流式接口的时序图如图 4 所示。

图4-流式接口的调用时序图
(图4-流式接口的调用时序图)

解决问题

通过分析,对流式输出的情况提出两种解决方案:

  1. 将过滤器中的部分业务逻辑迁移到拦截器中。
  2. 根据条件,跳过第二次的拦截器 preHandle 方法。

笔者选择了第二个方案,实现代码如下。

@Slf4j
public class MyInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull Object handler) throws Exception {log.info("preHandle method is running..., thread: {}, dispatcherType: {}", Thread.currentThread(), request.getDispatcherType());// 如果是异步请求,则跳过if (DispatcherType.ASYNC == request.getDispatcherType()) {log.info("preHandle dispatcherType={}", request.getDispatcherType());return true;}return true;}@Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {log.info("postHandle method is running..., thread: {}", Thread.currentThread());     }@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {log.info("afterCompletion method is running..., thread: {}", Thread.currentThread());} 
}

需要注意,请求线程和回调线程都需考虑清理线程变量,不然会导致内存泄漏。


了解更多技术干货、研发管理实践等分享,请关注 LigaAI。

邀您体验 LigaAI-智能研发协作平台,开启 AI 驱动的智能研发协作!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/24361.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

R语言探索与分析14-美国房价及其影响因素分析

一、选题背景 以多元线性回归统计模型为基础&#xff0c;用R语言对美国部分地区房价数据进行建模预测&#xff0c;进而探究提高多元回 归线性模型精度的方法。先对数据进行探索性预处理&#xff0c;随后设置虚拟变量并建模得出预测结果&#xff0c;再使用方差膨胀因子对 多重共…

python panads读取保存数据

学习目标 能够使用Pandas读写文件中的数据 知道Pandas读取数据时常用参数index_col、parse_dates、sheet_name、index的作用和用法 知道Pandas和MySQL数据库进行读写交互的方法 1 读写文件 常用读写文件函数清单【查表】无需记忆 文件格式读取函数写入函数xlsxpd.read_exce…

SOLIDWORKS工艺软件 慧德敏学

SOLIDWORKS工艺软件功能介绍 1、快速制作工艺过程卡 a)调用模板&#xff0c;快速生成工艺过程卡中的工序、设备、简图以及工时等信息&#xff0c;支持“加工工艺卡”和“装配工艺卡”的制作 b)选择文件&#xff0c;快速读取现有工艺过程卡文件&#xff0c;提取数据并显示&…

【问题分析】WMS无焦点窗口的ANR问题 + transientLaunch介绍【Android 14】

问题描述 Monkey跑出的Camera发生ANR的问题&#xff0c;其实跟Camera无关&#xff0c;任意一个App都会在此场景下发生ANR&#xff0c;场景涉及到Launcher的RecentsActivity界面&#xff0c;和transientLaunch相关。 1 log分析 看问题发生的场景&#xff1a; 1、Camera App的…

小学一年级数学上册,我终于学完了

目录 一、背景二、过程1.我对课程中的一些知识的思考2.我对于产品的思考3.我对自己儿子与知识产品结合的思考4.产品反馈的那些有意思的数据 三、总结 一、背景 简约而不简单&#xff0c;即是曾经的再现&#xff0c;也是未来的延伸&#xff0c;未来已来&#xff0c;就在脚下。 …

公司面试题总结(一)

1.说说你对盒子模型的理解&#xff0c;如何切换 当对一个文档进行布局的时候&#xff0c;浏览器的渲染引擎会根据标准之一的 CSS 基础框盒模型&#xff0c; 将所有元素表示为一个个矩形的盒子 • content&#xff0c;即实际内容&#xff0c;显示文本和图像 • boreder&am…

export 和 export default 的区别

在 JavaScript 中&#xff0c;export 和 export default 都是用于导出模块中的内容的关键字&#xff0c;但它们有一些区别&#xff1a; export: export 关键字用于导出多个变量、函数或对象。可以一次导出多个内容&#xff0c;并且在导入时需要使用对应的名称。例如&#xff1a…

qmt量化交易策略小白学习笔记第18期【qmt编程之获取对应周期的北向南向数据--方式2:原生python】

qmt编程之获取对应周期的北向南向数据 qmt更加详细的教程方法&#xff0c;会持续慢慢梳理。 也可找寻博主的历史文章&#xff0c;搜索关键词查看解决方案 &#xff01; 获取对应周期的北向南向数据 提示 该数据通过get_market_data_ex接口获取获取历史数据前需要先用downl…

增加强制索引依然慢

版本: 阿里云RDS MySQL 8.0.25 线上数据库CPU达到100%, 定位到如下SQL EXPLAIN SELECT ssd.goods_no,ssd.goods_name,ssd.goods_spec,ssd.goods_unit,ssd.create_time,w.warehouse_name,sb.batch_no,swl.warehouse_region_location_name,sc.customer_name AS goodsOwnerName,s…

idm2024最新完美破解版免费下载 idm绿色直装版注册机免费分享 idm永久激活码工具

IDM 2024破解版重新开发了调度程序和MMS协议支持、重新设计和增强的下载引擎、与所有最新浏览器的独特高级集成、改进的工具栏以及大量其他改进和新功能&#xff0c;这一全新的更新&#xff0c;使得IDM下载器更加完美。值得一提的是&#xff0c;它可以借助油猴浏览器的脚本&…

Maven核心功能依赖和构建管理

1.依赖管理和配置 Maven 依赖管理是 Maven 软件中最重要的功能之一。Maven 的依赖管理能够帮助开发人员自动解决软件包依赖问题&#xff0c;使得开发人员能够轻松地将其他开发人员开发的模块或第三方框架集成到自己的应用程序或模块中&#xff0c;避免出现版本冲突和依赖缺失等…

苹果手机618大降价重登销量榜首 红米K70pro为何成京东618国产手机之光

今天的618已经好几天了&#xff0c;很多买有机的已经下单&#xff0c;不出意外苹果15系列手机仍然是最卖座的手机&#xff0c;大家虽然口号喊得很响身体却是诚实的。但令人感到意外的是&#xff0c;今年618国产手机的第一把交椅确实红米K70系列&#xff0c;说好的支持华为呢&am…

45-4 护网溯源 - 溯源相关思路

一、先了解国家的相关法规 根据中华人民共和国网络安全法和刑法相关规定,严禁从事危害网络安全的活动,包括但不限于入侵他人网络、扰乱网络正常功能、窃取网络数据等行为。任何个人和组织都不得以非法方式获取公民个人信息、出售或非法提供个人信息给他人。违反法律规定,给…

如何在手机上恢复误删除的视频?

说到移动设备上的视频恢复&#xff0c;我们仍将揭开4种解决方案供您使用。希望它们对您的案件有所帮助。 众所周知&#xff0c;我们移动设备上的视频应用程序将创建一个缓存文件夹&#xff0c;以在它们永远消失之前临时存储已删除的项目。因此&#xff0c;有许多iPhone / Andr…

C++基础与深度解析 | 模板 | 函数模板 | 类模板与成员函数模板 | concepts | 完美转发 | 模板的其他内容

文章目录 一、函数模板二、类模板与成员函数模板三、Concepts(C20)四、模板相关内容1.数值模板参数与模板模板参数2.别名模板与变长模板3.包展开与折叠表达式4.完美转发与lambda表达式模板5.消除歧义与变量模板 一、函数模板 在C中&#xff0c;函数模板是一种允许你编写可以处理…

iOS 实现蓝牙设备重连的四种方式

文章目录 一、通过identifiers的方式实现重连二、通过UUID的方式实现重连三、通过scan的方式实现重连四、通过didDisconnect后回连实现重连 一、通过identifiers的方式实现重连 /*!* method retrievePeripheralsWithIdentifiers:** param identifiers A list of <code…

java项目使用jsch下载ftp文件

pom <dependency><groupId>com.jcraft</groupId><artifactId>jsch</artifactId><version>0.1.55</version> </dependency>demo1&#xff1a;main方法直接下载 package com.example.controller;import com.jcraft.jsch.*; im…

【mysql】数据报错: incorrect datetime value ‘0000-00-00 00:00:00‘ for column

一、问题原因 时间字段在导入值0000-00-00 00:00:00或者添加 NOT NULL的时间字段时&#xff0c;会往mysql添加0值&#xff0c;此时可能出现此报错。 这是因为当前的MySQL不支持datetime为0&#xff0c;在MySQL5.7版本以上&#xff0c;默认设置sql_mode模式包含NO_ZERO_DATE, N…

基于UrBAN数据集:用声音监测和预测蜜蜂群体的健康状况

蜜蜂在生态平衡中扮演着关键角色&#xff0c;是农业作物和自然生态系统中的重要传粉者。它们不仅生产蜂蜜和蜂蜡&#xff0c;还对许多水果和种子作物产生影响&#xff0c;包括杏仁、柑橘类水果和蓝莓等。蜜蜂群体的健康状况和数量的下降可能对农业产业产生重大影响。全球范围内…

Python爬取城市空气质量数据

Python爬取城市空气质量数据 一、思路分析1、寻找数据接口2、发送请求3、解析数据4、保存数据二、完整代码一、思路分析 目标数据所在的网站是天气后报网站,网址为:www.tianqihoubao.com,需要采集武汉市近十年每天的空气质量数据。先看一下爬取后的数据情况: 1、寻找数据…