ServletRequest中的getReader() 和getInputStream()只能调用一次的问题探究

起因:

在项目中做对外接口API防重设计的时候,发现项目有个类 XXXRequestWrapper ,这个类继承 HttpServletRequestWrapper 并重写了 getInputStream()getReader()方法,代码示例如下:

public class XXXRequestWrapper extends HttpServletRequestWrapper{

    //省略constrcutor以及自定义filed

    @Override
    public BufferedReader getReader() {
        return new BufferedReader(new InputStreamReader(getInputStream(), StandardCharsets.UTF_8));
  }

   @Override
    public ServletInputStream getInputStream() {
        //省略实现代码

}

项目中自定义的Filter中都会把 servletRequest转化为 该warpper,让我十分不解,一度认为这是脱裤子放屁,多此一举的行为。然鹅,最后被打脸的确是我自己……

一探:

经过搜索后得知,因为ServletRequest中的getInputStream()/getReader()只能被调用一次 而一个项目中肯定存在多处需要调用该方法获取流中数据的场景,所以需要自定义一个XXXWrapper类 并重写getReader()getInputStream()方法,将流中数据保存并向下传递(其他Filter).伪代码如下:

public class CustomizeFilter implements Filter{
    @Override  
    public void doFilter(ServletRequest request, ServletResponse response,  
            FilterChain chain) throws IOException, ServletException {  

        ServletRequest requestWrapper = null;    
        if(request instanceof HttpServletRequest) {  
            HttpServletRequest httpServletRequest = (HttpServletRequest) request;
            requestWrapper = new XXXWrapper(request)
        }

        if(requestWrapper == null) {    
            chain.doFilter(request, response);    
        } else {    
            chain.doFilter(requestWrapper, response);  //将requet 替换requestWrapper向下传递
        }    
    }  
}

此种做法可以避免之前提到的流只能被读取一次的问题。但是仍然有个问题困扰着我,为什么只能被读取一次?这个问题,我搜了下,其实也有不少人研究过,有些给的答案很形象,你可以把流中的数据当作一瓢水,你调用getInputStream方法就相当于从瓢中取水,取了一次以后,自然就没有了。坦白说,看到这个解释的时候,我恍然大悟好像明白了,又好像什么都没明白。因为,这个答案没有从根本上说明答案。

二探:

ServletRequest中的getInputStream()为什么只能调用一次? 这个问题还是让我有些疑惑,我继续寻找答案,终于,一番努力(百度)下 ,找到了想要的答案。

其实这个问题最终归于I/O的读取,我们可以先看下ServletRequest的getInputStream()是怎样的,源码如下:

    /**
     * 省略
     */
    public ServletInputStream getInputStream() throws IOException;

其返回值为ServletInputStreamServletInputStream又是怎样的呢?

public abstract class ServletInputStream extends InputStream {

    /**
     * Does nothing, because this is an abstract class.
     */
    protected ServletInputStream() {
        // NOOP
    }
    //省略其余部分
}

由此我们可以看到 ServletInputStream其实是 InputStream的子类,而且该方法的实现上来看,

getInputStream() 方法 最终是由RequestFacade类实现的,其实现源码为:

    @Override
    public ServletInputStream getInputStream() throws IOException {

        if (request == null) {
            throw new IllegalStateException(
                            sm.getString("requestFacade.nullRequest"));
        }

        return request.getInputStream();
    }

其中的 request.getInputStream() 则是由 Request实现的:

    @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;

    }

再往下探究其实已经没必要了,因为ServletRequestgetInputStream()返回的为InputStream的子类,而我们在读取该流的时候(即在Filter中获取request中的参数时),其使用的是InputStreamread方法.

对于InputStreamread方法:

  /**
     * Reads up to <code>len</code> bytes of data from the input stream into
     * an array of bytes.  An attempt is made to read as many as
     * <code>len</code> bytes, but a smaller number may be read.
     * The number of bytes actually read is returned as an integer.
     *
     * <p> This method blocks until input data is available, end of file is
     * detected, or an exception is thrown.
     *
     * <p> If <code>len</code> is zero, then no bytes are read and
     * <code>0</code> is returned; otherwise, there is an attempt to read at
     * least one byte. If no byte is available because the stream is at end of
     * file, the value <code>-1</code> is returned; otherwise, at least one
     * byte is read and stored into <code>b</code>.
     **/
public int read(byte b[], int off, int len) throws IOException {
    //省略实现
}

关于read()方法,通俗的讲,就是每次读取都会记录该位置pos,下次读取,则从该pos+1的位置读取流中数据,直到读取到文件或者流的结束位置返回-1,而我们在Filter中调用getInputStream()则是相当于将流中数据全部读取了,使得pos 指向文件/流末尾,再次

读取自然不能获取到流中数据,聊到这,我估计有些人会有跟我一样的想法,既然是pos指针问题,那重置该pos的值不就可以解决问题了嘛,是的 , InputStream提供了reset()方法用来重置pos指针,然鹅,然鹅,然鹅,我们还是看下源码吧。

    public synchronized void reset() throws IOException {
        throw new IOException("mark/reset not supported");
    }

直接抛出IO异常,查了下资料,有解释说,Java期望有需要重写该方法的子类,自己去重写该方法,只需要遵循方法合约(可自行查看该方法说明)即可。

在其子类里翻了 下,确实有重写了该方法的,如 BufferedInputStream

    public synchronized void reset() throws IOException {
        getBufIfOpen(); // Cause exception if closed
        if (markpos < 0)
            throw new IOException("Resetting to invalid mark");
        pos = markpos;
    }

那么我们上文提到的 ServletInputStream呢 ,很不巧,其并未重写该方法。

所以,这也就导致我们想要在读取完之后重置pos的想法落空,只能另辟蹊径,采用文章开头介绍的方式了。

三探:

关于I/O的流读取问题,其实也做一些测试,但是发掘不如网上的文章说的详细,这里也懒得赘述了,链接在此,有兴趣的可以去看下。


Fatal error: Allowed memory size of 134217728 bytes exhausted (tried to allocate 12288 bytes) in /usr/local/lighthouse/softwares/wordpress-plugin/wp-includes/class-wp-comment.php on line 202