1. 引言
在实际开发中,我们经常会遇到需要多次读取 HttpServletRequest
请求体的场景,比如:
- 日志记录(记录入参)
- 签名验证
- 敏感词检测
- 全局拦截处理
但有个“坑”你肯定踩过:HTTP 请求体的输入流(InputStream)只能读一次 ❌。一旦被读取(比如被 Spring MVC 的 @RequestBody
消费),再次读取就会得到空内容。
本文将带你实现一个可重复读取请求体的解决方案,适用于 Spring 项目。我们先看官方方案的局限,再手撸一个通用性更强的实现 ✅。
2. Maven 依赖
确保项目中引入了以下核心依赖:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>6.0.13</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.15.3</version>
</dependency>
🔍 说明:
jackson-databind
用于 JSON 序列化/反序列化,Spring 默认使用它处理@RequestBody
。
3. Spring 内置方案:ContentCachingRequestWrapper
Spring 提供了一个现成的工具类:ContentCachingRequestWrapper
,它会缓存请求体,支持通过 getContentAsByteArray()
多次读取。
使用方式(简单示例)
ContentCachingRequestWrapper wrapper =
new ContentCachingRequestWrapper(request);
// 可多次调用
byte[] body = wrapper.getContentAsByteArray();
⚠️ 踩坑点:局限性明显
- ❌ 不能通过
getInputStream()
或getReader()
多次读取
一旦你调用过原生getInputStream()
,缓存机制就失效了。 - ❌ 依赖 Filter 顺序
必须在其他 Filter 之前包装,否则流已被消费,缓存失败。
👉 所以它不适合复杂场景,比如你有多个 Filter 都可能提前读取流。
4. 自定义可重复读取的 Request 包装类
我们通过继承 HttpServletRequestWrapper
,实现一个真正支持多次读取的包装类:CachedBodyHttpServletRequest
。
4.1 构造函数:缓存请求体
在构造时,立即读取原始输入流并缓存为 byte[]
,避免后续流被关闭或消费。
public class CachedBodyHttpServletRequest extends HttpServletRequestWrapper {
private byte[] cachedBody;
public CachedBodyHttpServletRequest(HttpServletRequest request) throws IOException {
super(request);
InputStream requestInputStream = request.getInputStream();
this.cachedBody = StreamUtils.copyToByteArray(requestInputStream);
}
}
✅
StreamUtils.copyToByteArray()
是 Spring 提供的工具方法,安全高效。
4.2 重写 getInputStream()
返回一个自定义的 ServletInputStream
实现,每次调用都基于缓存的 byte[]
创建新流。
@Override
public ServletInputStream getInputStream() throws IOException {
return new CachedBodyServletInputStream(this.cachedBody);
}
4.3 重写 getReader()
支持通过 BufferedReader
读取,适用于表单或文本类请求。
@Override
public BufferedReader getReader() throws IOException {
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(this.cachedBody);
return new BufferedReader(new InputStreamReader(byteArrayInputStream));
}
5. 实现自定义 ServletInputStream
我们需要一个 ServletInputStream
的实现类,来支持容器规范。
5.1 构造函数
public class CachedBodyServletInputStream extends ServletInputStream {
private InputStream cachedBodyInputStream;
public CachedBodyServletInputStream(byte[] cachedBody) {
this.cachedBodyInputStream = new ByteArrayInputStream(cachedBody);
}
}
5.2 重写 read()
代理到 ByteArrayInputStream
的读取逻辑。
@Override
public int read() throws IOException {
return cachedBodyInputStream.read();
}
5.3 重写 isFinished()
判断流是否读取完毕。
@Override
public boolean isFinished() {
try {
return cachedBodyInputStream.available() == 0;
} catch (IOException e) {
return true;
}
}
5.4 重写 isReady()
由于数据已全部缓存,始终可读。
@Override
public boolean isReady() {
return true;
}
✅ 注意:
isReady()
在非阻塞 IO 中使用,这里我们简单返回true
即可。
6. 注册 Filter:全局启用缓存
通过 OncePerRequestFilter
在请求进入时包装 HttpServletRequest
,确保后续所有组件拿到的都是可重复读的版本。
@Component
@Order(Ordered.HIGHEST_PRECEDENCE + 1)
public class CachedBodyFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
// 只对 POST、PUT 等有请求体的接口包装
if ("POST".equals(request.getMethod()) || "PUT".equals(request.getMethod())) {
CachedBodyHttpServletRequest cachedRequest = new CachedBodyHttpServletRequest(request);
filterChain.doFilter(cachedRequest, response);
} else {
filterChain.doFilter(request, response);
}
}
}
🔍 关键点
- ✅
@Order(HIGHEST_PRECEDENCE + 1)
:确保在其他 Filter 之前执行 - ✅ 只包装有请求体的请求,避免无谓开销
- ✅ 使用
OncePerRequestFilter
,保证每个请求只执行一次
7. 总结
方案 | 是否推荐 | 说明 |
---|---|---|
ContentCachingRequestWrapper |
⚠️ 有限使用 | 简单场景可用,但易踩坑 |
自定义 CachedBodyHttpServletRequest |
✅ 强烈推荐 | 真正支持多次 getInputStream() 调用 |
我们通过以下步骤实现了请求体的安全多次读取:
- ✅ 缓存原始请求体为
byte[]
- ✅ 重写
getInputStream()
和getReader()
- ✅ 实现
ServletInputStream
支持容器规范 - ✅ 通过 Filter 全局包装请求
💡 源码已托管至 GitHub:https://github.com/yourname/spring-request-body-cache
这个方案在生产环境稳定运行,适用于日志、审计、签名等场景,建议集合备用。