httpRequest中重复读取inputstream注意事项

在Filter中获取请求体,就得解析一次request的inputStream。

而在tomcat的设计中,request的inputStream只能读取一次,读取完一次后,虽然inputStream还在那,也不会变成空,但里面的内容已经被没有了。

解决方案很简单,就是继承HttpServletRequestWrapper,缓存request中流的内容。

比如实现一个类:ServletRequestReadWrapper

整个类的结构是这样的:

然而世事总不那么一帆风顺。

当请求头Content-Type值为 multipart/form-data 时,情况就出现了问题。在https://www.zhihu.com/question/434950674 这个回答中有说明。

在使用multipart/form-data上传文件时,controller类似这样的

1
2
3
@PostMapping
public void upload(MultipartFile file) throws IOException {
}

想要得到一个MultipartFile类型的内容,原理与其他类型一样,spring从request中获得流内容,并反射创建MultipartFile。

但相对于普通类型,Multipart并没有从request.getInputstream中获取内容,而是从另一个方法getParts里面获取,但getParts返回的内容也是解析的request.getInputStream中的内容。

是不是有点奇怪,为什么在wrapper里面已经缓存了inputstream内容,为什么到了右下解再去getInputStream时,却没有了呢?

tomcat的request类结构是一个装饰器模式

requestWrapper中的getInputStream其实还得来源于真实的Request对象。而在整个处理过程的末端,获取inputStream,并不是从requestWrapper中获取的,而是从真实的Request对象中获取。此时流内容已经被读取过,自然就读取不到了。

再回味下主要的源码,HttpServletRequestWrapper中getParts()

1
2
3
4
5
6
7
8
@Override
public Collection<Part> getParts() throws IOException, ServletException {
return this._getHttpServletRequest().getParts();
}

private HttpServletRequest _getHttpServletRequest() {
return (HttpServletRequest) super.getRequest();
}

到了RquestFacade的getParts()

1
2
3
4
5
@Override
public Collection<Part> getParts() throws IllegalStateException,
IOException, ServletException {
return request.getParts();
}

再到Request的getParts()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public Collection<Part> getParts() throws IOException, IllegalStateException,
ServletException {

parseParts(true);
}

private void parseParts(boolean explicit) {

...
//解析流中文件内容,upload.parseRequest中会调用request.getInputStream
//而传入的request,并不requestWrapper,而是真实的Request本身
//而此时Request中的流已经被读取过了,所以解析出的文件内容为空
//controller中的file变成了null
List<FileItem> items = upload.parseRequest(new ServletRequestContext(this));

...

}

到此,原理已经讲清楚了。怎么解决呢?

1、在Wrapper中,非multipart缓存inputstream,是multipart时,则缓存parts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public ServletRequestReadWrapper(final HttpServletRequest request) throws IOException, ServletException {
super(request);
if (HttpUtils.isMultipartContent(request)) {
parts = request.getParts();
} else {

final ServletInputStream is = request.getInputStream();

if (null != is) {
final ByteArrayOutputStream os = new ByteArrayOutputStream();
this.bodyLength = ByteStreams.copy(is, os);
this.body = os.toByteArray();
}
}
}

2、放弃multipart的消息体。

得到消息体的内容,有时只是为了记录日志,像文件上传,其实得到的消息体也没啥好记录的,所以放弃记录mulipart类型消息体。

公众号:码农戏码
欢迎关注微信公众号『码农戏码』