SpringBoot项目中HTTP请求体只能读一次?试试这方案

news/2024/11/19 12:43:25/文章来源:https://www.cnblogs.com/itartisans/p/18347861

问题描述

在基于Spring开发Java项目时,可能需要重复读取HTTP请求体中的数据,例如使用拦截器打印入参信息等,但当我们重复调用getInputStream()或者getReader()时,通常会遇到类似以下的错误信息:
image
大体的意思是当前request的getInputStream()已经被调用过了。那为什么会出现这个问题呢?

原因分析

主要原因有两个,一是Java自身的设计中,InputStream作为数据管道本身只支持读取一次,如果要支持重复读取的话就需要重新初始化;二是Servlet容器中Request的实现问题,我们以默认的Tomcat为例,可以发现在Request有两个boolean类型的属性,分别是usingReader和usingInputStream,当调用getInputStream()或getReader()时会分别检查两个属性的值,并在执行后将对应的属性设置为true,如果在检查时变量的值已经为true了,那么就会报出以上错误信息。
image

解决方案

不太可行的方案:简单粗暴的反射机制

涉及到变量的修改,我们首先想到的就是有没有提供方法进行修改,不过可惜的是usingReader和usingInputStream并未提供,所以想要在使用过程中修改这两个属性估计只能靠反射了,在使用过程中每次调用后通过反射将usingReader和usingInputStream设置为false,每次根据读取出的内容把数据流初始化回去,理论上就可以再次读取了。

首先说反射机制本身就是通过破坏类的封装来实现动态修改的,有点过于粗暴了,其次也是主要原因,我们只能针对我们自己实现的代码进行处理,框架本身如果调用getInputStream()和getReader()的话,我们就没法通过这个办法干预了,所以这个方案在给予Spring的Web项目中并不可行。

理论上可行的方案:HttpServletRequest接口

HttpServletRequest是一个接口,理论上我们只需要创建一个实现类就可以自定义getInputStream()和getReader()的行为,自然也就能解决RequestBody不能重复读取的问题,但这个方案的问题在于HttpServletRequest有70个方法,而我们只需要修改其中两个而已,通过这种方式去解决有点得不偿失。

部分场景可行的方案:ContentCachingRequestWrapper

Spring本身提供了一个Request包装类来处理重复读取的问题,即ContentCachingRequestWrapper,其实现思路就是在读取RequestBody时将内存缓存到它内部的一个字节流中,后续读取可以通过调用getContentAsString()或getContentAsByteArray()获取到缓存下来的内容。

之所以说这个方案是部分场景可行主要是两个方面,一是ContentCachingRequestWrapper没有重写getInputStream()和getReader()方法,所以框架中使用这两个方法的地方依然获取不到缓存下来的内容,仅支持自定义的业务逻辑;第二点和第一点有所关联,因为其没有修改getInputStream()和getReader()方法,所以我们在使用时只能在使用RequestBody注解后使用ContentCachingRequestWrapper,否则就会出现RequestBody注解修饰的参数无法正常读取请求体的问题,也就限定了它的使用范围如下图所示:
image

如果仅需要在业务代码后再次读取请求体内容,那么使用ContentCachingRequestWrapper也足以满足需求,具体使用方法请参考下一节的说明。

目前的最佳实践:继承HttpServletRequestWrapper

之前我们提到实现HttpServletRequest需要实现70个方法,所以不太可能自行实现,这个方案算是进阶版本,继承HttpServletRequest的实现类,之后再自定义我们需要修改的两个方法。

HttpServletRequest作为一个接口,肯定会有其实现去支撑它的业务功能,因为Servlet容器的选择较多,我们也不能使用某一方提供的实现,所以选择的范围也就被限制到了Java EE(现在叫Jakarta EE)标准范围内,通过查看HttpServletRequest的实现,可以发现在标准内提供了一个包装类:HttpServletRequestWrapper,我们的方案也是围绕它展开。

思路简述

  1. 自定义子类,继承HttpServletRequestWrapper,在子类的构造方法中将RequestBody缓存到自定义的属性中。
  2. 自定义getInputStream()和getReader()的业务逻辑,不再校验usingReader和usingInputStream,且在调用时读取缓存下来的内容。
  3. 自定义Filter,将默认的HttpServletRequest替换为自定义的包装类。

代码展示

  1. 继承HttpServletRequestWrapper,实现子类CustomRequestWrapper,并自定义getInputStream()和getReader()的业务逻辑
// 1.继承HttpServletRequestWrapper
public class CustomRequestWrapper extends HttpServletRequestWrapper {// 2.定义final属性,用于缓存请求体内容private final byte[] content;public CustomRequestWrapper(HttpServletRequest request) throws IOException {super(request);// 3.构造方法中将请求体内容缓存到内部属性中this.content = StreamUtils.copyToByteArray(request.getInputStream());}// 4.重新getInputStream()@Overridepublic ServletInputStream getInputStream() {// 5.将缓存下来的内容转换为字节流final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(content);return new ServletInputStream() {@Overridepublic boolean isFinished() {return false;}@Overridepublic boolean isReady() {return false;}@Overridepublic void setReadListener(ReadListener listener) {}@Overridepublic int read() {// 6.读取时读取第5步初始化的字节流return byteArrayInputStream.read();}};}// 7.重写getReader()方法,这里复用getInputStream()的逻辑@Overridepublic BufferedReader getReader() {return new BufferedReader(new InputStreamReader(getInputStream()));}
}
  1. 自定义Filter将默认的HttpServletRequest替换为自定义的CustomRequestWrapper
// 1.实现Filter接口,此处也可以选择继承HttpFilter
public class RequestWrapperFilter implements Filter {// 2. 重写或实现doFilter方法@Overridepublic void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {// 3.此处判断是为了缩小影响范围,本身CustomRequestWrapper只是针对HttpServletRequest,不进行判断可能会影响其他类型的请求if (request instanceof HttpServletRequest) {// 4.将默认的HttpServletRequest转换为自定义的CustomRequestWrapperCustomRequestWrapper requestWrapper = new CustomRequestWrapper((HttpServletRequest) request);// 5.将转换后的request传递至调用链中chain.doFilter(requestWrapper, response);} else {chain.doFilter(request, response);}}
}
  1. 将Filter注册到Spring容器,这一步可以通过多种方式执行,这里采用比较传统但比较灵活的Bean方式注册,如果图方便可以通过ServletComponentScan注解+ WebFilter注解的方式。
/*** 过滤器配置,支持第三方过滤器*/
@Configuration
public class FilterConfigure {/*** 请求体封装* @return*/@Beanpublic FilterRegistrationBean<RequestWrapperFilter> filterRegistrationBean(){FilterRegistrationBean<RequestWrapperFilter> bean = new FilterRegistrationBean<>();bean.setFilter(new RequestWrapperFilter());bean.addUrlPatterns("/*");return bean;}
}

至此我们就可以在项目中重复读取请求体了,如果选择使用Spring提供的ContentCachingRequestWrapper,那么在Filter中将CustomRequestWrapper替换为ContentCachingRequestWrapper即可,不过需要注意在上一节提到的可用范围较小的问题。

文章内的代码可以参考 https://gitee.com/itartisans/itartisans-framework,这是我开源的一个SpringBoot项目脚手架,我会不定期加入一些通用功能,欢迎关注。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/779437.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

类与类之间的基本关系

类与类之间的基本关系 类与类之间的六种关系 一、继承关系继承指的是一个类(称为子类、子接口)继承另外的一个类(称为父类、父接口)的功能,并可以增加它自己的新功能的能力。在Java中继承关系通过关键字extends明确标识,在设计时一般没有争议性。在UML类图设计中,继承用…

js 将十进制字符串转换成4字节的字节数组

函数function convertToHexArrays(input) {// 通过制表符分割输入字符串const numbers = input.split(\t);// 用于存储结果的数组const result = [];for (let num of numbers) {// 将字符串转换为数字const value = parseInt(num);// 创建一个 4 字节的 ArrayBufferconst buffe…

超异构计算杂谈

超异构计算杂谈 在这一节中要从更远的视角来看看计算机架构发展的黄金 10 年,主要将围绕异构计算和超异构来展开。在开始具体内容前,非常推荐观看以下两个视频:计算机架构的新黄金时代:A New Golden Age for Computer Architecture 编译器的黄金时代:The Golden Age of Co…

转发wsa和安卓模拟器网络

adb连接上设备后, 执行 执行端口转发 adb forward tcp:6789 tcp:888`就可以了, 把设备的8888端口转发到本机6789, 本机postman之类直接访问 127.0.0.1:6789即可 其他笔记:连接wsa: adb connect 127.0.0.1:58526 连接安卓模拟器: adb connect 127.0.0.1:58526 安装app adb -s 1…

09HTML+CSS

完成小兔鲜儿商城界面1 <!DOCTYPE html>2 <html lang="en">3 4 <head>5 <meta charset="UTF-8">6 <meta name="viewport" content="width=device-width, initial-scale=1.0">7 <!-- 提升…

macos上安装esp-idf v4.2版本

参考 https://docs.espressif.com/projects/esp-idf/en/release-v5.0/esp32/get-started/linux-macos-setup.html 安装 Prerequisites brew install cmake ninja dfu-utilgit下载idf 4.2版本并安装 git clone -b release/v4.2 --recursive https://github.com/espressif/esp-id…

VS设置 LLVM-Clang 编译器进行编译C++项目

在VS中默认的C++编译器一般为 MSVC 编译器,可以根据自己的需要将其设置为 LLVM-Clang 编译器。主要有两种方案: 1)直接使用 Visual Studio Installer来自动下载对应的 Clang 编译器和构建工具,后续无需再进行配置,便可直接使用。 2)使用自己编译或者单独下载的 LLVM-Clan…

记一次微信聊天记录导出工具的折腾

小记微信聊天记录选择性导出工具: WechatExporter 的使用目前的微信app(iOS端 v8.0.46)聊天记录中, 允许用户基于图片/视频进行筛选 单个或者少量保存到本机没啥问题 但是如果你量很大, 不好意思, 有批量操作功能, 但是我不支持全选, 因为我批量操作单次最多只支持 9 个文件 就…

《加缪情书集》-1944

用直白的话语,短句子,热烈表达感情。写很具体的细节打动人全文背诵,谢谢 【PS:加缪和玛丽亚这种不被世俗赞同的感情是不是可以直接拿来用...?】分手后

当你用bing搜索张云杰时

首页会跳出:总结一下:(张杰自称)张云杰现实中是完完全全的废物。打开张云杰相关的图片可以看到:只能说气质相符!

洛谷P3842 线段——题解

洛谷P3842题解传送锚点摸鱼环节 [TJOI2007] 线段 题目描述 在一个 \(n \times n\) 的平面上,在每一行中有一条线段,第 \(i\) 行的线段的左端点是\((i, L_{i})\),右端点是\((i, R_{i})\)。 你从 \((1,1)\) 点出发,要求沿途走过所有的线段,最终到达 \((n,n)\) 点,且所走的路…

IDEA中开启注解处理器时的问题

项目构建脚本:gradle 项目中使用mapstruct(要求开启注解处理器) 使用IDEA构建和运行:在上面的条件下,运行时会报错。解决办法参考: https://blog.csdn.net/kq1983/article/details/130740712 https://blog.csdn.net/qq_33240556/article/details/137046631