简介
- HTTP 请求 RequestBody 只能被读取一次 :
HttpServletRequest
的输入流 (InputStream
) 在被读取后会被关闭,导致后续无法再次读取。 - 本文将介绍如何通过 请求包装类 (RequestWrapper) 来解决这个问题。
问题背景
当我们需要在以下场景中多次读取 RequestBody
时,就会遇到这个问题:
- 日志记录:需要在拦截器或过滤器中记录请求体
- 参数校验:需要在多个地方校验请求体数据
- 数据解析 :需要在不同的组件中解析请求体(如 Spring 的
@RequestBody
和手动解析)
直接尝试多次读取 InputStream
会导致异常:
java
// 第一次读取(可以成功)
String body1 = IOUtils.toString(request.getInputStream(), "UTF-8");
// 第二次读取
// 场景一:(抛出异常:Stream closed)
String body2 = IOUtils.toString(request.getInputStream(), "UTF-8");
// 场景二:Controller层的@RequestBody直接报错 org.springframework.http.converter.HttpMessageNotReadableException: Required request body is missing: ...
解决方案:缓存请求体
我们可以通过自定义 HttpServletRequestWrapper
来缓存请求体,使得它可以被多次读取。
1. 实现 RequestWrapper
java
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.*;
public class RequestWrapper extends HttpServletRequestWrapper {
//参数字节数组
@Getter
private byte[] requestBody;
//Http请求对象
private final HttpServletRequest request;
public RequestWrapper(HttpServletRequest request) throws IOException {
super(request);
this.request = request;
}
@Override
public ServletInputStream getInputStream() throws IOException {
/*
每次调用此方法时将数据流中的数据读取出来,然后再回填到InputStream之中
解决通过@RequestBody和@RequestParam(POST方式)读取一次后控制器拿不到参数问题
*/
if (null == this.requestBody) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
IOUtils.copy(request.getInputStream(), baos);
this.requestBody = baos.toByteArray();
}
final ByteArrayInputStream bais = new ByteArrayInputStream(requestBody);
return new ServletInputStream() {
@Override
public boolean isFinished() {
return false;
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setReadListener(ReadListener listener) {
}
@Override
public int read() {
return bais.read();
}
};
}
@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(this.getInputStream()));
}
}
2. 使用 RequestWrapper
在 Filter 中包装原始请求:
java
@Component
@Order(Ordered.HIGHEST_PRECEDENCE) // 最高优先级
public class RequestCachingFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
// 包装原始请求
RequestWrapper wrappedRequest = new RequestWrapper((HttpServletRequest) request);
// 注意这里传的是request的装饰类 RequestWrapper
chain.doFilter(wrappedRequest, response);
}
}
方案优势
- 性能优化:只在构造时读取一次请求体,后续从缓存读取
- 线程安全:每个请求有自己的包装实例,互不干扰
- 兼容性 :完全兼容
HttpServletRequest
的所有方法 - 灵活性:可以随时获取原始请求体数据
注意事项
- 大文件处理:对于大文件上传,缓存整个请求体可能消耗较多内存
- 流式处理:如果确实需要流式处理大数据,不宜使用此方案
- Filter 顺序:确保此 Filter 在其他需要读取请求体的 Filter 之前执行