技术分享 | SpringBoot 流式输出时,正常输出后为何突然报错?
项目背景
- 一个 SpringBoot 项目同时使用了 Tomcat 的过滤器和 Spring 的拦截器,一些线程变量在过滤器中初始化并在拦截器中使用。
- 该项目需要调用大语言模型进行流式输出。
- 项目中,笔者使用 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-1
,web-async-1
,io-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 普通输出调用流程图)
结合日志,我们可以简单画出流式输出接口对应的流程图(图 2)。
(图2-SpringBoot 流式输出调用流程图)
2. 线程的差异
普通接口的执行时序图如图 3 所示。
(图3-普通接口的时序图)
而流式接口的时序图如图 4 所示。
(图4-流式接口的调用时序图)
解决问题
通过分析,对流式输出的情况提出两种解决方案:
- 将过滤器中的部分业务逻辑迁移到拦截器中。
- 根据条件,跳过第二次的拦截器 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。

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
在任何云上运行 云的可移植性你考虑过吗
云可移植性是构建可扩展、有弹性、云原生应用程序的一种策略。谈到云原生*通常也会暗含地考虑到云的可移植性。云原生是一种应用程序开发和部署架构方法*可最大限度利用云计算资源的弹性和敏捷性。然而*当团队开始使用单一云平台*并围绕这个平台的供应商所提供的专用工具和托管服务进行构建时*很快就会面临供应商锁定的局面。 延伸阅读*了解 Akamai cloud*computing 可移植的工作负载能在不同计算环境和基础设施平台上轻松迁移、部署和管理。借此*企业能够避免被供应商锁定*并保持云战略的灵活性。 如果从一开始就选择“云中立”的方法*并使用能与任何云平台相兼容的工具*我们就可以根据需求的变化灵活做出改变。可移植性战略还能让我们更深入地了解资源的使用方式和原因*并根据应用和业务需求选择不同的云平台*甚至在不同平台间转移。 设计云可移植性策略 如果你正在考虑云应用架构*那么可以通过以下无方面考虑着手*设计成功的可移植工作负载。 确定需求 实现可移植工作负载的第一步是客观地确定工作负载需求。我们可能经常会看到这样的情况*在进行最开始的这一步工作之前*主观臆断就已经污染了整个过程*因为人们的目光会被云...
- 下一篇
MyBatis-Flex v1.9.0 发布,一个优雅的 MyBatis 增强框架
MyBatis-Flex: 一个优雅的 MyBatis 增强框架 特征 1、很轻量 MyBatis-Flex 整个框架只依赖 MyBatis,再无其他任何第三方依赖。 2、只增强 MyBatis-Flex 支持 CRUD、分页查询、多表查询、批量操作,但不丢失 MyBatis 原有的任何功能。 3、高性能 MyBatis-Flex 采用独特的技术架构、相比许多同类框架,MyBatis-Flex 的在增删改查等方面的性能均超越其 5~10 倍或以上。 4、更灵动 MyBatis-Flex 支持多主键、多表查询、逻辑删除、乐观锁、数据脱敏、数据加密、多数据源、分库分表、字段权限、 字段加密、多租户、事务管理、SQL 审计... 等等等等。 这一切,免费且灵动。 MyBatis-Flex v1.9.0 更新细节如下: 优化:重构 Mapper 的获取,使之减少一层代理从而获得更高性能 优化:优化 LambdaUtil 的性能 优化:优化代码生成器 Controller 代码生成的主键类型,感谢@王帅 优化:优化代码生成器的 JdbcTypeMapping 优化:优化 QueryColumn ...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- CentOS7,8上快速安装Gitea,搭建Git服务器
- Docker使用Oracle官方镜像安装(12C,18C,19C)
- SpringBoot2整合MyBatis,连接MySql数据库做增删改查操作
- Windows10,CentOS7,CentOS8安装Nodejs环境
- Docker快速安装Oracle11G,搭建oracle11g学习环境
- 2048小游戏-低调大师作品
- SpringBoot2初体验,简单认识spring boot2并且搭建基础工程
- CentOS7编译安装Cmake3.16.3,解决mysql等软件编译问题
- SpringBoot2配置默认Tomcat设置,开启更多高级功能
- CentOS7编译安装Gcc9.2.0,解决mysql等软件编译问题