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

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

 

posted @   思凡念真  阅读(961)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
历史上的今天:
2021-10-15 springboot 时间戳和 数据库时间相差12~13个小时
2021-10-15 MySQL出现 Waiting for table metadata lock 问题
点击右上角即可分享
微信分享提示