Stay hungry, Stay foolish

0%

Spring重复读取requestBody问题排查

凡是使用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的话,就不会走后面的解析方法了

那么这两个标识位是在什么时候被设置的呢?

分别在getReadergetInputStream这两个方法被调用的时候

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();
}
}
}
据说打赏我的人,代码没有BUG