阅读 518

Spring Boot中HttpServletRequest输入流只能读取一次问题

这是我参与8月更文挑战的第5天,活动详情查看:8月更文挑战

问题简述

在安全性要求比较高的接口上,项目一般会在过滤器做一些验签的工作。对于携带JSON数据的请求,我们需要调用HttpServletRequest输入流来获取JSON数据从而解析参数。但是采用该方法后,在Controller层使用@RequestBody解析参数将会抛出异常java.lang.IllegalStateException

问题原因

无论是我们自己解析JSON,还是使用@RequestBody进行解析,本质上都是对HttpServletRequest以流的形式进行读取再解析,而HttpServletRequestgetInputStream()getReader()都是继承ServletRequest接口的方法,都只能读取一次,所以导致异常的出现。

以方法 getInputStream()为例,方法返回一个 ServletInputStream对象,该对象由HttpServletRequest的实现类Request的成员变量inputBuffer所得, getReader()同样是由inputBuffer所得。所以 getInputStream()getReader()的方法只能使用其中一种。Request源码注释对此也有所说明。

    /**
     * The associated input buffer.
     */
    protected final InputBuffer inputBuffer = new InputBuffer();
​
    
    /**
     * @return the servlet input stream for this Request.  The default
     * implementation returns a servlet input stream created by
     * <code>createInputStream()</code>.
     *
     * @exception IllegalStateException if <code>getReader()</code> has
     *  already been called for this request
     * @exception IOException if an input/output error occurs
     */
    @Override
    public ServletInputStream getInputStream() throws IOException {
​
        if (usingReader) {
            throw new IllegalStateException(sm.getString("coyoteRequest.getInputStream.ise"));
        }
​
        usingInputStream = true;
        if (inputStream == null) {
            inputStream = new CoyoteInputStream(inputBuffer);
        }
        return inputStream;
​
    }
​
      /**
     * @return the servlet input stream for this Request.  The default
     * implementation returns a servlet input stream created by
     * <code>createInputStream()</code>.
     *
     * @exception IllegalStateException if <code>getReader()</code> has
     *  already been called for this request
     * @exception IOException if an input/output error occurs
     */
    @Override
    public ServletInputStream getInputStream() throws IOException {
​
        if (usingReader) {
            throw new IllegalStateException(sm.getString("coyoteRequest.getInputStream.ise"));
        }
​
        usingInputStream = true;
        if (inputStream == null) {
            inputStream = new CoyoteInputStream(inputBuffer);
        }
        return inputStream;
​
    }
复制代码

而对于流只能读取一次的问题,当我们调用getInputStream()方法获取输入流时得到的是一个InputStream对象,而实际类型是ServletInputStream,它继承于InputStream

InputStreamread()方法内部有一个postion,标志当前流被读取到的位置,每读取一次,该标志就会移动一次,如果读到最后,read()会返回-1,表示已经读取完了。如果想要重新读取则需要调用reset()方法,position就会移动到上次调用mark的位置,mark默认是0,所以就能从头再读了。调用reset()方法的前提是已经重写了reset()方法,当然能否reset也是有条件的,它取决于markSupported()方法是否返回true。而InputStream默认不实现reset(),并且markSupported()默认也是返回false。

解决方案

解决的核心思路就是对HttpServletRequest输入流进行备份。具体做法就是借助包装类对HttpServletRequest进行功能上的增强,将请求体中的流copy一份,覆写getInputStream()getReader()方法供外部使用。每次调用覆写后的getInputStream()方法都是从复制出来的二进制数组中进行获取,这个二进制数组在对象存在期间一直存在,这样就实现了流的重复读取。

具体代码实现如下:

public class RequestWrapper extends HttpServletRequestWrapper {
    //保存流
    private byte[] requestBody = null;
 
    public RequestWrapper(HttpServletRequest request) throws IOException {
        super(request);
        requestBody = StreamUtils.copyToByteArray(request.getInputStream());
    }
 
    @Override
    public ServletInputStream getInputStream() throws IOException {
 
        final ByteArrayInputStream bais = new ByteArrayInputStream(requestBody);
 
        return new ServletInputStream() {
 
            @Override
            public int read() throws IOException {
                return bais.read();
            }
 
            @Override
            public boolean isFinished() {
                return false;
            }
 
            @Override
            public boolean isReady() {
                return false;
            }
 
            @Override
            public void setReadListener(ReadListener readListener) {
 
            }
        };
    }
 
    @Override
    public BufferedReader getReader() throws IOException{
        return new BufferedReader(new InputStreamReader(getInputStream()));
    }
}
复制代码
文章分类
后端