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

项目背景

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

问题出现

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

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

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

场景重现

Filter

@Slf4j
@Component
@Order(1)
public class MyFilter implements Filter {

    @Override
    public 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 {

    @Override
    public 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;
    }
    
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        log.info("postHandle method is running..., thread: {}", Thread.currentThread());
    }      
    
    @Override
    public 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 {
    
    @Bean
    public MyInterceptor myInterceptor() {
        return new MyInterceptor();
    }
    
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(myInterceptor()).addPathPatterns("/**");
    }
    
    @Override
    public 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 {
    
    @Override
    public 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;
    }
    
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        log.info("postHandle method is running..., thread: {}", Thread.currentThread());     
    }
    
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        log.info("afterCompletion method is running..., thread: {}", Thread.currentThread());
    } 
}

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


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

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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

java实现地形dem产汇流流场数据提取解析

一、基础概念 在GIS和气象学、海洋学、大气科学、水文学等领域&#xff0c;"提取流场"通常指的是从数据集中识别和分析流体&#xff08;如水流、风场、洋流、大气流&#xff09;的运动模式和流向的过程。这个过程涉及数据处理、可视化和分析技术&#xff0c;下面是提…

LDR6500一拖二快充线方案

随着科技的飞速发展&#xff0c;我们的电子设备日益增多&#xff0c;从智能手机到平板电脑&#xff0c;再到各种可穿戴设备&#xff0c;它们已成为我们日常生活不可或缺的一部分。然而&#xff0c;随之而来的充电问题也日益凸显。为了解决这一难题&#xff0c;Type-C接口一拖二…

Element快速入门

Vue组件库Element 1 Element介绍 vue是侧重于VM开发的&#xff0c;主要用于数据绑定到视图的&#xff0c;ElementUI就是一款侧重于V开发的前端框架&#xff0c;主要用于开发美观的页面的。 Element&#xff1a;是饿了么公司前端开发团队提供的一套基于 Vue 的网站组件库&…

⌈ 传知代码 ⌋ 基于BERT的语义分析实现

&#x1f49b;前情提要&#x1f49b; 本文是传知代码平台中的相关前沿知识与技术的分享~ 接下来我们即将进入一个全新的空间&#xff0c;对技术有一个全新的视角~ 本文所涉及所有资源均在传知代码平台可获取 以下的内容一定会让你对AI 赋能时代有一个颠覆性的认识哦&#x…

鸿蒙开发接口媒体:【@ohos.multimedia.camera (相机管理)】

相机管理 说明&#xff1a; 开发前请熟悉鸿蒙开发指导文档&#xff1a; gitee.com/li-shizhen-skin/harmony-os/blob/master/README.md点击或者复制转到。 本模块首批接口从API version 9开始支持。后续版本的新增接口&#xff0c;采用上角标单独标记接口的起始版本。 导入模块…

【LeetCode算法】第101题:对称二叉树

目录 一、题目描述 二、初次解答 三、官方解法 四、总结 一、题目描述 二、初次解答 1. 思路&#xff1a;递归判定左子树和右子树是否对称。用一个新函数sym来递归判定左子树和右子树是否对称。该函数细节&#xff1a;判定当前传入的两个根节点是否为空&#xff0c;若均为空…

React + Taro 项目 实际书写 感受

之前我总结了部分react 基础 根据官网的内容 以及Taro 框架的内容 今天我试着开始写了一下页面和开发 说一下我的感受 我之前写的是vue3 今天是第一次真正根据需求做页面开发 和逻辑功能 代码的书写 主体就是开发了这个页面 虽说这个页面 很简单 但是如果你要是第一次写 难说…

通过nginx解决跨域问题,并测试

*表示所有域名 # 测试域名server {listen 80;server_name chat.test.com;#配置根目录location / {proxy_pass http://127.0.0.1:3000;}location /api/ {# 设置允许跨域的域&#xff0c;* 表示允许任何域&#xff0c;也可以设置特定的域add_header Access-Control-Allow-Origin …

将三个字符串通过strcat连接起来并打印输出

将三个字符串通过strcat连接起来并打印输出 #include <stdio.h> #include <string.h> int main () { char a[10]"I", b[10]" am",c[10]" happy"; strcat(a,b); strcat(a,c); printf("%s",a); printf("\n"); re…

Linux基本命令的使用(ls cd touch)

一、Windows系统常见的文件类型 • 文本文件格式&#xff1a;txt、doc、pdf、html等。 • 图像文件格式&#xff1a;jpg、png、bmp、gif等。 • 音频文件格式&#xff1a;mp3、wav、wma等。 • 视频文件格式&#xff1a;mp4、avi、wmv、mov等。 • 压缩文件格式&#xff1a;zip…

配置华为路由器通过RADIUS对接安当ASP身份认证服务器以实现上网功能解决方案

当配置华为路由器通过RADIUS对接安当ASP身份认证服务器以实现上网功能时&#xff0c;以下是一个更详细的解决方案&#xff1a; 一、前期准备 1. 确认网络环境&#xff1a; 确保华为路由器与安当ASP身份认证服务器之间的网络连接稳定可靠。确定RADIUS协议所需的端口&#xff08…

【量算分析工具-贴地距离】GeoServer改造Springboot番外系列九

【量算分析工具-概述】GeoServer改造Springboot番外系列三-CSDN博客 【量算分析工具-水平距离】GeoServer改造Springboot番外系列四-CSDN博客 【量算分析工具-水平面积】GeoServer改造Springboot番外系列五-CSDN博客 【量算分析工具-方位角】GeoServer改造Springboot番外系列…

思科防火墙 网线连接的端口还是down 已配置 端口还是down

环境&#xff1a; 思科防火墙fpr-2100 isco Firepower 2100 系列防火墙是思科系统&#xff08;Cisco Systems&#xff09;推出的一款中端网络安全和防火墙设备。这一系列的产品主要针对中到大型企业的需求&#xff0c;提供高性能的威胁防护和网络流量管理功能。 问题描述&am…

【算法】MT2 棋子翻转

✨题目链接&#xff1a; MT2 棋子翻转 ✨题目描述 在 4x4 的棋盘上摆满了黑白棋子&#xff0c;黑白两色棋子的位置和数目随机&#xff0c;其中0代表白色&#xff0c;1代表黑色&#xff1b;左上角坐标为 (1,1) &#xff0c;右下角坐标为 (4,4) 。 现在依次有一些翻转操作&#…

【Linux】磁盘结构文件系统软硬链接动静态库

目录 一.磁盘结构 1、磁盘的物理结构 2、磁盘的存储结构 3、磁盘的逻辑结构 二.文件系统 1、对IO单位的优化 2、磁盘分区与分组 3、对分组的具体管理方法 4、文件操作 三.软硬链接 1、理解硬链接 2、理解软连接 3、理解.和.. 四、动静态库 1、什么是动静态库 2、…

HSViT: Horizontally Scalable Vision Transformer

论文链接&#xff1a;https://arxiv.org/pdf/2404.05196 代码链接&#xff1a;https://github.com/xuchenhao001/HSViT 根据文档内容&#xff0c;我梳理出以下大纲&#xff1a; 一、引言 ViT模型在计算机视觉领域受到广泛关注&#xff0c;但需要大规模数据集进行预训练才能取…

python绘制北京汽车流量热力图:从原理到实践

新书上架~&#x1f447;全国包邮奥~ python实用小工具开发教程http://pythontoolsteach.com/3 欢迎关注我&#x1f446;&#xff0c;收藏下次不迷路┗|&#xff40;O′|┛ 嗷~~ 目录 一、引言 二、热力图绘制原理 三、热力图绘制实践 1. 数据准备 2. 地图组件选择 3. 数据…

【Python】解决Python报错:AttributeError: ‘function‘ object has no attribute ‘xxx‘

&#x1f9d1; 博主简介&#xff1a;阿里巴巴嵌入式技术专家&#xff0c;深耕嵌入式人工智能领域&#xff0c;具备多年的嵌入式硬件产品研发管理经验。 &#x1f4d2; 博客介绍&#xff1a;分享嵌入式开发领域的相关知识、经验、思考和感悟&#xff0c;欢迎关注。提供嵌入式方向…

关于网络编程

目录 1、InetAdress类 2、Socket套接字 3、UDP数据报套接字编程 &#xff08;1&#xff09;DatagramSocket 类 &#xff08;2&#xff09;DatagramPacket类 &#xff08;3&#xff09;处理无连接问题 UdpEchoServer.java UdpEchoClient.java 4、TCP流套接字编程 &…

设计模式23——状态模式

写文章的初心主要是用来帮助自己快速的回忆这个模式该怎么用&#xff0c;主要是下面的UML图可以起到大作用&#xff0c;在你学习过一遍以后可能会遗忘&#xff0c;忘记了不要紧&#xff0c;只要看一眼UML图就能想起来了。同时也请大家多多指教。 状态模式&#xff08;State&am…