实现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通常由一个芯片或…...
从WWDC看苹果产品发展的规律
WWDC 是苹果公司一年一度面向全球开发者的盛会,其主题演讲展现了苹果在产品设计、技术路线、用户体验和生态系统构建上的核心理念与演进脉络。我们借助 ChatGPT Deep Research 工具,对过去十年 WWDC 主题演讲内容进行了系统化分析,形成了这份…...
shell脚本--常见案例
1、自动备份文件或目录 2、批量重命名文件 3、查找并删除指定名称的文件: 4、批量删除文件 5、查找并替换文件内容 6、批量创建文件 7、创建文件夹并移动文件 8、在文件夹中查找文件...
dedecms 织梦自定义表单留言增加ajax验证码功能
增加ajax功能模块,用户不点击提交按钮,只要输入框失去焦点,就会提前提示验证码是否正确。 一,模板上增加验证码 <input name"vdcode"id"vdcode" placeholder"请输入验证码" type"text&quo…...
大语言模型如何处理长文本?常用文本分割技术详解
为什么需要文本分割? 引言:为什么需要文本分割?一、基础文本分割方法1. 按段落分割(Paragraph Splitting)2. 按句子分割(Sentence Splitting)二、高级文本分割策略3. 重叠分割(Sliding Window)4. 递归分割(Recursive Splitting)三、生产级工具推荐5. 使用LangChain的…...
vue3 字体颜色设置的多种方式
在Vue 3中设置字体颜色可以通过多种方式实现,这取决于你是想在组件内部直接设置,还是在CSS/SCSS/LESS等样式文件中定义。以下是几种常见的方法: 1. 内联样式 你可以直接在模板中使用style绑定来设置字体颜色。 <template><div :s…...
反射获取方法和属性
Java反射获取方法 在Java中,反射(Reflection)是一种强大的机制,允许程序在运行时访问和操作类的内部属性和方法。通过反射,可以动态地创建对象、调用方法、改变属性值,这在很多Java框架中如Spring和Hiberna…...
什么?连接服务器也能可视化显示界面?:基于X11 Forwarding + CentOS + MobaXterm实战指南
文章目录 什么是X11?环境准备实战步骤1️⃣ 服务器端配置(CentOS)2️⃣ 客户端配置(MobaXterm)3️⃣ 验证X11 Forwarding4️⃣ 运行自定义GUI程序(Python示例)5️⃣ 成功效果 1. 项目级 build.gradle // 注意:沪江插件已停更,推荐官方兼容方案 buildscript {dependencies {classpath org.aspectj:aspectjtools:1.9.9.1 // AspectJ 工具} } 2. 模块级 build.gradle plu…...
2025年渗透测试面试题总结-腾讯[实习]科恩实验室-安全工程师(题目+回答)
安全领域各种资源,学习文档,以及工具分享、前沿信息分享、POC、EXP分享。不定期分享各种好玩的项目及好用的工具,欢迎关注。 目录 腾讯[实习]科恩实验室-安全工程师 一、网络与协议 1. TCP三次握手 2. SYN扫描原理 3. HTTPS证书机制 二…...
