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

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

@Slf4jpublic 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

@Configurationpublic 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 ms2024-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: REQUEST2024-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: REQUEST2024-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: ASYNC2024-05-06 07:35:28.167  INFO 209108 --- [io-14000-exec-2] c.p.java.study.web.config.MyInterceptor  : preHandle dispatcherType=ASYNC2024-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 普通输出调用流程图

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

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

2. 线程的差异

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

图3-普通接口的时序图

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

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

解决问题

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

  1. 将过滤器中的部分业务逻辑迁移到拦截器中。

  2. 根据条件,跳过第二次的拦截器 preHandle 方法。

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

@Slf4jpublic 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());    } }

复制代码

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

相关文章:

  • 《Windows PE》3.2.3 NT头-扩展头
  • Vscode、小皮面板安装
  • 智能招聘系统小程序的设计
  • OpenCL 学习(1)---- OpenCL 基本概念
  • PGMP-03战略一致性
  • 解决docker一直出现“=> ERROR [internal] load metadata for docker.io/library/xxx“的问题
  • 【Kubernetes】常见面试题汇总(四十)
  • 大联大友尚集团推出基于炬芯科技产品的蓝牙音箱方案
  • Linux-基础篇文件权限和组的管理-练习实践(附答案)
  • 算法打卡:第十一章 图论part11
  • RabbitMQ的高级特性-事务
  • 深度学习之贝叶斯分类器
  • NASA数据集:ATLAS/ICESat-2 L3B 每日和每月网格化海冰自由面高度,第 4 版
  • 多个excel表数据比对操作
  • 叉车防撞系统方案,引领安全作业新时代
  • Angular6错误 Service: No provider for Renderer2
  • gf框架之分页模块(五) - 自定义分页
  • java2019面试题北京
  • java正则表式的使用
  • JS笔记四:作用域、变量(函数)提升
  • linux安装openssl、swoole等扩展的具体步骤
  • Linux中的硬链接与软链接
  • orm2 中文文档 3.1 模型属性
  • quasar-framework cnodejs社区
  • react-core-image-upload 一款轻量级图片上传裁剪插件
  • select2 取值 遍历 设置默认值
  • spark本地环境的搭建到运行第一个spark程序
  • 阿里云容器服务区块链解决方案全新升级 支持Hyperledger Fabric v1.1
  • 读懂package.json -- 依赖管理
  • 浅析微信支付:申请退款、退款回调接口、查询退款
  • 使用 Node.js 的 nodemailer 模块发送邮件(支持 QQ、163 等、支持附件)
  • 使用SAX解析XML
  • 手写一个CommonJS打包工具(一)
  • 长三角G60科创走廊智能驾驶产业联盟揭牌成立,近80家企业助力智能驾驶行业发展 ...
  • # 数论-逆元
  • #stm32整理(一)flash读写
  • $var=htmlencode(“‘);alert(‘2“); 的个人理解
  • (02)Hive SQL编译成MapReduce任务的过程
  • (CPU/GPU)粒子继承贴图颜色发射
  • (delphi11最新学习资料) Object Pascal 学习笔记---第14章泛型第2节(泛型类的类构造函数)
  • (LLM) 很笨
  • (ZT)薛涌:谈贫说富
  • (补)B+树一些思想
  • (层次遍历)104. 二叉树的最大深度
  • (三十)Flask之wtforms库【剖析源码上篇】
  • .ai域名是什么后缀?
  • .apk文件,IIS不支持下载解决
  • .gitignore
  • .net core使用EPPlus设置Excel的页眉和页脚
  • .Net Framework 4.x 程序到底运行在哪个 CLR 版本之上
  • .Net程序猿乐Android发展---(10)框架布局FrameLayout
  • .NET开源项目介绍及资源推荐:数据持久层 (微软MVP写作)
  • .NET命令行(CLI)常用命令
  • .one4-V-XXXXXXXX勒索病毒数据怎么处理|数据解密恢复
  • /bin/bash^M: bad interpreter: No such file or directory