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 int in the range 0 to
     * 255. If no byte is available because the end of the stream
     * has been reached, the value -1 is returned. This method
     * blocks until input data is available, the end of the stream is detected,
     * or an exception is thrown.
     *
     * 

A subclass must provide an implementation of this method. * * @return the next byte of data, or -1 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 int in the range
     * 0 to 255. If no byte is available
     * because the end of the stream has been reached, the value
     * -1 is returned.
     * 

* This read method * cannot block. * * @return the next byte of data, or -1 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 ByteArrayInputStream
     * so that it  uses buf as its
     * buffer array.
     * The buffer array is not copied.
     * The initial value of pos
     * is 0 and the initial value
     * of  count is the length of
     * buf.
     *
     * @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 mark() method.
     * The current buffer position is set to this point by the
     * reset() method.
     * 

* 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 InputStream supports mark/reset. The
     * markSupported method of ByteArrayInputStream
     * always returns true.
     *
     * @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 readLine method for reading data one line
 * at a time. With some protocols, such as HTTP POST and PUT, a
 * ServletInputStream object can be used to read data sent from the
 * client.
 * 

* A ServletInputStream object is normally retrieved via the * {@link ServletRequest#getInputStream} method. *

* This is an abstract class that a servlet container implements. Subclasses of * this class must implement the java.io.InputStream.read() 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. *

* 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 == '\n' || count == len) { break; } } return count > 0 ? count : -1; } /** * Has the end of this InputStream been reached? * * @return true if all the data has been read from the stream, * else false * * @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 true if data can be read without blocking, else * false * * @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
     * mark method was last called on this input stream.
     *
     * 

The general contract of reset is: * *

    *
  • If the method markSupported returns * true, then: * *
    • If the method mark has not been called since * the stream was created, or the number of bytes read from the stream * since mark was last called is larger than the argument * to mark at that last call, then an * IOException might be thrown. * *
    • If such an IOException is not thrown, then the * stream is reset to a state such that all the bytes read since the * most recent call to mark (or since the start of the * file, if mark has not been called) will be resupplied * to subsequent callers of the read method, followed by * any bytes that otherwise would have been the next input data as of * the time of the call to reset.
    * *
  • If the method markSupported returns * false, then: * *
    • The call to reset may throw an * IOException. * *
    • If an IOException 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 read method depend on the * particular type of the input stream.
* *

The method reset for class InputStream * does nothing except throw an IOException. * * @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 mark and * reset methods. Whether or not mark and * reset are supported is an invariant property of a * particular input stream instance. The markSupported method * of InputStream returns false. * * @return true if this stream instance supports the mark * and reset methods; false 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);
	}

}

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

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

 

转载于:https://www.cnblogs.com/yueli/p/7986009.html

你可能感兴趣的:(request.getInputStream() 流只能读取一次问题)