SpringBoot 请求体 InputStream 多次读取的问题(2021.05.11)

SpringBoot 请求体 InputStream 多次读取的问题(2021.05.11)

1. 问题描述

笔者最近为一个 SpringBoot Web 项目添加日志审查,需要在请求完成后记录接口的响应时间、请求参数等信息,在请求完成后读取 RequestBody 时遇到了 java.io.IOException: Stream closed 的异常,如果改为在请求处理之前进行记录则在 controller 层无法获取到 RequestBody 的内容。

2. 原因分析

经过分析,定位原因为 Java 中 InputStream 只能读取一次,所以在请求完成之后读取时流已经关闭了,同理在请求处理之前进行读取则在请求处理时就获取不到内容了。

3. 问题解决

问题出在 InputStream 无法多次读取,所以我们只需要将 RequestBody 的内容缓存下来,之后每次获取 InputStream 时根据缓存内容返回一个新的 InputStream 对象即可。

实现思路为自定义类 MultiReadHttpServletRequest 继承 HttpServletRequestWrapper 类重写 getInputStreamgetReader 方法,使其每次根据缓存的内容返回新的 InputStream 对象,然后在过滤器中将 HttpServletRequest 对象替换为可多次读取请求体的 MultiReadHttpServletRequest 对象。

自定义类 MultiReadHttpServletRequest.java 代码如下:

import org.apache.commons.io.IOUtils;

import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.*;

/**
 * 自定义类继承 <code>HttpServletRequestWrapper</code> 实现请求体 <code>RequestBody</code> 的多次读取.
 * <p>
 * 为了解决请求体的 <code>RequestBody</code> 无法多次读取而编写,通过重写 <code>getInputStream</code> 与
 * <code>getReader</code> 方法实现.
 *
 * @author <a href="mailto:xiaoQQya@126.com">xiaoQQya</a>
 * @date 2021/05/06
 * @since 1.0
 */
public class MultiReadHttpServletRequest extends HttpServletRequestWrapper {

    private ByteArrayOutputStream cachedBytes;

    public MultiReadHttpServletRequest(HttpServletRequest request) {
        super(request);
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {
        if (cachedBytes == null) {
            cacheInputStream();
        }
        return new CachedServletInputStream();
    }

    @Override
    public BufferedReader getReader() throws IOException {
        return new BufferedReader(new InputStreamReader(getInputStream()));
    }

    /**
     * Cache the inputstream in order to read it multiple times. For
     * convenience, I use apache.commons IOUtils.
     *
     * @throws IOException IOException
     * @author xiaoqqya
     * @date 2021/05/06
     */
    private void cacheInputStream() throws IOException {
        cachedBytes = new ByteArrayOutputStream();
        IOUtils.copy(super.getInputStream(), cachedBytes);
    }

    /**
     * An inputstream which reads the cached request body.
     *
     * @author xiaoqqya
     * @date 2021/05/06
     */
    public class CachedServletInputStream extends ServletInputStream {
        private final ByteArrayInputStream input;

        public CachedServletInputStream() {
            // create a new input stream from the cached request body
            input = new ByteArrayInputStream(cachedBytes.toByteArray());
        }

        @Override
        public boolean isFinished() {
            return false;
        }

        @Override
        public boolean isReady() {
            return false;
        }

        @Override
        public void setReadListener(ReadListener readListener) {

        }

        @Override
        public int read() throws IOException {
            return input.read();
        }
    }
}

过滤器 MultiReadHttpServletRequestFilter.java 代码如下:

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;

/**
 * 请求体过滤器.
 * <p>
 * 将请求体由 <code>HttpServletRequest</code> 类型替换为自定义 <code>MultiReadHttpServletRequest</code>
 * 类型,实现请求体 <code>RequestBody</code> 的多次读取.
 *
 * @author <a href="mailto:xiaoQQya@126.com">xiaoQQya</a>
 * @date 2021/05/06
 * @since 1.0
 */
@WebFilter(
        filterName = "MultiReadHttpServletRequestFilter",
        urlPatterns = "/api/*"
)
public class MultiReadHttpServletRequestFilter implements Filter {

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        ServletRequest multiReadRequest = null;
        if (servletRequest instanceof HttpServletRequest) {
            multiReadRequest = new MultiReadHttpServletRequest((HttpServletRequest) servletRequest);
        }
        if (multiReadRequest == null) {
            filterChain.doFilter(servletRequest, servletResponse);
        } else {
            filterChain.doFilter(multiReadRequest, servletResponse);
        }
    }
}

参考文章:Http Servlet request lose params from POST body after read it once - Stack Overflow

posted @ 2022-05-26 15:52  Hit不死的小强  阅读(230)  评论(0编辑  收藏  举报