You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在拦截器中读取请求体且不影响下游控制器获取该请求体?

如何在拦截器中读取请求体且不影响下游控制器获取该请求体?

这个问题我之前做接口日志的时候也踩过坑,Servlet规范里请求流默认只能被读取一次,拦截器先读了之后,下游控制器再调用getInputStream()或者getReader()就会直接返回空或者抛出异常。你已经找对了核心方向——用HttpServletRequestWrapper包装请求来缓存请求体,我来帮你补全完整的实现方案。

核心思路

自定义一个继承自HttpServletRequestWrapper的包装类,在初始化时把请求体完整读取到内存(或者临时文件)中,然后重写getInputStream()和getReader()方法,每次调用都返回基于缓存数据的新流,这样不管是拦截器还是控制器,都能独立读取请求体。

完整的ClonedHttpServletRequest实现

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequestWrapper;
import jakarta.servlet.ServletInputStream;
import jakarta.servlet.ReadListener;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

public class ClonedHttpServletRequest extends HttpServletRequestWrapper {

    private final byte[] body;
    private final Map<String, String> customHeaders = new HashMap<>();

    public ClonedHttpServletRequest(HttpServletRequest request) throws IOException {
        super(request);
        // 读取并缓存请求体到字节数组
        try (InputStream inputStream = request.getInputStream()) {
            this.body = inputStream.readAllBytes();
        }
        // 复制原始请求的所有头信息(如果需要自定义头可以用下面的方法)
        Collections.list(request.getHeaderNames()).forEach(headerName ->
                customHeaders.put(headerName, request.getHeader(headerName))
        );
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {
        // 返回基于缓存字节数组的新输入流,每次调用都是独立的
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body);
        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 listener) {
                // 同步场景下不需要实现,异步场景可根据需求补充
            }
        };
    }

    @Override
    public BufferedReader getReader() throws IOException {
        // 基于缓存字节数组构造Reader,指定请求编码避免乱码
        String encoding = getCharacterEncoding() != null ? getCharacterEncoding() : StandardCharsets.UTF_8.name();
        return new BufferedReader(new InputStreamReader(getInputStream(), encoding));
    }

    // 提供直接获取缓存请求体的方法,方便拦截器快速使用
    public byte[] getBody() {
        return body.clone();
    }

    // 自定义添加头信息的方法(可选)
    public void addCustomHeader(String name, String value) {
        customHeaders.put(name, value);
    }

    @Override
    public String getHeader(String name) {
        // 优先返回自定义头,没有则返回原始请求的头
        String customHeader = customHeaders.get(name);
        return customHeader != null ? customHeader : super.getHeader(name);
    }
}

关键代码说明

  1. 构造方法:初始化时一次性读取原始请求的输入流,把内容缓存到body字节数组中,同时复制原始请求的头信息。
  2. 重写getInputStream():每次调用都返回基于缓存字节数组的ServletInputStream,保证每次读取都是独立的流,不会被耗尽。
  3. 重写getReader():基于缓存的流构造BufferedReader,同时处理字符编码,避免乱码问题。
  4. getBody()方法:给拦截器提供直接获取请求体字节数组的快捷方式,不用重复读流。

如何在拦截器中使用?

注意:不能直接在拦截器里包装请求,因为Spring的拦截器是在DispatcherServlet之后执行的,这时候原始请求的流可能已经被读取过了。需要先通过过滤器把请求包装好,再传递给后续的拦截器和控制器。

1. 实现请求包装过滤器

import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import java.io.IOException;

public class RequestWrapperFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        // 只包装有请求体的请求(POST/PUT/PATCH等)
        if (request instanceof HttpServletRequest httpServletRequest) {
            String method = httpServletRequest.getMethod().toLowerCase();
            if ("post".equals(method) || "put".equals(method) || "patch".equals(method)) {
                ClonedHttpServletRequest wrappedRequest = new ClonedHttpServletRequest(httpServletRequest);
                chain.doFilter(wrappedRequest, response);
                return;
            }
        }
        // GET等无请求体的请求直接放行
        chain.doFilter(request, response);
    }
}

2. 注册过滤器(Spring Boot环境)

如果你用的是Spring Boot,可以通过两种方式注册过滤器:

  • 方式一:用@WebFilter注解
    给RequestWrapperFilter添加@WebFilter(urlPatterns = "/*"),然后在启动类上添加@ServletComponentScan注解。

  • 方式二:用FilterRegistrationBean

import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class FilterConfig {

    @Bean
    public FilterRegistrationBean<RequestWrapperFilter> requestWrapperFilter() {
        FilterRegistrationBean<RequestWrapperFilter> registrationBean = new FilterRegistrationBean<>();
        registrationBean.setFilter(new RequestWrapperFilter());
        // 指定要拦截的路径,这里拦截所有请求
        registrationBean.addUrlPatterns("/*");
        // 设置过滤器优先级,确保它在其他过滤器之前执行
        registrationBean.setOrder(1);
        return registrationBean;
    }
}

3. 在拦截器中读取请求体

现在在拦截器的preHandle方法里,就可以安全地读取请求体了,而且不会影响下游控制器:

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.HandlerInterceptor;
import java.nio.charset.StandardCharsets;

public class RequestBodyInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 把请求强转成我们的包装类
        if (request instanceof ClonedHttpServletRequest clonedRequest) {
            // 读取请求体
            String requestBody = new String(clonedRequest.getBody(), StandardCharsets.UTF_8);
            // 这里可以做日志记录、参数校验等操作
            System.out.println("拦截器读取到请求体:" + requestBody);
        }
        return true;
    }
}

注意事项

  • 大请求体处理:如果你的应用会接收很大的请求体(比如几百MB),把整个请求体读到内存里可能会导致OOM,这时候可以考虑用临时文件来缓存请求体,而不是字节数组。
  • 字符编码一致性:确保拦截器和控制器使用相同的字符编码读取请求体,避免乱码问题。
  • 异步请求:如果你的应用使用了异步Servlet,需要补充实现ServletInputStream的setReadListener方法,适配异步场景。

备注:内容来源于stack exchange,提问作者RIO

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.16 10:29:34