如何在HttpServletRequest中重置请求输入流?Spring拦截器场景
解决Spring拦截器读取请求体后控制器获取为null的问题
这个问题我之前也碰到过,核心原因是HttpServletRequest的输入流是一次性读取的——拦截器里读完请求体后,输入流的指针就到末尾了,后续控制器再读取时就拿不到任何内容,自然返回null。不过咱们可以通过包装请求的方式,把请求体缓存起来实现重复读取,具体方案如下:
1. 自定义请求包装类,缓存请求体
首先创建一个继承HttpServletRequestWrapper的类,把原始请求体读取到字节数组里缓存起来,重写getInputStream()和getReader()方法,让后续读取都从缓存的字节数组中获取:
import javax.servlet.ReadListener; import javax.servlet.ServletInputStream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequestWrapper; import java.io.*; import java.nio.charset.StandardCharsets; public class CachedBodyHttpServletRequest extends HttpServletRequestWrapper { private final byte[] cachedBody; public CachedBodyHttpServletRequest(HttpServletRequest request) throws IOException { super(request); // 读取原始请求体并缓存到字节数组 InputStream requestInputStream = request.getInputStream(); this.cachedBody = requestInputStream.readAllBytes(); } @Override public ServletInputStream getInputStream() throws IOException { // 返回基于缓存字节数组的输入流 return new ServletInputStream() { private final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(cachedBody); @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 readListener) { // 同步场景下无需实现,异步场景可按需补充 } }; } @Override public BufferedReader getReader() throws IOException { return new BufferedReader(new InputStreamReader(getInputStream(), StandardCharsets.UTF_8)); } // 提供方法直接获取缓存的请求体字符串,方便拦截器日志记录 public String getCachedBody() { return new String(cachedBody, StandardCharsets.UTF_8); } }
2. 创建Filter,替换原始请求为包装后的请求
注意这里要用Filter而不是Interceptor,因为Filter的执行时机在DispatcherServlet之前,我们需要在请求到达拦截器和控制器之前就完成请求的包装,这样后续所有组件使用的都是可重复读取的请求:
import javax.servlet.*; import javax.servlet.annotation.WebFilter; import javax.servlet.http.HttpServletRequest; import java.io.IOException; @WebFilter(urlPatterns = "/*") public class CachedBodyFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { ServletRequest wrappedRequest = request; // 判断是否为Http请求,替换为自定义包装类 if (request instanceof HttpServletRequest) { wrappedRequest = new CachedBodyHttpServletRequest((HttpServletRequest) request); } // 传递包装后的请求到后续流程 chain.doFilter(wrappedRequest, response); } @Override public void init(FilterConfig filterConfig) throws ServletException {} @Override public void destroy() {} }
3. 在拦截器中读取缓存的请求体
现在拦截器里就可以安全地读取请求体了,而且不会影响后续控制器的获取:
import org.springframework.web.servlet.HandlerInterceptor; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; public class LoggingInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 判断请求是否为我们包装后的类型 if (request instanceof CachedBodyHttpServletRequest) { CachedBodyHttpServletRequest cachedRequest = (CachedBodyHttpServletRequest) request; String requestBody = cachedRequest.getCachedBody(); // 这里执行日志记录逻辑,比如打印到控制台或存入日志系统 System.out.println("请求体日志:" + requestBody); } return true; } }
4. 配置组件生效
最后需要让Filter和Interceptor在Spring Boot中生效:
- 对于Filter:在启动类上添加
@ServletComponentScan注解,扫描自定义的Filter; - 对于Interceptor:配置
WebMvcConfigurer注册拦截器:
import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LoggingInterceptor()) .addPathPatterns("/**"); // 指定需要拦截的接口路径 } }
注意事项
如果你的系统有超大请求体的场景,缓存字节数组可能会占用较多内存,这时可以考虑添加请求体大小限制,或者只针对需要日志记录的接口做包装,避免不必要的内存消耗。
内容的提问来源于stack exchange,提问作者user6543599
相关产品推荐
相关产品推荐

