解决 HTTP 请求 RequestBody 只能被读取一次的问题

简介

  • HTTP 请求 RequestBody 只能被读取一次HttpServletRequest 的输入流 (InputStream) 在被读取后会被关闭,导致后续无法再次读取。
  • 本文将介绍如何通过 请求包装类 (RequestWrapper) 来解决这个问题。

问题背景

当我们需要在以下场景中多次读取 RequestBody 时,就会遇到这个问题:

  1. 日志记录:需要在拦截器或过滤器中记录请求体
  2. 参数校验:需要在多个地方校验请求体数据
  3. 数据解析 :需要在不同的组件中解析请求体(如 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);
    }
}

方案优势

  1. 性能优化:只在构造时读取一次请求体,后续从缓存读取
  2. 线程安全:每个请求有自己的包装实例,互不干扰
  3. 兼容性 :完全兼容 HttpServletRequest 的所有方法
  4. 灵活性:可以随时获取原始请求体数据

注意事项

  1. 大文件处理:对于大文件上传,缓存整个请求体可能消耗较多内存
  2. 流式处理:如果确实需要流式处理大数据,不宜使用此方案
  3. Filter 顺序:确保此 Filter 在其他需要读取请求体的 Filter 之前执行