“这是我参与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()));
}
}