实现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通常由一个芯片或…...
设计模式和设计原则回顾
设计模式和设计原则回顾 23种设计模式是设计原则的完美体现,设计原则设计原则是设计模式的理论基石, 设计模式 在经典的设计模式分类中(如《设计模式:可复用面向对象软件的基础》一书中),总共有23种设计模式,分为三大类: 一、创建型模式(5种) 1. 单例模式(Sing…...
QMC5883L的驱动
简介 本篇文章的代码已经上传到了github上面,开源代码 作为一个电子罗盘模块,我们可以通过I2C从中获取偏航角yaw,相对于六轴陀螺仪的yaw,qmc5883l几乎不会零飘并且成本较低。 参考资料 QMC5883L磁场传感器驱动 QMC5883L磁力计…...
大数据零基础学习day1之环境准备和大数据初步理解
学习大数据会使用到多台Linux服务器。 一、环境准备 1、VMware 基于VMware构建Linux虚拟机 是大数据从业者或者IT从业者的必备技能之一也是成本低廉的方案 所以VMware虚拟机方案是必须要学习的。 (1)设置网关 打开VMware虚拟机,点击编辑…...
渲染学进阶内容——模型
最近在写模组的时候发现渲染器里面离不开模型的定义,在渲染的第二篇文章中简单的讲解了一下关于模型部分的内容,其实不管是方块还是方块实体,都离不开模型的内容 🧱 一、CubeListBuilder 功能解析 CubeListBuilder 是 Minecraft Java 版模型系统的核心构建器,用于动态创…...
04-初识css
一、css样式引入 1.1.内部样式 <div style"width: 100px;"></div>1.2.外部样式 1.2.1.外部样式1 <style>.aa {width: 100px;} </style> <div class"aa"></div>1.2.2.外部样式2 <!-- rel内表面引入的是style样…...
【python异步多线程】异步多线程爬虫代码示例
claude生成的python多线程、异步代码示例,模拟20个网页的爬取,每个网页假设要0.5-2秒完成。 代码 Python多线程爬虫教程 核心概念 多线程:允许程序同时执行多个任务,提高IO密集型任务(如网络请求)的效率…...
【JavaSE】绘图与事件入门学习笔记
-Java绘图坐标体系 坐标体系-介绍 坐标原点位于左上角,以像素为单位。 在Java坐标系中,第一个是x坐标,表示当前位置为水平方向,距离坐标原点x个像素;第二个是y坐标,表示当前位置为垂直方向,距离坐标原点y个像素。 坐标体系-像素 …...
爬虫基础学习day2
# 爬虫设计领域 工商:企查查、天眼查短视频:抖音、快手、西瓜 ---> 飞瓜电商:京东、淘宝、聚美优品、亚马逊 ---> 分析店铺经营决策标题、排名航空:抓取所有航空公司价格 ---> 去哪儿自媒体:采集自媒体数据进…...
全面解析各类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…...
代理篇12|深入理解 Vite中的Proxy接口代理配置
在前端开发中,常常会遇到 跨域请求接口 的情况。为了解决这个问题,Vite 和 Webpack 都提供了 proxy 代理功能,用于将本地开发请求转发到后端服务器。 什么是代理(proxy)? 代理是在开发过程中,前端项目通过开发服务器,将指定的请求“转发”到真实的后端服务器,从而绕…...
