“这是我参与8月更文挑战的第5天,活动详情查看:8月更文挑战”
问题简述
在安全性要求比较高的接口上,项目一般会在过滤器做一些验签的工作。对于携带JSON数据的请求,我们需要调用HttpServletRequest输入流
来获取JSON数据从而解析参数。但是采用该方法后,在Controller层使用@RequestBody
解析参数将会抛出异常java.lang.IllegalStateException
。
问题原因
无论是我们自己解析JSON,还是使用@RequestBody
进行解析,本质上都是对HttpServletRequest
以流的形式进行读取再解析,而HttpServletRequest
的 getInputStream()
和 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
。
InputStream
的read()
方法内部有一个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()));
}
}
复制代码