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() 调用

我们通过以下步骤实现了请求体的安全多次读取

  1. ✅ 缓存原始请求体为 byte[]
  2. ✅ 重写 getInputStream()getReader()
  3. ✅ 实现 ServletInputStream 支持容器规范
  4. ✅ 通过 Filter 全局包装请求

💡 源码已托管至 GitHub:https://github.com/yourname/spring-request-body-cache

这个方案在生产环境稳定运行,适用于日志、审计、签名等场景,建议集合备用。


原始标题:Reading HttpServletRequest Multiple Times in Spring