转载

为什么SpringMVC中请求的body不支持多次读取

前言

Springboot 的项目中使用 ServletFilter 来实现方法签名时,发现请求的 body 不支持多次读取。我是通过 getInputStream() 来获取流,然后通过读取流来获取请求的 body

虽然网上有很多解决方案的例子,但是我发现没有一篇文章解释为什么会这样的文章,所以决定自己去研究源码。

问题表现

Content-Typeapplication/jsonPOST 请求时,会返回状态码为 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() 获取到的流被读取了所以引起这个问题呢?

复盘代码

于是我编写了一个复盘的示例,就是一个在日志中打印请求 bodyFilter ,关键代码如下:

@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);
    }

RequestResponseBodyMethodProcessor

首先是找出抛出 HttpMessageNotReadableException 的方法和对应的类,关键类为 RequestResponseBodyMethodProcessorreadWithMessageConverters() 方法:

@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

而实际上其实是调用了 AbstractMessageConverterMethodArgumentResolverreadWithMessageConverters() 方法,而在这个方法中其实是通过调用 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 的实现来进行分析。

HttpInput

Jetty 中继承 InputStrean 的类是 org.eclipse.jetty.server.HttpInputOverHTTP ,而关键的代码在其父类 HttpInput 上。

首先 HttpInput 继承了 ServletInputStream (这个抽象类继承了· InputStream 抽象类),且并未重写 markSupported() 方法(这个方法默认实现为返回 false )。所以问题应该是由于 HttpInput 流重复读取会直接返回 -1 引起的。这里不做展开,有兴趣的朋友可以自行跟踪源码的运行

原文  https://segmentfault.com/a/1190000021446184
正文到此结束
Loading...