在 Springboot
的项目中使用 Servlet
的 Filter
来实现方法签名时,发现请求的 body
不支持多次读取。我是通过 getInputStream()
来获取流,然后通过读取流来获取请求的 body
。
虽然网上有很多解决方案的例子,但是我发现没有一篇文章解释为什么会这样的文章,所以决定自己去研究源码。
Content-Type
为 application/json
的 POST
请求时,会返回状态码为 400
的响应,响应的 body
如下:
{ "timestamp": "2019-12-27T02:48:50.544+0000", "status": 400, "error": "Bad Request", "message": "Required request body is missing: ...省略非关键信息...", "path": "/" }
而在日志中则有以下关键日志
2019-12-27 10:48:50.543 WARN 18352 --- [nio-8080-exec-2] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.http.converter.HttpMessageNotReadableException:...省略非关键信息...
根据提示信息可以得知,由于请求的 body
没有了,所以才会抛出这个异常。但是为什么 body
会没有了呢?是否因为通过 getInputStream()
获取到的流被读取了所以引起这个问题呢?
于是我编写了一个复盘的示例,就是一个在日志中打印请求 body
的 Filter
,关键代码如下:
@Slf4j @WebFilter public class InputStreamFilter implements Filter { @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws ServletException, IOException { ServletInputStream inputStream=servletRequest.getInputStream(); String charSetStr = servletRequest.getCharacterEncoding(); if (charSetStr == null) { charSetStr = "UTF-8"; } Charset charSet = Charset.forName(charSetStr); log.info("请求的body为:/n{}",StreamUtils.copyToString(inputStream,charSet)); filterChain.doFilter(servletRequest,servletResponse); }
首先是找出抛出 HttpMessageNotReadableException
的方法和对应的类,关键类为 RequestResponseBodyMethodProcessor
的 readWithMessageConverters()
方法:
@Override protected <T> Object readWithMessageConverters(NativeWebRequest webRequest, MethodParameter parameter, Type paramType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException { ...省略非关键代码... // 关键代码 Object arg = readWithMessageConverters(inputMessage, parameter, paramType); if (arg == null && checkRequired(parameter)) { throw new HttpMessageNotReadableException("Required request body is missing: " + parameter.getExecutable().toGenericString(), inputMessage); } return arg; }
从上面的代码可以得知,异常是由于 readWithMessageConverters()
方法返回 null
且这个参数是必填引起,现在主要关注为什么返回 null
。所以查看 readWithMessageConverters()
方法
而实际上其实是调用了 AbstractMessageConverterMethodArgumentResolver
的 readWithMessageConverters()
方法,而在这个方法中其实是通过调用 AbstractMessageConverterMethodArgumentResolver
的内部类 EmptyBodyCheckingHttpInputMessage
的构造方法来获取流。
readWithMessageConverters()
关键代码如下:
@Nullable protected <T> Object readWithMessageConverters(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException { ...省略非关键代码... EmptyBodyCheckingHttpInputMessage message; try { // 此处调用构造方法时进行了流的读取 message = new EmptyBodyCheckingHttpInputMessage(inputMessage); for (HttpMessageConverter<?> converter : this.messageConverters) { Class<HttpMessageConverter<?>> converterType = (Class<HttpMessageConverter<?>>) converter.getClass(); GenericHttpMessageConverter<?> genericConverter = (converter instanceof GenericHttpMessageConverter ? (GenericHttpMessageConverter<?>) converter : null); if (genericConverter != null ? genericConverter.canRead(targetType, contextClass, contentType) : (targetClass != null && converter.canRead(targetClass, contentType))) { if (message.hasBody()) { ...省略非关键代码... } else { // 此处是处理流读取返回null的情况 body = getAdvice().handleEmptyBody(null, message, parameter, targetType, converterType); } break; } } } catch (IOException ex) { throw new HttpMessageNotReadableException("I/O error while reading input message", ex, inputMessage); } ...省略非关键代码... MediaType selectedContentType = contentType; Object theBody = body; LogFormatUtils.traceDebug(logger, traceOn -> { String formatted = LogFormatUtils.formatValue(theBody, !traceOn); return "Read /"" + selectedContentType + "/" to [" + formatted + "]"; }); return body; }
EmptyBodyCheckingHttpInputMessage
关键构造方法如下:
// 从请求中获取到的InputStream对象 @Nullable private final InputStream body; public EmptyBodyCheckingHttpInputMessage(HttpInputMessage inputMessage) throws IOException { this.headers = inputMessage.getHeaders(); InputStream inputStream = inputMessage.getBody(); // 判断InputStream是否支持mark if (inputStream.markSupported()) { // 标记流的起始位置 inputStream.mark(1); // 读取流 this.body = (inputStream.read() != -1 ? inputStream : null); // 重置流,下次读取会从起始位置重新开始读取 inputStream.reset(); } else { // 回退输入流,支持重复读取的InputStream PushbackInputStream pushbackInputStream = new PushbackInputStream(inputStream); // 读取流 int b = pushbackInputStream.read(); if (b == -1) { // 返回-1表示流无数据存在 this.body = null; } else { // 流存在数据,直接赋值 this.body = pushbackInputStream; // 回退流到起始位置,等价于reset()方法 pushbackInputStream.unread(b); } } }
从上面的代码可以得知,起始 SpringMVC
是支持对请求的 InputStream
进行多次读取的以及 InputStream
其实可以 支持流重复读取
。但是实际上却出现不支持流重复读取的情况,这是为什么呢?
下面会通过分析 Jetty
应用服务器对 InputStream
的实现来进行分析。
Jetty
中继承 InputStrean
的类是 org.eclipse.jetty.server.HttpInputOverHTTP
,而关键的代码在其父类 HttpInput
上。
首先 HttpInput
继承了 ServletInputStream
(这个抽象类继承了· InputStream
抽象类),且并未重写 markSupported()
方法(这个方法默认实现为返回 false
)。所以问题应该是由于 HttpInput
流重复读取会直接返回 -1
引起的。这里不做展开,有兴趣的朋友可以自行跟踪源码的运行