0%

post请求体内容无法重复获取

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
// Tomcat 的 CoyoteInputStream 部分源码
public int read(byte[] b, int off, int len) throws IOException {
if (closed) { // 若流已关闭,直接抛出异常
throw new IOException("Stream closed");
}
if (checkByteBufferEof()) { // 若数据已读完,返回 -1
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();
}
}

// 重写 getInputStream(),从缓存中读取
@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) {
// 异步处理时使用,此处简化实现
}
};
}

// 重写 getReader(),从缓存中读取(字符流)
@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 {

// 仅对 POST 请求进行包装(GET 请求无请求体)
if (request instanceof HttpServletRequest &&
"POST".equalsIgnoreCase(((HttpServletRequest) request).getMethod())) {

// 使用包装类替换原始请求
HttpServletRequest wrappedRequest = new CachedBodyServletRequest((HttpServletRequest) request);
chain.doFilter(wrappedRequest, response);
} else {
// 非 POST 请求直接放行
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() 仍能获取完整请求体,数据来源于缓存的字节数组。

注意事项

  1. 仅对 POST 请求生效:GET 请求的参数在 URL 中,无请求体,无需包装。
  2. 内存占用:请求体较大时(如文件上传),缓存字节数组可能占用较多内存,需根据业务场景调整(如限制最大缓存大小)。
  3. 编码一致性:读取请求体时需使用正确的字符编码(如 UTF-8),避免乱码。
  4. 框架集成:若使用 Spring MVC 等框架,可通过 RequestContextHolder 获取包装后的请求,确保全链路使用缓存的请求体

欢迎关注我的其它发布渠道

表情 | 预览
快来做第一个评论的人吧~
Powered By Valine
v1.3.10