在Springboot
的项目中使用Servlet
的Filter
来实现方法签名时,发现ServletInputStream
不支持屡次读取流。apache
虽然网上有不少解决方案的例子,可是我发现没有一篇文章解释为何会这样的文章,因此决定本身去研究源码。tomcat
首先确定是研究ServletInputStream
这个类了,却发现这个类只是一个抽象类,它继承了InputStream
这个类。服务器
那么首先研究ServletInputStream
,却发现惟一和流读取的方法readLine()
并未限制流进行重复读取。eclipse
既然这样,那限制流重复读取的缘由是不是在InputStream
中呢?ide
却在InputStream
中发现了其实流是支持重复读取的相关方法定义:this
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
的内部类,关键代码以下code
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
不能重复读取的状况呢?server
因而我又再次进行调试,总算发现了问题在于应用服务器上,因为我调试的代码是用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
设计为不可重复读取?