前言
在Springboot
的项目中使用Servlet
的Filter
来实现方法签名时,发现ServletInputStream
不支持多次读取流。
虽然网上有很多解决方案的例子,但是我发现没有一篇文章解释为什么会这样的文章,所以决定自己去研究源码。
ServletInputStream和InputStream
首先肯定是研究ServletInputStream
这个类了,却发现这个类只是一个抽象类,它继承了InputStream
这个类。
那么首先研究ServletInputStream
,却发现唯一和流读取的方法readLine()
并未限制流进行重复读取。
既然这样,那限制流重复读取的原因是否是在InputStream
中呢?
却在InputStream
中发现了其实流是支持重复读取的相关方法定义:
-
mark()
标记当前流读取的位置 -
reset()
重置流到mark()
所标记的位置 -
markSupported()
是否支持标记
既然不是由于ServletInputStream
引起的,那只好辛苦点,调试整个请求的链路了。
AbstractMessageConverterMethodArgumentResolver
全链路跟踪调试后,总算是发现了端倪,在AbstractMessageConverterMethodArgumentResolver
中发现了关键方法readWithMessageConverters()
,关键代码如下
@Nullable
protected <T> Object readWithMessageConverters(HttpInputMessage inputMessage, MethodParameter parameter,
Type targetType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {
...省略非关键代码...
EmptyBodyCheckingHttpInputMessage message;
try {
// 此处为关键代码
message = new EmptyBodyCheckingHttpInputMessage(inputMessage);
...省略非关键代码...
}
catch (IOException ex) {
throw new HttpMessageNotReadableException("I/O error while reading input message", ex, inputMessage);
}
...省略非关键代码...
return body;
}
在上面代码中EmptyBodyCheckingHttpInputMessage
这个类就是关键类,而这个关键其实是AbstractMessageConverterMethodArgumentResolver
的内部类,关键代码如下
public EmptyBodyCheckingHttpInputMessage(HttpInputMessage inputMessage) throws IOException {
this.headers = inputMessage.getHeaders();
InputStream inputStream = inputMessage.getBody();
// 判断InputStream支持mark()
if (inputStream.markSupported()) {
// 在InputStream起始位置进行标记
inputStream.mark(1);
// 如果InputStream不为空则赋值
this.body = (inputStream.read() != -1 ? inputStream : null);
// 重置流,表示流可以进行重复读取
inputStream.reset();
}
else {
// PushbackInputStream是一个支持重复读取的流
PushbackInputStream pushbackInputStream = new PushbackInputStream(inputStream);
int b = pushbackInputStream.read();
if (b == -1) { // 为-1表示流中没有数据
this.body = null;
}
else {
this.body = pushbackInputStream;
// 回退操作,使InputStream可以进行重复读取
pushbackInputStream.unread(b);
}
}
}
从上面的代码可以看出,其实Spring MVC
对于ServletInputStream
是支持重复读的(关于PushbackInputStream
的源码这里不进行展开)。但是为什么会出现ServletInputStream
不能重复读取的情况呢?
于是我又再次进行调试,总算发现了问题在于应用服务器上,由于我调试的代码是用SpringBoot
的,使用的应用服务器是tomcat
。
应用服务器
tomcat
在tomcat
的org.apache.catalina.connector.Request
实现了HttpServletRequest
,我们首先要关注其实现的getInputStream()
方法,关键代码如下
/**
* ServletInputStream
*/
protected CoyoteInputStream inputStream =
new CoyoteInputStream(inputBuffer);
// ...省略非关键代码...
@Override
public ServletInputStream getInputStream() throws IOException {
...省略非关键代码...
if (inputStream == null) {
// 关键代码
inputStream = new CoyoteInputStream(inputBuffer);
}
return inputStream;
}
从上面的关键代码可以得知,实际返回ServletInputStream
其实是CoyoteInputStream
,继续研究CoyoteInputStream
后发现其内部其实是使用一个InputBuffer
对象来存储实际的流数据,关键代码如下:
/**
* 实际存储的数据
*/
protected InputBuffer ib;
@Override
public int read() throws IOException {
checkNonBlockingRead();
if (SecurityUtil.isPackageProtectionEnabled()) {
...省略非关键代码...
} else {
// 关键代码
return ib.readByte();
}
}
从上面的关键代码可以得知,实际上对于流的读取还是使用了org.apache.catalina.connector.InputBuffer
的readByte()
方法,InputBuffer
的关键代码如下:
/**
* The byte buffer.
*/
private ByteBuffer bb;
...省略非关键代码...
public int readByte() throws IOException {
if (closed) {
throw new IOException(sm.getString("inputBuffer.streamClosed"));
}
// 关键代码
if (checkByteBufferEof()) {
return -1;
}
return bb.get() & 0xFF;
}
private boolean checkByteBufferEof() throws IOException {
if (bb.remaining() == 0) {
int n = realReadBytes();
if (n < 0) {
return true;
}
}
return false;
}
后续不进行展开,因为tomcat
的调用关系特别复杂。但是可以确定了ServletInputStream
不支持多次读取是由于tomcat
引起的。
后续我调试跟踪了jetty
和undertow
,下面会提供关键类及关键方法,有兴趣的朋友可以自行断点调试。
jetty
jetty
也是不支持ServletInputStream
多次读取,关键类及关键方法为org.eclipse.jetty.server.HttpInput
的read()
方法
undertow
jetty
也是不支持ServletInputStream
多次读取,关键类及关键方法为io.undertow.servlet.spec
的read()
方法
疑问
为什么应用服务器都将ServletInputStream
设计为不可重复读取?
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。