如何在拦截器中读取请求体且不影响下游控制器获取该请求体?
如何在拦截器中读取请求体且不影响下游控制器获取该请求体?
这个问题我之前做接口日志的时候也踩过坑,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); } }
关键代码说明
- 构造方法:初始化时一次性读取原始请求的输入流,把内容缓存到
body字节数组中,同时复制原始请求的头信息。 - 重写
getInputStream():每次调用都返回基于缓存字节数组的ServletInputStream,保证每次读取都是独立的流,不会被耗尽。 - 重写
getReader():基于缓存的流构造BufferedReader,同时处理字符编码,避免乱码问题。 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
相关产品推荐
相关产品推荐

