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


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

目录
  • SpringBoot 请求体 InputStream 多次读取的问题(2021.05.11)
    • 1. 问题描述
    • 2. 原因分析
    • 3. 问题解决

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.*;

/**
 * 自定义类继承 HttpServletRequestWrapper 实现请求体 RequestBody 的多次读取.
 * 

* 为了解决请求体的 RequestBody 无法多次读取而编写,通过重写 getInputStream 与 * getReader 方法实现. * * @author xiaoQQya * @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;

/**
 * 请求体过滤器.
 * 

* 将请求体由 HttpServletRequest 类型替换为自定义 MultiReadHttpServletRequest * 类型,实现请求体 RequestBody 的多次读取. * * @author xiaoQQya * @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