POST 请求体无法重复读取的原因与解决方案
在 Java Web 开发中,经常会遇到 “POST 请求体只能读取一次” 的问题:当过滤器(Filter)读取请求体后,Servlet 中再次读取时会返回空数据。这一现象与请求流的底层实现密切相关,本文将解析其原因,并提供基于请求包装类的解决方案。
为什么 POST 请求体无法重复读取?
POST 请求体的内容通过输入流(ServletInputStream)读取,而输入流具有一次性读取的特性,核心原因如下:
1. 输入流的 “消费性”
输入流(如 CoyoteInputStream,Tomcat 的实现)本质上是对底层字节流的封装,其读取过程是 “消费性” 的:
- 读取数据时,流的指针会向后移动;
- 当流读取完毕(指针到达末尾),再次读取会返回
-1(表示已读完);
- 流关闭后(如
close() 被调用),无法再次打开或重置。
2. Tomcat 中的具体实现
以 Tomcat 为例,getInputStream() 返回的 CoyoteInputStream 内部依赖 InputBuffer 存储请求体数据。当数据被读取后,InputBuffer 中的缓存会被清空,且流的 closed 状态会被标记为 true:
1 2 3 4 5 6 7 8 9 10 11 12 13
| public int read(byte[] b, int off, int len) throws IOException { if (closed) { throw new IOException("Stream closed"); } if (checkByteBufferEof()) { return -1; } int n = Math.min(len, bb.remaining()); bb.get(b, off, n); return n; }
|
因此,当过滤器首次读取请求体后,流的指针已到达末尾,后续读取自然返回空。
解决方案:使用请求包装类缓存请求体
解决思路是在首次读取时缓存请求体数据,后续读取从缓存中获取,而非直接操作原始流。这一功能可通过 HttpServletRequestWrapper 实现。
1. HttpServletRequestWrapper 简介
HttpServletRequestWrapper 是 Servlet 规范提供的请求包装类,用于扩展或修改原始请求的行为:
- 内部持有原始
HttpServletRequest 对象;
- 重写特定方法(如
getInputStream()、getReader())以定制请求处理逻辑;
- 避免直接修改容器的
Request 实现类(如 Tomcat 的 CoyoteRequest),降低耦合。
2. 实现缓存请求体的包装类
(1)自定义包装类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72
| import javax.servlet.ReadListener; import javax.servlet.ServletInputStream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequestWrapper; import java.io.*; import java.nio.charset.StandardCharsets;
public class CachedBodyServletRequest extends HttpServletRequestWrapper {
private final byte[] cachedBody;
public CachedBodyServletRequest(HttpServletRequest request) throws IOException { super(request); this.cachedBody = readBody(request); }
private byte[] readBody(HttpServletRequest request) throws IOException { try (InputStream inputStream = request.getInputStream(); ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
byte[] buffer = new byte[1024]; int bytesRead; while ((bytesRead = inputStream.read(buffer)) != -1) { outputStream.write(buffer, 0, bytesRead); } return outputStream.toByteArray(); } }
@Override public ServletInputStream getInputStream() throws IOException { ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(cachedBody); return new ServletInputStream() { @Override public int read() throws IOException { return byteArrayInputStream.read(); }
@Override public boolean isFinished() { return byteArrayInputStream.available() == 0; }
@Override public boolean isReady() { return true; }
@Override public void setReadListener(ReadListener readListener) { } }; }
@Override public BufferedReader getReader() throws IOException { return new BufferedReader( new InputStreamReader(getInputStream(), StandardCharsets.UTF_8) ); }
public String getCachedBodyAsString() { return new String(cachedBody, StandardCharsets.UTF_8); } }
|
(2)通过过滤器应用包装类
在过滤器中,将原始请求替换为自定义包装类,确保后续组件(如 Servlet)使用缓存的请求体:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| import javax.servlet.*; import javax.servlet.annotation.WebFilter; import javax.servlet.http.HttpServletRequest; import java.io.IOException;
@WebFilter("/*") public class CachedBodyFilter implements Filter {
@Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
if (request instanceof HttpServletRequest && "POST".equalsIgnoreCase(((HttpServletRequest) request).getMethod())) {
HttpServletRequest wrappedRequest = new CachedBodyServletRequest((HttpServletRequest) request); chain.doFilter(wrappedRequest, response); } else { chain.doFilter(request, response); } }
@Override public void init(FilterConfig filterConfig) throws ServletException {} @Override public void destroy() {} }
|
3. 使用效果
- 过滤器中读取请求体:
wrappedRequest.getCachedBodyAsString() 或 wrappedRequest.getInputStream();
- Servlet 中再次读取:通过
request.getInputStream() 或 request.getReader() 仍能获取完整请求体,数据来源于缓存的字节数组。
注意事项
- 仅对 POST 请求生效:GET 请求的参数在 URL 中,无请求体,无需包装。
- 内存占用:请求体较大时(如文件上传),缓存字节数组可能占用较多内存,需根据业务场景调整(如限制最大缓存大小)。
- 编码一致性:读取请求体时需使用正确的字符编码(如
UTF-8),避免乱码。
- 框架集成:若使用 Spring MVC 等框架,可通过
RequestContextHolder 获取包装后的请求,确保全链路使用缓存的请求体
v1.3.10