为什么SpringMVC从HttpRequest拿出数据需要重写getInputStream方法

前言

很多文章解释这个问题的是,说是SpringMVC自己用的InputStream没有实现reset()方法,而JDK大部分自带InputStream实现了这个方法,所以可以让InputStream重复读取,但是在笔者仔细了解后,发现实质上不是reset()的问题。

背景

在进行为公司开发接口验签,想在controller前就拿到数据进行验签时,出现了到达controller拿到的body为空的问题,网络上大部分文章千篇一律,都说了是InputStream的read方法只能读取一次的问题,在笔者debug后,逐层分析,写下这篇文章。

分析过程

1. 在进入controller层前拿出输入流的问题

因为SpringMVC框架中,最后是由DispatcherServlet进行请求分发,再走到相应的RequestMapping对应的方法,所以在filter中拿出数据,就能模拟出controller前拿出数据的方法。
首先写一个Filter,并在启动类上配置@ServletComponentScan注解扫描servlet系列组件

/**
 * @author kiring
 * @date 2020/3/1 1:11
 */
@WebFilter(urlPatterns = "/*")
public class RequestBufferedFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        if(servletRequest instanceof HttpServletRequest){
            byte[] bytes = new byte[1024];
            // InputStream被封装进request中,所以我们也要从request拿到这个inputstream
            servletRequest.getInputStream().read(bytes);
            System.out.println(new String(bytes,"utf-8"));
            filterChain.doFilter("过滤器拿到请求数据:" + servletRequest,servletResponse);
        }
    }

    @Override
    public void destroy() {

    }
}

当我们调用POST请求时,(为什么是POST请求,因为除了文件传输等请求,POST的requestBody数据是以InputStream进行传输的,其他数据可以从url上拿到并封装在request中,通过POST请求可以验证inputstream拿出数据的结果)
控制台会打印过滤器拿到请求数据:xxx,并且请求结果是400。这是便出现问题了,究其原因,我们要断点到源码去看看。

2. 读取数据为空,body为null

我们把断点下到过滤器的 servletRequest.getInputStream().read(bytes);这一句,来看看该请求中实现的HttpServletRequest类是什么,此时来到RequestFacade类中的getInputStream方法
在这里插入图片描述
此时我们在第一行判断上打上断点,并放行到下一段点,也就是让程序再次执行到这里,因为SpringMVC要拿到InputStream的数据,也要拿到request中的inputsteam并读取出来,当我们来到这一行的下一轮断点是,我们可以观察idea工具给我们展示的调用栈。

第一次来到这个断点的调用栈如下图:
在这里插入图片描述
调用这个方法的是我们自己在filter中写的那句代码。

第二次来到这个断点的调用栈:
在这里插入图片描述
可以看到是SevletServerHttpRequest中的方法在调用,这应该是SpringMVC在获取body数据没跑了,单击这一行,去到这段代码中,可以看到方法名就叫做getBody()
在这里插入图片描述
所以此时基本可以判断是准备真正的获取body中的数据了。
此时回到我们的断点,执行下一行,一直找到是哪里在调用这个ServletServerHttpRequest.getBody()方法。
在这里插入图片描述
此时可以发现调用点,AbstractMessageConverterMethodArgumentResolver的内部类EmptyBodyCheckingHttpInputMessage的初始化方法。这段代码很少,直接来解读一下,就能明白,程序首先拿到inputstream,并且判断这个inputstream是否是支持标记的,我们拿到这个inputstram对象,看到其是CoyoteInputStream对象,而这个对象的markSupported是直接继承自InputStream的,也就是false的。所以程序会执行到else代码段。一直往下执行就会发现pushbackInputStream.read()拿到值为-1,也就是数据已经被读完了,这个数据也确实在我们自定义的过滤器中就read读取了,每一次read都会让可读指针后推,直到数据全部被读取,那么再调用read方法都只会是-1,从而导致这里的body被赋值为null(这里PushbackInputStream不过是将inputstream包装了一遍)

所以到这里就破案了,为什么我们不能在Controller层之前将数据从inputstream读出,因为这样会导致我们的body为null,从而报400错误码。
在说解决方案之前,我们先看看假如我们的inputstream是markSupported的,那么也就是这段代码执行的是上面那一段,可以看大这段代码里确实是有调用reset方法,但是需要注意的是,这个reset()方法是在inputstream.read()方法之后才调用的,也就是实际上已经调用read方法,body已经被赋值为null,这个时候才调用reset,这个是没有必要了的。
在这里插入图片描述
所以其本质确实是inputstream只能读取一次的问题,当然,reset方法可以让输入流重新读取,但是这里并不是这个解决方案,同时如果要重写这段代码,那么需要把这个方法的上级等都重写了,这个就太麻烦了。

解决方案

至于解决方案,网上已经有很多文章进行了编码了,因为问题出在inputstream.read这里,主要思路还是让read可以重头读取,因为如果用reset来解决,代码量会非常大,并且我们又发现,每次要拿到inputstream,都会调用HttpServletRequest.getInputStream()这个方法,所以我们只要重写getInputStream()方法,每次调用这个方法时,给一个新的包裹着我们数据的InputStream对象即可,这样每次拿到的都是新的输入流,这里就大概贴下代码:

@WebFilter(urlPatterns = "/*")
public class RequestBufferedFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        if(servletRequest instanceof HttpServletRequest){
            servletRequest = new BufferedHttpRequest((HttpServletRequest)servletRequest);
            filterChain.doFilter(servletRequest,servletResponse);
        }
    }

    @Override
    public void destroy() {

    }
}

/**
 * 自定义一个HttpServletRequest,主要是为了将request的数据取出,避免request只能用一次的问题
 * @author kiring
 * @date 2020/2/29 17:20
 */
public class BufferedHttpRequest extends HttpServletRequestWrapper {
    private byte[] buffer;
    private ByteArrayInputStream byteArrayInputStream;
    public BufferedHttpRequest(HttpServletRequest request) {
        super(request);
        // 将buffer填充
        try {
            System.out.println("从request拿出inputstream");
            // 使用工具类将inputsteam的内容拿到并转换为byte数组
            buffer = IOUtils.toByteArray(request.getInputStream());
            byteArrayInputStream = new ByteArrayInputStream(buffer);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 重写getInputStream方法
     * @return
     * @throws IOException
     */
    @Override
    public ServletInputStream getInputStream() throws IOException {
        System.out.println("将byte数组封装进BufferedServletInputStream");
        // 这里可以做我们的数据校验逻辑,比如处理XSS攻击
        System.out.println("getInputStream方法中处理xss攻击");
        return new BufferedServletInputStream(buffer);	// 每次都返回一个新的ServletInputStream实现类
//        return new BufferedServletInputStream(byteArrayInputStream); // 使用这种方式也可以,但是这样byteinputstream是单例的
    }

    class BufferedServletInputStream extends ServletInputStream {
        private ByteArrayInputStream byteArrayInputStream;

        public BufferedServletInputStream(byte[] buffer){
            this.byteArrayInputStream = new ByteArrayInputStream(buffer);
        }

        public BufferedServletInputStream (ByteArrayInputStream inputStream){
            this.byteArrayInputStream = inputStream;
        }

        @Override
        public boolean isFinished() {
            return false;
        }

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

        @Override
        public void setReadListener(ReadListener readListener) {

        }

        /**
         * 最主要重写此方法,用ByteArrayInputStream去接收
         * @return
         * @throws IOException
         */
        @Override
        public int read() throws IOException {
            return byteArrayInputStream.read();
        }

        @Override
        public int read(byte[] b, int off, int len) throws IOException {
            return byteArrayInputStream.read(b, off, len);
        }

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

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


}

注意看我上面标红的一句话:这样每次拿到的都是新的输入流
其实上面的代码是有问题的,因为每次调用ServletRequest的getInputStream()方法,都会创建一个新的输入流,众所周知,输入流是要关闭的,不然很可能会造成内存泄露,从而导致OOM等情况。但是这里我们并没有关闭输入流。尤其在SpringMVC拿取InputStream方法的EmptyBodyCheckingHttpInputMessage的这段代码中,经过断点跟踪发现,后面也是没有将流关闭的,所以我们就要自己重新定义逻辑,这里笔者提供的思路是,在我们自定义的BufferedHttpRequest中,提供一个线程安全的成员变量ConcurrentLinkedQueue,在每次getInputSteam时,都将新创建的ServletInputStream对象装进这个队列中,最后在filter的filterChain.doFilter(servletRequest,servletResponse);后将队列拿出并比遍历关闭里面的inpustream。

画外

笔者突然想到Spring有没有帮我们去关闭流呢。
在经过漫长的debug后,笔者找到了结果。这里记录一下过程:
其实笔者寻找的思路是:一层一层debug,并根据方法名猜测是否可能是是关闭流,同时使用inputStream.read()方法去确认能否拿到数据,确认是否已经关闭流。

  1. 首先,执行完自定义filter的filterChain.doFilter(servletRequest,servletResponse)后,也就意味着servlet的部分执行完了,F6到下一个步骤,这时来到ApplicationFilterChain这个类,同时,我们也能知道filter是被ApplicationFilterChain类中的internalDoFilter方法的filter.doFilter(request, response, this)所调用的。
    需要注意的是,这里的ServletRequest并不是我们自己的BufferedHttpRequest,而是原来的RequestFacad,这里的原因就是很多面试题喜欢考的引用传递的知识点。
    在我们自定义的Filter中,虽然我们替换了request,但是我们在做的事情是将这个栈中的servletRequest所代表的引用替换成另一个引用了,而原来引用所指的RequestFacad对象还是被上一个栈帧的servletRequest所引用着
  2. debug来到RequestContextFilter的doFilterInternal方法的finally代码块,其中一句是resetContextHolders(),这名字看着有可能是,点进去后发现不过是remove了本地线程变量。

在这里插入图片描述
3. 最终,在CoyoteAdapter类中找到了关闭流的方法。在调用完response.finishResponse()方法后,就会将RequestFacad对象的inputstream关闭
在这里插入图片描述

总结

  1. 因为inputstream的read方法执行完后,如果没有把指针复位,那么之后怎么读都是-1,这将导致如果我们先于controller的参数被赋值前将输入流的数据取出,将导致controller拿到的参数为空,从而报400
  2. 解决read的主要思路就是重写ServeletRequest的getInputStream方法,使得每次调用getInputstream拿到的是新的对象,避免重复read的问题
  3. 因为每次都会拿到新的InputStream对象,所以要想办法将这些流在用完之后关闭
  4. 在filter中,执行完chain.doFilter()方法,就相当于执行完业务逻辑了,弹出filter后,后面受到操作的还是RequestFacad对象,而这个对象以及里面的输入流由Spring框架负责管理及关闭,我们不需要关心
发布了25 篇原创文章 · 获赞 9 · 访问量 6630

猜你喜欢

转载自blog.csdn.net/qq_40233503/article/details/104592362