• request.getInputStream() 流只能读取一次问题


    问题: 一次开发过程中同事在 sptring interceptor 中获取 request body 中值,以对数据的校验和预处理等操作 、导致之后spring 在读取request body 值做数据映射时一直报 request body is null 、以此在这通过源码回顾了一下 InputStream read 的方法中的基础知

    首先来看看inputStream 中 read() 源码 实现以 ByteArrayInputStream 中源码来查看:

    1: inputStream read()

     /**
         * Reads the next byte of data from the input stream. The value byte is
         * returned as an <code>int</code> in the range <code>0</code> to
         * <code>255</code>. If no byte is available because the end of the stream
         * has been reached, the value <code>-1</code> is returned. This method
         * blocks until input data is available, the end of the stream is detected,
         * or an exception is thrown.
         *
         * <p> A subclass must provide an implementation of this method.
         *
         * @return     the next byte of data, or <code>-1</code> if the end of the
         *             stream is reached.
         * @exception  IOException  if an I/O error occurs.
         */
        public abstract int read() throws IOException;
    

     大致意思从输入流中读取下一个字节、如果以达到末尾侧返回-1

    2: ByteArrayInputStream 中实现

     /**
         * Reads the next byte of data from this input stream. The value
         * byte is returned as an <code>int</code> in the range
         * <code>0</code> to <code>255</code>. If no byte is available
         * because the end of the stream has been reached, the value
         * <code>-1</code> is returned.
         * <p>
         * This <code>read</code> method
         * cannot block.
         *
         * @return  the next byte of data, or <code>-1</code> if the end of the
         *          stream has been reached.
         */
        public synchronized int read() {
            return (pos < count) ? (buf[pos++] & 0xff) : -1;
        }
    

     这个实现看起来很好理解、 方法内pos 标识当前流每次流读取的位置、 每读取一次pos 做一次位移、直至结束返回-1: 看看 pos 的 值定义:

    /**
         * Creates a <code>ByteArrayInputStream</code>
         * so that it  uses <code>buf</code> as its
         * buffer array.
         * The buffer array is not copied.
         * The initial value of <code>pos</code>
         * is <code>0</code> and the initial value
         * of  <code>count</code> is the length of
         * <code>buf</code>.
         *
         * @param   buf   the input buffer.
         */
        public ByteArrayInputStream(byte buf[]) {
            this.buf = buf;
            this.pos = 0;
            this.count = buf.length;
        }
    

     在这可以看到在类实例化时 给 pos 初始化 0 默认从流的的起始位置开始读! 当然如果想从固定位置读区可以看read的其它构造方法在这就不多说了。

    到这之后当流读取完成后 pos 变量已经达到流的末尾处。这是如果在读取就会直接返回 -1 、 现在很明白了、如果想在此读取流中的值只需要把 pos 的值 rest 到初始位置就可以、 OK 没问题在 inputStrean 也提供了 rest() 方法 我们一起来看看它的实现:

    /**
         * Resets the buffer to the marked position.  The marked position
         * is 0 unless another position was marked or an offset was specified
         * in the constructor.
         */
        public synchronized void reset() {
            pos = mark;
        }
     /**
         * The currently marked position in the stream.
         * ByteArrayInputStream objects are marked at position zero by
         * default when constructed.  They may be marked at another
         * position within the buffer by the <code>mark()</code> method.
         * The current buffer position is set to this point by the
         * <code>reset()</code> method.
         * <p>
         * If no mark has been set, then the value of mark is the offset
         * passed to the constructor (or 0 if the offset was not supplied).
         *
         * @since   JDK1.1
         */
        protected int mark = 0;
    

     从这两段中不难看出rest方法是将 pos 值从新初始化为0、 当然到这还没有结束、并不是所有的流都有权限实现 rest() 方法的取决条件在 markSupported() 方法中 请看下面源码的介绍

     /**
         * Tests if this <code>InputStream</code> supports mark/reset. The
         * <code>markSupported</code> method of <code>ByteArrayInputStream</code>
         * always returns <code>true</code>.
         *
         * @since   JDK1.1
         */
        public boolean markSupported() {
            return true;
        }
    

     从方法的注释上很容易看出markSupported是 mark/reset 方法的标识变量、由它来决定 mark/reset 是否可调用。

    到这我们基本很清楚inputStrean 中 read() 方法了。

    回头我们再来看看 request.getInputStream 

    /**
         * Retrieves the body of the request as binary data using a
         * {@link ServletInputStream}. Either this method or {@link #getReader} may
         * be called to read the body, not both.
         *
         * @return a {@link ServletInputStream} object containing the body of the
         *         request
         * @exception IllegalStateException
         *                if the {@link #getReader} method has already been called
         *                for this request
         * @exception IOException
         *                if an input or output exception occurred
         */
        public ServletInputStream getInputStream() throws IOException;
    

     从源码中可以看出 request.getInputStream 方法返回的是 ServletInputStream 对象 我们在来看看 ServletInputStream 中源码的实现

    /*
     * Licensed to the Apache Software Foundation (ASF) under one or more
     * contributor license agreements.  See the NOTICE file distributed with
     * this work for additional information regarding copyright ownership.
     * The ASF licenses this file to You under the Apache License, Version 2.0
     * (the "License"); you may not use this file except in compliance with
     * the License.  You may obtain a copy of the License at
     *
     *     http://www.apache.org/licenses/LICENSE-2.0
     *
     * Unless required by applicable law or agreed to in writing, software
     * distributed under the License is distributed on an "AS IS" BASIS,
     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     * See the License for the specific language governing permissions and
     * limitations under the License.
     */
    package javax.servlet;
    
    import java.io.IOException;
    import java.io.InputStream;
    
    /**
     * Provides an input stream for reading binary data from a client request,
     * including an efficient <code>readLine</code> method for reading data one line
     * at a time. With some protocols, such as HTTP POST and PUT, a
     * <code>ServletInputStream</code> object can be used to read data sent from the
     * client.
     * <p>
     * A <code>ServletInputStream</code> object is normally retrieved via the
     * {@link ServletRequest#getInputStream} method.
     * <p>
     * This is an abstract class that a servlet container implements. Subclasses of
     * this class must implement the <code>java.io.InputStream.read()</code> method.
     *
     * @see ServletRequest
     */
    public abstract class ServletInputStream extends InputStream {
    
        /**
         * Does nothing, because this is an abstract class.
         */
        protected ServletInputStream() {
            // NOOP
        }
    
        /**
         * Reads the input stream, one line at a time. Starting at an offset, reads
         * bytes into an array, until it reads a certain number of bytes or reaches
         * a newline character, which it reads into the array as well.
         * <p>
         * This method returns -1 if it reaches the end of the input stream before
         * reading the maximum number of bytes.
         *
         * @param b
         *            an array of bytes into which data is read
         * @param off
         *            an integer specifying the character at which this method
         *            begins reading
         * @param len
         *            an integer specifying the maximum number of bytes to read
         * @return an integer specifying the actual number of bytes read, or -1 if
         *         the end of the stream is reached
         * @exception IOException
         *                if an input or output exception has occurred
         */
        public int readLine(byte[] b, int off, int len) throws IOException {
    
            if (len <= 0) {
                return 0;
            }
            int count = 0, c;
    
            while ((c = read()) != -1) {
                b[off++] = (byte) c;
                count++;
                if (c == '
    ' || count == len) {
                    break;
                }
            }
            return count > 0 ? count : -1;
        }
    
        /**
         * Has the end of this InputStream been reached?
         *
         * @return <code>true</code> if all the data has been read from the stream,
         * else <code>false</code>
         *
         * @since Servlet 3.1
         */
        public abstract boolean isFinished();
    
        /**
         * Can data be read from this InputStream without blocking?
         * Returns  If this method is called and returns false, the container will
         * invoke {@link ReadListener#onDataAvailable()} when data is available.
         *
         * @return <code>true</code> if data can be read without blocking, else
         * <code>false</code>
         *
         * @since Servlet 3.1
         */
        public abstract boolean isReady();
    
        /**
         * Sets the {@link ReadListener} for this {@link ServletInputStream} and
         * thereby switches to non-blocking IO. It is only valid to switch to
         * non-blocking IO within async processing or HTTP upgrade processing.
         *
         * @param listener  The non-blocking IO read listener
         *
         * @throws IllegalStateException    If this method is called if neither
         *                                  async nor HTTP upgrade is in progress or
         *                                  if the {@link ReadListener} has already
         *                                  been set
         * @throws NullPointerException     If listener is null
         *
         * @since Servlet 3.1
         */
        public abstract void setReadListener(ReadListener listener);
    }
    

     从源码中查看 ServletInputStream 并没有重写 rest() 方法、我们在 到 InputStream 中去查看

    /**
         * Repositions this stream to the position at the time the
         * <code>mark</code> method was last called on this input stream.
         *
         * <p> The general contract of <code>reset</code> is:
         *
         * <ul>
         * <li> If the method <code>markSupported</code> returns
         * <code>true</code>, then:
         *
         *     <ul><li> If the method <code>mark</code> has not been called since
         *     the stream was created, or the number of bytes read from the stream
         *     since <code>mark</code> was last called is larger than the argument
         *     to <code>mark</code> at that last call, then an
         *     <code>IOException</code> might be thrown.
         *
         *     <li> If such an <code>IOException</code> is not thrown, then the
         *     stream is reset to a state such that all the bytes read since the
         *     most recent call to <code>mark</code> (or since the start of the
         *     file, if <code>mark</code> has not been called) will be resupplied
         *     to subsequent callers of the <code>read</code> method, followed by
         *     any bytes that otherwise would have been the next input data as of
         *     the time of the call to <code>reset</code>. </ul>
         *
         * <li> If the method <code>markSupported</code> returns
         * <code>false</code>, then:
         *
         *     <ul><li> The call to <code>reset</code> may throw an
         *     <code>IOException</code>.
         *
         *     <li> If an <code>IOException</code> is not thrown, then the stream
         *     is reset to a fixed state that depends on the particular type of the
         *     input stream and how it was created. The bytes that will be supplied
         *     to subsequent callers of the <code>read</code> method depend on the
         *     particular type of the input stream. </ul></ul>
         *
         * <p>The method <code>reset</code> for class <code>InputStream</code>
         * does nothing except throw an <code>IOException</code>.
         *
         * @exception  IOException  if this stream has not been marked or if the
         *               mark has been invalidated.
         * @see     java.io.InputStream#mark(int)
         * @see     java.io.IOException
         */
        public synchronized void reset() throws IOException {
            throw new IOException("mark/reset not supported");
        }
    
        /**
         * Tests if this input stream supports the <code>mark</code> and
         * <code>reset</code> methods. Whether or not <code>mark</code> and
         * <code>reset</code> are supported is an invariant property of a
         * particular input stream instance. The <code>markSupported</code> method
         * of <code>InputStream</code> returns <code>false</code>.
         *
         * @return  <code>true</code> if this stream instance supports the mark
         *          and reset methods; <code>false</code> otherwise.
         * @see     java.io.InputStream#mark(int)
         * @see     java.io.InputStream#reset()
         */
        public boolean markSupported() {
            return false;
        }
    

     至此我们基本搞清楚了 为什么 reqeust.getInputStream 方法只能读取一次、 因在读取一次后 pos 值已经达到文件末尾、而 ServletInputStream 没有重写 rest() 方法、从而导致request.getInputStream 只能读取一次。

    解决方案其实很简单 可以使用 HttpServletRequest 的装饰器 HttpServletRequestWrapper 来解决。写一个简单例子仅供参考: 这里装饰器模式不了解的同学可以去了解下。

    import java.io.BufferedReader;
    import java.io.ByteArrayInputStream;
    import java.io.IOException;
    import java.io.InputStreamReader;
    import java.nio.charset.StandardCharsets;
    
    import javax.servlet.ReadListener;
    import javax.servlet.ServletInputStream;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletRequestWrapper;
    
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    
    public class RequestWrapper extends HttpServletRequestWrapper {
    
    	private byte[] body;
    
    	public RequestWrapper(HttpServletRequest request) {
    		super(request);
    	}
    
    	public RequestWrapper(HttpServletRequest request, String body) {
    		super(request);
    		this.body = body.getBytes(StandardCharsets.UTF_8);
    	}
    
    	@Override
    	public ServletInputStream getInputStream() throws IOException {
    		final ByteArrayInputStream bais = new ByteArrayInputStream(body);
    
    		return new ServletInputStream() {
    
    			@Override
    			public boolean isFinished() {
    				// Auto-generated method stub
    				return false;
    			}
    
    			@Override
    			public boolean isReady() {
    				// Auto-generated method stub
    				return false;
    			}
    
    			@Override
    			public void setReadListener(ReadListener listener) {
    				// Auto-generated method stub
    
    			}
    
    			@Override
    			public int read() throws IOException {
    				return bais.read();
    			}
    		};
    	}
    
    	@Override
    	public BufferedReader getReader() throws IOException {
    		return new BufferedReader(new InputStreamReader(this.getInputStream()));
    	}
    	
    	public String getBody() {
    	    return new String(body, StandardCharsets.UTF_8);
    	}
    
    }

     这是一个简单的列子、 具体使用根据自己业务来实现。

    本章就介绍到这!如有不对的地方请多多指教、大家互相学习、谢谢!

     

  • 相关阅读:
    MVC @Url.Action 小示例
    Eclipse快捷键
    MVC视频下载/文件上传
    MySQL数据库备份/导出
    C#文件下载
    C#正则表达式匹配字符串中的数字
    常用的LINQ to SQL 用法
    C# 实现抓取网页内容(一)
    C# 繁体字和简体字之间的相互转换
    我到底会什么??
  • 原文地址:https://www.cnblogs.com/yueli/p/7986009.html
Copyright © 2020-2023  润新知