SpringBoot REST中如何在ExceptionHandler中获取请求体与查询参数
问题根因
所有尝试拿不到@RequestBody内容的核心原因只有一个:Servlet规范中HttpServletRequest的输入流仅支持读取一次,Spring MVC在执行Controller方法参数解析时,已经把请求体对应的流完全读取并反序列化为POJO对象,流读取完毕后会自动关闭,后续无论是直接调用request.getInputStream()、尝试注入请求POJO,拿到的都只会是空值。
同类方案失效,基本都是因为没有在Spring MVC读取请求流之前完成请求体缓存,等异常抛出时流已经没有剩余内容可读。
落地实现步骤
1. 实现请求体缓存逻辑
首先自定义可重复读的Request包装类,把请求体内容提前缓存到字节数组中:
import jakarta.servlet.ReadListener; import jakarta.servlet.ServletInputStream; import jakarta.servlet.http.HttpServletRequest; import jakarta.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() { return new CachedServletInputStream(new ByteArrayInputStream(cachedBody)); } @Override public BufferedReader getReader() { return new BufferedReader(new InputStreamReader(getInputStream(), StandardCharsets.UTF_8)); } public byte[] getCachedBody() { return cachedBody; } private static class CachedServletInputStream extends ServletInputStream { private final ByteArrayInputStream inputStream; public CachedServletInputStream(ByteArrayInputStream inputStream) { this.inputStream = inputStream; } @Override public boolean isFinished() { return inputStream.available() == 0; } @Override public boolean isReady() { return true; } @Override public void setReadListener(ReadListener readListener) { // 同步阻塞IO无需额外实现 } @Override public int read() { return inputStream.read(); } } }
2. 注册最高优先级的缓存过滤器
必须保证过滤器在所有请求处理逻辑(包括Spring MVC参数解析、自定义拦截器、安全框架过滤器)之前执行,把原始Request替换为缓存后的包装对象:
import jakarta.servlet.*; import jakarta.servlet.http.HttpServletRequest; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; import java.io.IOException; @Component @Order(Ordered.HIGHEST_PRECEDENCE) public class CachedBodyFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { // 仅缓存带请求体的HTTP方法,减少不必要的内存占用 if (request instanceof HttpServletRequest httpRequest && ("POST".equalsIgnoreCase(httpRequest.getMethod()) || "PUT".equalsIgnoreCase(httpRequest.getMethod()) || "PATCH".equalsIgnoreCase(httpRequest.getMethod()))) { CachedBodyHttpServletRequest wrappedRequest = new CachedBodyHttpServletRequest(httpRequest); chain.doFilter(wrappedRequest, response); return; } chain.doFilter(request, response); } }
如果项目使用Spring Security,需要额外在Security配置中将该过滤器添加到安全过滤器链最前端,避免安全框架提前读空流:
@Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .addFilterBefore(new CachedBodyFilter(), UsernamePasswordAuthenticationFilter.class) // 保留原有Security配置即可 .authorizeHttpRequests(auth -> auth.anyRequest().authenticated()); return http.build(); }
3. 在全局异常处理器中读取参数
在@ExceptionHandler标注的方法中,直接从包装后的Request对象中提取缓存的请求体;查询参数存储在URL中,不依赖请求流,可以直接通过Servlet原生方法获取,不需要额外缓存:
import jakarta.servlet.http.HttpServletRequest; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import java.nio.charset.StandardCharsets; import java.util.Enumeration; import java.util.HashMap; import java.util.Map; @ControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(Exception.class) public Object handleBizException(Exception e, HttpServletRequest request) { // 读取缓存的请求体内容 String requestBody = ""; if (request instanceof CachedBodyHttpServletRequest cachedRequest) { requestBody = new String(cachedRequest.getCachedBody(), StandardCharsets.UTF_8); // 需要转POJO的话,直接用项目内的JSON工具反序列化requestBody即可 } // 读取Query Param查询参数 Map<String, String> queryParams = new HashMap<>(); Enumeration<String> paramNames = request.getParameterNames(); while (paramNames.hasMoreElements()) { String paramName = paramNames.nextElement(); queryParams.put(paramName, request.getParameter(paramName)); } // 基于requestBody、queryParams组装自定义错误响应即可 return null; } }
避坑说明
- 不要在
@ExceptionHandler方法的参数列表中直接声明@RequestBody对应的POJO类,Spring不会为异常处理方法注入请求体参数,必然返回null - 注意Spring版本对应的Servlet包路径:Spring Boot 2.x对应
javax.servlet.*,Spring Boot 3.x/Spring 6+对应jakarta.servlet.*,包导错会导致过滤器不生效 - 大文件上传、流处理类接口不要走请求体缓存逻辑,会导致全量内容加载进内存引发OOM,可以在过滤器中通过请求路径、Content-Type做白名单/黑名单过滤
- 如果存在其他自定义过滤器读取请求流,必须保证
CachedBodyFilter的执行顺序在所有读流逻辑之前,否则缓存的内容为空
内容的提问来源于stack exchange,提问作者justina antony
相关产品推荐
相关产品推荐

