在 Springboot
的项目中使用 Servlet
的 Filter
来实现方法签名时,发现 ServletInputStream
不支持多次读取流。
虽然网上有很多解决方案的例子,但是我发现没有一篇文章解释为什么会这样的文章,所以决定自己去研究源码。
首先肯定是研究 ServletInputStream
这个类了,却发现这个类只是一个抽象类,它继承了 InputStream
这个类。
那么首先研究 ServletInputStream
,却发现唯一和流读取的方法 readLine()
并未限制流进行重复读取。
既然这样,那限制流重复读取的原因是否是在 InputStream
中呢?
却在 InputStream
中发现了其实 流是支持重复读取
的相关方法定义:
mark()
标记当前流读取的位置 reset()
重置流到 mark()
所标记的位置 markSupported()
是否支持标记
既然不是由于 ServletInputStream
引起的,那只好辛苦点,调试整个请求的链路了。
全链路跟踪调试后,总算是发现了端倪,在 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
的 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
也是不支持 ServletInputStream
多次读取,关键类及关键方法为 org.eclipse.jetty.server.HttpInput
的 read()
方法
jetty
也是不支持 ServletInputStream
多次读取,关键类及关键方法为 io.undertow.servlet.spec
的 read()
方法
为什么应用服务器都将 ServletInputStream
设计为不可重复读取?