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
类重写 getInputStream
和 getReader
方法,使其每次根据缓存的内容返回新的 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
标签:11,读取,2021.05,InputStream,IOException,import,public,请求 来源: https://www.cnblogs.com/xiaoQQya/p/16313751.html