凡是使用Spring写WEB项目,都会遇到重复读取requestBody的需求。通过一番搜索,会找到下文或与下文类似的解决方案:
拦截器中,request中getReader()和getInputStream()只能调用一次,构建可重复读取inputStream的request
我最开始用的就是这篇文章里的方法,加上后确实可以重复读取requestBody了。
但是没多久,一个application/x-www-form-urlencoded请求的报错就出现了
HttpMessageNotReadableException: Required request body is missing
这是什么问题呢?只能跟一下源码了
ServletServerHttpRequest
获取requestBody是通过ServletServerHttpRequest的getBody方法
1 2 3 4 5 6 7 8 9
| @Override public InputStream getBody() throws IOException { if (isFormPost(this.servletRequest)) { return getBodyFromServletRequestParameters(this.servletRequest); } else { return this.servletRequest.getInputStream(); } }
|
这里可以看到有一个分叉——会判断是否是Form表单请求(即根据ContentType是否为application/x-www-form-urlencoded)
所以getInputStream是正常的,而Form表单就会进入到另一个分支里
1 2 3 4 5 6 7 8 9 10 11 12
| private static InputStream getBodyFromServletRequestParameters(HttpServletRequest request) throws IOException { ByteArrayOutputStream bos = new ByteArrayOutputStream(1024); Writer writer = new OutputStreamWriter(bos, FORM_CHARSET);
Map<String, String[]> form = request.getParameterMap(); for (Iterator<String> nameIterator = form.keySet().iterator(); nameIterator.hasNext();) { } writer.flush();
return new ByteArrayInputStream(bos.toByteArray()); }
|
Request
getParameterMap最终会走到Request里
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| @Override public Map<String, String[]> getParameterMap() {
if (parameterMap.isLocked()) { return parameterMap; }
Enumeration<String> enumeration = getParameterNames(); while (enumeration.hasMoreElements()) { String name = enumeration.nextElement(); String[] values = getParameterValues(name); parameterMap.put(name, values); }
parameterMap.setLocked(true);
return parameterMap;
}
|
getParameterMap会先通过getParameterNames方法获得所有参数的名称
1 2 3 4 5 6 7
| @Override public Enumeration<String> getParameterNames() { if (!parametersParsed) { parseParameters(); } return coyoteRequest.getParameters().getParameterNames(); }
|
如果参数没有解析过,会先去解析参数
解析参数的方法parseParameters里有这么一段
1 2 3 4
| if (usingInputStream || usingReader) { success = true; return; }
|
如果usingInputStream和usingReader这两个标识位任意一个被设为true的话,就不会走后面的解析方法了
那么这两个标识位是在什么时候被设置的呢?
分别在getReader和getInputStream这两个方法被调用的时候
1 2 3 4
| @Override public BufferedReader getReader() throws IOException{} @Override public ServletInputStream getInputStream() throws IOException {}
|
再回头看我最开始搜索到的解决方案的代码:
1 2 3 4 5
| public RepeatedlyReadRequestWrapper(HttpServletRequest request) throws IOException { super(request); body = readBytes(request.getReader(), "utf-8"); }
|
构造器里直接调用了request.getReader()!
所以请注意了:
凡是解决方案的构造器里调用了getReader()和getInputStream()这两个方法的,都无法重复读取Form表单提交的数据!
正确的做法可以参考stackoverflow上的这篇: How to read request.getInputStream() multiple times
这个方案的优势在于对于requestBody是读时复制,从而不会过早地设置标识位
实现的部分代码如下,Enjoy!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
| public class RepeatedlyReadRequestWrapper extends HttpServletRequestWrapper {
private ByteArrayOutputStream cachedBytes;
public RepeatedlyReadRequestWrapper(HttpServletRequest request) { super(request); }
@Override public ServletInputStream getInputStream() throws IOException { if (cachedBytes == null){ cacheInputStream(); }
return new CachedServletInputStream(); }
@Override public BufferedReader getReader() throws IOException{ return new BufferedReader(new InputStreamReader(getInputStream())); }
private void cacheInputStream() throws IOException {
cachedBytes = new ByteArrayOutputStream(); IOUtils.copy(super.getInputStream(), cachedBytes); }
public class CachedServletInputStream extends ServletInputStream { private ByteArrayInputStream input;
public CachedServletInputStream() { input = new ByteArrayInputStream(cachedBytes.toByteArray()); }
@Override public boolean isFinished() { return false; }
@Override public boolean isReady() { return false; }
@Override public void setReadListener(ReadListener listener) {
}
@Override public int read() throws IOException { return input.read(); } } }
|