实现HttpServletRequest下多次获取流数据
HttpServletRequest下多次获取流数据
- 背景
- 示例
- 错误的尝试
- 全局替换
- 执行顺序
背景
众所周知request的输入流只能读取一次,不能重复读取。而在HttpServletRequest
中,获取请求体数据的流(通过getInputStream()方法)默认只能被读取一次。一旦读取后,流将处于末尾状态,再次尝试读取会返回EOF(文件结束符),无法重新获取原始数据。
如果在过滤器或者拦截器中有业务需求对输入流进行一些其他操作,那么此处读取过后再到controller
层就会报错,提示IO异常,本次的需求就是在拦截器中获取请求体中的数据。
如果多次调用会出现如下错误【如果拦截器中将请求体中的流消费完毕,那么到了Controller方法中如果有一个参数需要读取请求体内容(例如@RequestBody注解的参数)那么会出现异常)】
java.lang.IllegalStateException: getInputStream() has already been called for this request
这里采用实现HttpServletRequestWrapper
自定义一个包装器的方式解决输入流不能重复读取的问题,并实现修改流的功能。
示例
主要思想:将流转换成字节数组作为对象的属性持久化保存起来,当需要获取的时候再将字节数组转换回数据流。
import org.springframework.http.HttpInputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.util.WebUtils;import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;public class BufferedRequestWrapper extends HttpServletRequestWrapper {private byte[] requestBodyBytes;//在类的序列化过程中忽略这些字段private transient ServletInputStream inputStream;private transient BufferedReader reader;public BufferedRequestWrapper(HttpServletRequest request) throws IOException {super(request);// 一次性将请求体内容读取并缓存到requestBodyBytes中requestBodyBytes = StreamUtils.copyToByteArray(request.getInputStream());}@Overridepublic ServletInputStream getInputStream() throws IOException {if (inputStream == null) {inputStream = new BufferedServletInputStream();}return inputStream;}@Overridepublic BufferedReader getReader() throws IOException {if (reader == null) {reader = new BufferedReader(new InputStreamReader(getInputStream()));}return reader;}// 自定义ServletInputStream以实现多次读取private class BufferedServletInputStream extends ServletInputStream {private ByteArrayInputStream buffer;public BufferedServletInputStream() {buffer = new ByteArrayInputStream(requestBodyBytes);}@Overridepublic int read() throws IOException {return buffer.read();}@Overridepublic boolean isFinished() {return buffer.available() == 0;}@Overridepublic boolean isReady() {return true;}@Overridepublic void setReadListener(ReadListener listener) {throw new UnsupportedOperationException("Not supported");}}// 如果需要以String形式获取请求体内容public String getRequestBody() throws IOException {return new String(requestBodyBytes, getCharacterEncoding());}// 可选:将请求体反序列化为JSON对象public <T> T getRequestBodyAs(Class<T> clazz) throws IOException {ObjectMapper mapper = new ObjectMapper();return mapper.readValue(requestBodyBytes, clazz);}
}
然后,在我们需要的地方使用这个BufferedRequestWrapper
。但是,需要注意的是这个新的 request
对象是我们消耗掉原来 request 中的流数据创建的,也就是说,原来的流已经被关闭了无法再次使用。
既然如此,我们就需要让新建的请求对象与之前的进行替换,达到可以多次获取数据流的效果。
注意:
从Servlet 3.1开始,
ServletInputStream
有新的方法isFinished()
,isReady()
和setReadListener(ReadListener readListener)
,在自定义CachedServletInputStream
时可能需要实现这些方法。因为这些方法用于支持非阻塞IO操作,如果你不使用非阻塞读取,可以简单地实现这些方法并返回默认值(例如,isFinished()
返回true
,而isReady()
返回true
)。
错误的尝试
报错:HttpMessageNotReadableException: Required request body is missing
。
错误解释:Controller
方法中有一个参数需要读取请求体内容(例如@RequestBody注解的参数),但实际请求中并没有包含请求体或者请求体为空。
错误的代码
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) th// 正确使用 RepeatableRequestWrapper 包装请求if (!(request instanceof BufferedRequestWrapper)) {request = new BufferedRequestWrapper(request);}//判断当前拦截到的是Controller的方法还是其他资源if (!(handler instanceof HandlerMethod)) {//当前拦截到的不是动态方法(控制器中的方法),直接放行return true;}//获取访问的方法HandlerMethod handlerMethod = (HandlerMethod) handler;Method method = handlerMethod.getMethod();//如果没有被日志注解注解,则放行if (!method.isAnnotationPresent(Logger.class)) {return true;}//其他无关校验逻辑和其他信息(略).....String requestBody = ((BufferedRequestWrapper) request).getRequestBody();//3.记录方法的参数 request.setAttribute("rqParam", requestBody);return true;
}@Override
public void postHandle(HttpServletRequest request,HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {//这里的需求是获取请求参数之后和其他信息一起插入到数据库中,记录下操作//2.获取请求参数String rqParam = (String) request.getAttribute("rqParam");//其他略......
}
这里可以发现
request = new BufferedRequestWrapper(request);
这段代码已经将 request 请求替换为了 BufferedRequestWrapper
,但是会出现如上报错,可知这里仅仅只是替换了此处的请求对象,其他的地方使用的还是之前的请求。
因此,为了确保 BufferedRequestWrapper 正确工作,应该在拦截器链中尽早应用此拦截器,以便所有后续的处理都能使用到包装后的请求对象。
全局替换
创建一个 Filter
类,使它包装 HttpServletRequest
为我们自己定义的 BufferedRequestWrapper
:
import com.shen.stock.config.BufferedRequestWrapper;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;@Component
//设置高优先级
@Order(1)
public class CachedBodyFilter implements Filter {@Overridepublic void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;BufferedRequestWrapper cachedBodyHttpServletRequest = new BufferedRequestWrapper(httpServletRequest);filterChain.doFilter(cachedBodyHttpServletRequest, servletResponse);}// Add init() and destroy() methods if needed
}
在这里,使用了 @Component
和 @Order
注解来标记这是一个 Spring 组件,以及定义了它在所有过滤器中的执行顺序,使其优先级高于其他 Filter ,这样就能确保其他的Filter使用的是包装后的请求对象。
确保 CachedBodyFilter
被Spring Boot自动检测并添加到过滤器链中。由于我们使用了 @Component
注解,Spring Boot会自动发现这个过滤器并将其注册为一个Spring Bean。如果你的Spring Boot应用中有自定义的Filter注册逻辑,则需要在那里添加对 CachedBodyFilter
的支持。
现在,任何在过滤器链之后执行的代码(如控制器方法)都将能够多次读取 HttpServletRequest
中的流,因为它将被 CachedBodyHttpServletRequest
包装,它缓存了请求体的内容。
要注意的一点是,如果请求体数据很大或者请求频率很高,这种缓存方法可能会产生性能问题或大量内存占用。确保你的应用场景可以接受这种实现方式。
执行顺序
另外,补充一下过滤器和拦截器的执行顺序问题。
如果你按照上述步骤正确创建并注册了 CachedBodyFilter
类,并将其优先级设置得高于你的自定义拦截器,那么在 Spring Boot 的过滤器链中,自定义的拦截器将会接收到 BufferedRequestWrapper
对象作为请求对象。
Spring Boot 中过滤器(Filter)和拦截器(Interceptor)有不同的执行顺序。Filter
是基于Servlet
标准,而Interceptor
是Spring
的概念。
- Filter: 是在请求进入Servlet之前进行预处理和在响应客户端之前进行后处理的对象。
- Interceptor : 在
DispatcherServlet
(Spring的前端控制器)之后执行,它可以访问执行链中的Controller,并且可以在Controller方法执行之前、之后以及完成渲染视图返回给客户端之后执行操作。
由于Filter在Servlet容器级别工作,它在Interceptor之前执行,所以任何请求都会首先经过Filter然后才到达Interceptor。因此,如果在Filter中将普通的 HttpServletRequest
包装成 BufferedRequestWrapper
,那么随后在Spring的处理流程中——包括Interceptor和Controller中——接收到的都将是已经包装的 BufferedRequestWrapper
。
为了确保CachedBodyFilter
的执行顺序正确,请在@Order
注解或者Filter的注册中明确指定足够低的顺序值(或优先级高)。在Spring中,@Order
注解中值越低,优先级越高。
示例中的@Order(1)
表明CachedBodyFilter
会在大多数其他Filter之前执行,但你可能需要根据你的应用配置进行必要的调整。如果你使用WebSecurityConfigurerAdapter
进行额外的过滤器配置,确保CachedBodyFilter
优先于Spring Security的过滤器链执行。
请记住,如果你使用了第三方库或已有的Filter实现,也需要确保它们的执行顺序是正确的。任何在CachedBodyFilter
之后执行并打算处理请求体的组件都会收到BufferedRequestWrapper
对象,从而能够多次读取请求体内容。
相关文章:
实现HttpServletRequest下多次获取流数据
HttpServletRequest下多次获取流数据 背景示例错误的尝试全局替换执行顺序 背景 众所周知request的输入流只能读取一次,不能重复读取。而在HttpServletRequest中,获取请求体数据的流(通过getInputStream()方法)默认只能被读取一…...
uviewplus在uniapp中的配置使用
版本: "uview-plus": "^3.1.45"在page.json中配置: "easycom": {"autoscan": true,"custom": {"^u--(.*)": "uview-plus/components/u-$1/u-$1.vue","^up-(.*)": "uview-plus/componen…...
C++11 新特性之future和packaged_task
C11 新特性之future #include <iostream> #include <thread> #include <future> #include <chrono>void test(std::promise<int>& probj){std::this_thread::sleep_for(std::chrono::seconds(5));probj.set_value(20); }int main(){std::pr…...
Flutter APP下载更新
由于我做的项目不是放在APP商店(公司内部用)的,一些flutter的第三方库不合适我,我需要用的是从网上下载再安装(从服务下),网上也找了花了我好几天时间。不全又乱,这我自己做一下备份…...

Pinctrl子系统_04_Pinctrl子系统主要数据结构
引言 本节说明Pinctrl子系统中主要的数据结构,对这些数据结构有所了解,也就是对Pinctrl子系统有所了解了。 前面说过,要使用Pinctrl子系统,就需要去配置设备树。 以内核面向对象的思想,设备树可以分为两部分&#x…...

设计模式(十):抽象工厂模式(创建型模式)
Abstract Factory,抽象工厂:提供一个创建一系列相关或相互依赖对 象的接口,而无须指定它们的具体类。 之前写过简单工厂和工厂方法模式(创建型模式),这两种模式比较简单。 简单工厂模式其实不符合开闭原则,即对修改关闭…...
计算机网络概论01
计算机系统基础知识 基本组成 计算机系统由硬件和软件组成。 硬件由五大部分,他们分别是: 运算器 执行算数运算和逻辑运算控制器 控制cpu的工作,决定了计算机运行过程的自动化。包括指令控制逻辑、时序控制逻辑、总线控制逻辑和中断控制逻辑…...

新零售SaaS架构:订单履约系统架构设计(万字图文总结)
什么是订单履约系统? 订单履约系统用来管理从接收客户订单到将商品送达客户手中的全过程。 它连接了上游交易(客户在销售平台下单环)和下游仓储配送(如库存管理、物流配送),确保信息流顺畅、操作协同&…...
Hive SQL 开发指南(三)优化及常见异常
在大数据领域,Hive SQL 是一种常用的查询语言,用于在 Hadoop上进行数据分析和处理。为了确保代码的可读性、维护性和性能,制定一套规范化的 Hive SQL 开发规范至关重要。本文将介绍 Hive SQL 的基础知识,并提供一些规范化的开发指…...

Spring Boot 自动装配的原理!!!
SpringBootApplication SpringBootConfiguration:标识启动类是一个IOC容器的配置类 EnableAutoConfiguration: AutoConfigurationPackage:扫描启动类所在包及子包中所有的组件,生…...
Linux运维_Bash脚本_编译安装Wayland-1.22.0
Linux运维_Bash脚本_编译安装Wayland-1.22.0 Bash (Bourne Again Shell) 是一个解释器,负责处理 Unix 系统命令行上的命令。它是由 Brian Fox 编写的免费软件,并于 1989 年发布的免费软件,作为 Sh (Bourne Shell) 的替代品。 您可以在 Linu…...

Python数字类型
文章目录 Python数字类型1. 数字类型1.1 数字类型概述1.2 整数类型1.3 浮点数类型1.4 复数 2. 数字类型的操作2.1 内置的数值运算操作符2.2 内置的数值运算函数2.3 内置的数字类型转换函数 思考与练习 Python数字类型 1. 数字类型 1.1 数字类型概述 数字是自然界计数活动的抽…...
每天一个数据分析题(一百九十六)
在多元线性回归模型的自变量选择方法中,关于向后回归法和逐步回归法的描述,以下哪些是正确的? A. 向后回归法开始时包含所有自变量,并逐步剔除每个不显著的变量。 B. 逐步回归法结合了向前回归法和向后回归法,可以在…...

华为北向网管NCE开发教程(1)闭坑选接口协议
华为北向网管NCE开发教程(1)闭坑选接口协议 华为北向网管NCE开发教程(2)REST接口开发 华为北向网管NCE开发教程(3)CORBA协议开发 本文一是记录自己开发华为北向网管遇到的坑,二是给需要的人&…...

JavaScript极速入门-综合案例(3)
综合案例 猜数字 预期效果 代码实现 <button type"button" id"reset">重新开始一局游戏</button><br>请输入要猜的数字:<input type"text" id"number"><button type"button" id"button&q…...

RabbitMQ架构详解
文章目录 概述架构详解核心组件虚拟主机(Virtual Host)RabbitMQ 有几种广播类型 概述 RabbitMQ是⼀个高可用的消息中间件,支持多种协议和集群扩展。并且支持消息持久化和镜像队列,适用于对消息可靠性较高的场合 官网https://www.…...

编译内核错误 multiple definition of `yylloc‘
编译内核错误 # make ARCHarm CROSS_COMPILEarm-mix410-linux- uImageHOSTLD scripts/dtc/dtc /usr/bin/ld: scripts/dtc/dtc-parser.tab.o:(.bss0x10): multiple definition of yylloc; scripts/dtc/dtc-lexer.lex.o:(.bss0x0): first defined here collect2: error: ld ret…...

深度学习模型部署(四)常用模型及推理平台评估指标
判断选择什么模型,什么量化方案,什么推理框架,最基础的知识就是如何评估自己的模型以及推理平台。 模型衡量标准 衡量一个模型的最直接标准就是运算速度,但是运算速度是无法计算的,所以定义了一些间接标准来推测模型的…...

【控制台警告】npm WARN EBADENGINE Unsupported engine
今天用webpack下载几个loader依赖,爆出了三个警告,大概的意思就是本地安装的node和npm的版本不是很匹配? 我的解决思路是: 先检查node和npm版本 然后去官网查找版本的对应 靠,官网404 Node.js (nodejs.org) 就找到…...

ArmSoM Rockchip系列产品 通用教程 之 GPIO 使用
1. GPIO简介 GPIO,全称 General-Purpose Input/Output(通用输入输出),是一种在计算机和嵌入式系统中常见的数字输入输出接口。它允许软件控制硬件的数字输入和输出,例如开关、传感器、LED灯等。GPIO通常由一个芯片或…...
挑战杯推荐项目
“人工智能”创意赛 - 智能艺术创作助手:借助大模型技术,开发能根据用户输入的主题、风格等要求,生成绘画、音乐、文学作品等多种形式艺术创作灵感或初稿的应用,帮助艺术家和创意爱好者激发创意、提高创作效率。 - 个性化梦境…...

高频面试之3Zookeeper
高频面试之3Zookeeper 文章目录 高频面试之3Zookeeper3.1 常用命令3.2 选举机制3.3 Zookeeper符合法则中哪两个?3.4 Zookeeper脑裂3.5 Zookeeper用来干嘛了 3.1 常用命令 ls、get、create、delete、deleteall3.2 选举机制 半数机制(过半机制࿰…...

什么是库存周转?如何用进销存系统提高库存周转率?
你可能听说过这样一句话: “利润不是赚出来的,是管出来的。” 尤其是在制造业、批发零售、电商这类“货堆成山”的行业,很多企业看着销售不错,账上却没钱、利润也不见了,一翻库存才发现: 一堆卖不动的旧货…...
使用van-uploader 的UI组件,结合vue2如何实现图片上传组件的封装
以下是基于 vant-ui(适配 Vue2 版本 )实现截图中照片上传预览、删除功能,并封装成可复用组件的完整代码,包含样式和逻辑实现,可直接在 Vue2 项目中使用: 1. 封装的图片上传组件 ImageUploader.vue <te…...

论文浅尝 | 基于判别指令微调生成式大语言模型的知识图谱补全方法(ISWC2024)
笔记整理:刘治强,浙江大学硕士生,研究方向为知识图谱表示学习,大语言模型 论文链接:http://arxiv.org/abs/2407.16127 发表会议:ISWC 2024 1. 动机 传统的知识图谱补全(KGC)模型通过…...

C++ 求圆面积的程序(Program to find area of a circle)
给定半径r,求圆的面积。圆的面积应精确到小数点后5位。 例子: 输入:r 5 输出:78.53982 解释:由于面积 PI * r * r 3.14159265358979323846 * 5 * 5 78.53982,因为我们只保留小数点后 5 位数字。 输…...
全面解析各类VPN技术:GRE、IPsec、L2TP、SSL与MPLS VPN对比
目录 引言 VPN技术概述 GRE VPN 3.1 GRE封装结构 3.2 GRE的应用场景 GRE over IPsec 4.1 GRE over IPsec封装结构 4.2 为什么使用GRE over IPsec? IPsec VPN 5.1 IPsec传输模式(Transport Mode) 5.2 IPsec隧道模式(Tunne…...

Android 之 kotlin 语言学习笔记三(Kotlin-Java 互操作)
参考官方文档:https://developer.android.google.cn/kotlin/interop?hlzh-cn 一、Java(供 Kotlin 使用) 1、不得使用硬关键字 不要使用 Kotlin 的任何硬关键字作为方法的名称 或字段。允许使用 Kotlin 的软关键字、修饰符关键字和特殊标识…...

优选算法第十二讲:队列 + 宽搜 优先级队列
优选算法第十二讲:队列 宽搜 && 优先级队列 1.N叉树的层序遍历2.二叉树的锯齿型层序遍历3.二叉树最大宽度4.在每个树行中找最大值5.优先级队列 -- 最后一块石头的重量6.数据流中的第K大元素7.前K个高频单词8.数据流的中位数 1.N叉树的层序遍历 2.二叉树的锯…...

10-Oracle 23 ai Vector Search 概述和参数
一、Oracle AI Vector Search 概述 企业和个人都在尝试各种AI,使用客户端或是内部自己搭建集成大模型的终端,加速与大型语言模型(LLM)的结合,同时使用检索增强生成(Retrieval Augmented Generation &#…...