首页 > 其他分享 >过滤器拦截器拦截了request后,controller的@RequestBody 无法获取request内容,报错 Required request body is missing 的根源

过滤器拦截器拦截了request后,controller的@RequestBody 无法获取request内容,报错 Required request body is missing 的根源

时间:2024-10-15 11:03:01浏览次数:1  
标签:body 拦截器 read request 报错 Override import public

SpringMVC的拦截器、过滤器、Controller之间的关系

 


众所周知所有的post请求中的body参数是已流形式存在的,而流数据只能读取一次(为啥看这里),如果在拦截器和过滤器中需要对post参数进行处理的话,就会报Required request body is missing 异常。既然知道原因,那只要能将流保存起来就可以解决问题。

怎样让参数流能多次读取? 我在网上找到的方案是使用HttpServletRequestWrapper包装HttpServletRequest。


但是实际使用,对于controller参数中使用@RequestBody,仍旧会导致相同的问题。

类似于

 而他的缓存(caching)性其实表现在这里,举个例子

 这里getContentAsByteArray就是可以重复读取的一个方法,其底层就是ByteArrayOutputStream

 

 

既然它可以被重复读取那么为什么又会因为无法重复读取而抛出异常呢?

为什么ConContentCachingRequestWrapper无法解决的重复读取问题

我们先故意触发这个异常,看看异常堆栈信息

 

再转到对应方法 

 

 

对于我们这个需要反序列化的参数,含有RequestBody注解,且使用required的默认值true,且不为optional,所以这个判断函数为true,所以抛出这个异常的原因在于arg为null,进而问题出在readWithMessageConverters方法上。

那么我们再去寻找什么情况下这个方法会返回null(其实不是这里返回的null,这里是启发我向上找的原因)

 

那么我们就把断点放到这里(有注释的AbstractMessageConverterMethodArgumentResolver类202行),再此执行

 

 

 发现其实它比较的是内部的body是否为空,我们再来看这个EmptyBodyCheckingHttpInputMessage类到底在哪里初始化的body这个变量

 

原来是在构造函数里面,我们再在蓝色高亮处打个断点再重新试试

结合idea提示的类型信息和源码,也就说如果body不为空,那么其中含有的inputstream类就要支持(mark,reset)或者还未读取完毕。

我们再回看ContentCachingRequestWrapper这个类中的ContentCachingInputStream类,首先这个时候因为我们故意在拦截器消费了这个流,所以我们要看看它支不支持(mark,reset)功能

 

所以说不支持

那么我们再看else分支的这个PushbackInputStream和他的read方法到底何方神圣

 

 因为单参数初始化的后的pos =1 buf数组长度 =1,即返回值为super.read()的返回值

 即传入的那个inputStream调用read()方法

 

 

结论

你看问题就出在这里。还是调用的ServletInputStream的read,因为直接原请求流被我们消费了,所以返回值为-1

再走到了else中进行处理空body

 

 

在这个方法中返回了null(因为第一个参数为null),这就是为什么这个方法返回为null的真正原因

进而我们上面提到的这个if为true的,也就因此抛出了这个我们熟悉的

Required request body is missing异常提示

 

归根结底是因为ContentCachingRequestWrapper的内部类 ContentCachingInputStream的read方法还是由ServletInputStream去执行read方法的

解决方案

我来提供一个简单的解决方法

我们先来复习一下我们需要什么样的InputStream?支持reset,mark

那么jdk有没有这样一个呢?有!ByteArrayInputStream,这个是个实现InputStream的假装成流的字符数组缓存。

设计思路如下,由这个包装类先行消费输入流做成比特数组储存起来,通过getInputStream提供一个

ServletInputStream的实现类用于代理ByteArrayInputStream进行操作

ByteArrayInputStream只是保留了一个引用,同时这个body的字符数组是只读的,也不用担心线程安全问题,更不用担心ByteArrayInputStream的关闭问题(毕竟不是真正的流)

import lombok.SneakyThrows;
import org.springframework.util.StreamUtils;

import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.*;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;

public class RepeatedlyHttpServletRequestWrapper extends HttpServletRequestWrapper {

    /**
     * 缓存下来的HTTP body
     */
    private byte[] body;
    private Charset charset;

    @SneakyThrows
    public RepeatedlyHttpServletRequestWrapper(HttpServletRequest request,Charset charset) {
        super(request);
        try {
            body = StreamUtils.copyToByteArray(request.getInputStream());
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        this.charset = charset;
    }

    public RepeatedlyHttpServletRequestWrapper(HttpServletRequest request) {
        this(request, StandardCharsets.UTF_8);
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {
        return new RepeatableInputStream();
    }

    @Override
    public BufferedReader getReader() throws IOException {
        return new BufferedReader(new InputStreamReader(getInputStream(),charset));
    }

    private class RepeatableInputStream extends ServletInputStream{
        private ByteArrayInputStream byteArrayInputStream;

        @Override
        public synchronized void reset() throws IOException {
            byteArrayInputStream.reset();
        }

        @Override
        public synchronized void mark(int readlimit) {
            byteArrayInputStream.mark(readlimit);
        }

        public RepeatableInputStream() {
            byteArrayInputStream = new ByteArrayInputStream(body);
        }

        @Override
        public boolean isFinished() {
            return byteArrayInputStream.available() == 0;
        }

        @Override
        public boolean isReady() {
            return true;
        }

        @Override
        public void setReadListener(ReadListener readListener) {
            throw new UnsupportedOperationException("不支持监听");
        }

        @Override
        public int read() throws IOException {
            return byteArrayInputStream.read();
        }

        @Override
        public boolean markSupported() {
            return byteArrayInputStream.markSupported();
        }
    }

}

我们再来请求一次

这次可以了

@RequestMapping(value = "/api/test",method = { RequestMethod.POST, RequestMethod.GET })
    public Object test(HttpServletRequest request,@RequestBody Param param){
       System.out.println(parm.getName); 
}

 

参考:

https://juejin.cn/post/6858645006635401224#%E7%BB%93%E8%AE%BA

https://www.cnblogs.com/qixingchao/p/18262499

https://www.jianshu.com/p/9d3e9b92d535

 

标签:body,拦截器,read,request,报错,Override,import,public
From: https://www.cnblogs.com/sfnz/p/18466992

相关文章

  • electron-builder 在打包universal的时候报错 both x64 and arm64 builds and not cov
    ⨯Detectedfile"Contents/Resources/src/files/adbtool/mac/adb"that'sthesameinbothx64andarm64buildsandnotcoveredbythex64ArchFilesrule:"undefined"failedTask=buildstackTrace=Error:Detectedfile"Contents/Re......
  • 【QAMISRA】解决导入commands.json时报错问题
    1、文档目标解决导入commands.json时报错“Couldnotobtainsystem-wideincludesanddefines”的问题。2、问题场景客户导入commands.json时报错“Couldnotobtainsystem-wideincludesanddefines”。3、软硬件环境1、软件版本: QA-MISRA23.042、机器环境......
  • datframe格式 填充apply 报错incompatible index of inserted column with frame inde
    源代码importpandasaspddf=pd.read_csv("Titanic.csv")#对Sex分组,用各组乘客的平均年龄填充各组中的缺失年龄df_cleaned['Age']=df_cleaned.groupby('Sex')['Age'].apply(lambdax:x.fillna(x.mean()))报错信息incompatibleindexofinsertedcol......
  • 解决 Maven 插件报错:The plugin org.codehaus.mojo:flatten-maven-plugin:1.5.0 requi
    检查Maven版本:首先,确认当前使用的Maven版本是否与插件要求的版本一致。可以通过在命令行中输入 mvn-v 来查看当前Maven的版本信息。升级或降级Maven版本:如果当前Maven版本过低,需要升级到插件要求的版本;如果过高,可能需要降级。升级或降级Maven可以参考Maven的官......
  • node打包报错:ERROR in xxxx.js Module not found: Error: Can’t resolve xxxx in ‘
    原文链接:node打包报错:ERRORinxxxx.jsModulenotfound:Error:Can’tresolvexxxxin‘xxx’errorCommandfailedwithexitcode1.–每天进步一点点(longkui.site) 0.背景anguar项目。分为主包和子包,子包推送到npm私有仓库中,然后主包在packjson中引入子包的版本......
  • git报错:unsafe repository(‘D/xxx/xxx/xx’) To add an exception for this director
    原文链接:git报错:unsaferepository(‘D/xxx/xxx/xx’)Toaddanexceptionforthisdirectory…–每天进步一点点(longkui.site)0.背景电脑重装系统以后,IDEA重新跑起来,然后git拉代码,报错:unsaferepository(‘D/xxx/xxx/xx’)Toaddanexceptionforthisdirectory,call......
  • javaweb实现下载功能报错sockettimeout
    javaweb压缩zip包下载,并响应头里面指定文件大小在JavaWeb应用程序中,如果你想要创建一个ZIP文件并通过HTTP响应提供下载,并且希望在响应头中指定文件大小,你可以先将文件写入到一个临时的ByteArrayOutputStream中,这样你就可以计算出压缩后的文件大小。然后,你可以将这个字节......
  • problemmatcher 引用无效: $esbuild-watch vscode插件报错
    vscode插件esbuild类型提示报错最近在上手开发vscode插件,demo阶段就遇到了一个小问题。搜索引擎没有特别好的回答,记录一下,以供查漏补缺。vscode插件开发做为一统前端的开发插件,vscode+其丰富的插件能力,共同构建了欣欣向荣的vscode插件。在团队效率方面,也是不可或缺的利器......
  • Windows11下安装wsl报错:无法解析服务器的名称或地址
    问题描述之前在自己的笔记本电脑(Windows10)上下载安装WSL很顺利,具体教程见前面的文章,但是在新电脑(Windows11)上下载就报错:无法解析服务器的名称或地址,按照网上说的两个解决方案:修改 DNS 为手动114.114.114.114;查询 raw.githubusercontent.com 这个域名对应的能ping通的ip,......
  • mysql8: 主从复制从库报错时,找到主库上的sql
    一,从库机器上:从库mysql日志中的报错信息给出了日志文件名和end_log_pos2024-10-12T09:41:23.761203Z414[ERROR][MY-013146][Repl]ReplicaSQLforchannel'':Worker1failedexecutingtransaction'ANONYMOUS'atsourcelogmysql-master-bin.000002,end_log_pos......