Java中HttpServletRequest请求体默认不可重复读取,需在Filter中用ContentCachingRequestWrapper尽早缓存,再于异常处理器中安全获取日志内容。

在 Java 中,HttpServletRequest 的请求体(request body)默认是**不可重复读取**的——一旦被读取(如通过 getInputStream() 或 getReader()),后续再尝试读取就会返回空或抛出异常。这导致在异常处理(如 catch 块)中想记录原始请求体时经常失败。
核心问题:请求体流只能消费一次
Servlet 规范规定,HttpServletRequest.getInputStream() 和 getReader() 返回的流/Reader 是单次消费的。框架(如 Spring MVC)在参数解析、@RequestBody 绑定等过程中已提前读取过一次,到 catch 时原始流已关闭或耗尽。
解决方案:使用可重复读取的包装类(推荐)
最可靠的方式是在请求进入链路**最初阶段**就将原始请求体缓存到内存(或临时存储),并用自定义包装类替换原始 request,使其支持多次读取。Spring 提供了现成支持:
-
使用
ContentCachingRequestWrapper(Spring 内置):
它会自动缓存请求体内容(最多缓存maxRequestSize字节),并重写getInputStream()和getReader()方法,返回可重复读取的缓存副本。 -
在 Filter 中尽早包装 request:
必须在任何可能触发 body 读取的组件(如@RequestBody处理器、FormContentFilter)之前完成包装,否则缓存会为空。
示例(Spring Boot Filter):
立即学习“Java免费学习笔记(深入)”;
@Component
public class RequestCachingFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
// 只对有 body 的请求(POST/PUT/PATCH)缓存,避免 GET 浪费内存
if ("POST".equalsIgnoreCase(httpRequest.getMethod()) ||
"PUT".equalsIgnoreCase(httpRequest.getMethod()) ||
"PATCH".equalsIgnoreCase(httpRequest.getMethod())) {
ContentCachingRequestWrapper wrappedRequest =
new ContentCachingRequestWrapper(httpRequest);
chain.doFilter(wrappedRequest, response);
return;
}
chain.doFilter(request, response);
}
}
在 catch 块中安全读取并记录
确保上述 Filter 已生效后,在异常处理器或全局 @ExceptionHandler 中即可安全提取缓存内容:
- 从
HttpServletRequest向上转型为ContentCachingRequestWrapper; - 调用
getContentAsByteArray()获取原始字节,再按编码转为字符串(注意处理空值和编码); - 建议限制日志长度(如截取前 1024 字符),避免敏感信息泄露或日志爆炸。
示例(全局异常处理器):
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
public ResponseEntity<String> handleException(HttpServletRequest request, Exception e) {
String requestBody = getRequestBody(request);
log.error("请求异常,URI: {}, Method: {}, Body: {}",
request.getRequestURI(), request.getMethod(), requestBody, e);
return ResponseEntity.status(500).body("服务内部错误");
}
private String getRequestBody(HttpServletRequest request) {
if (!(request instanceof ContentCachingRequestWrapper)) {
return "[未缓存:非 POST/PUT/PATCH 请求 或 Filter 未生效]";
}
ContentCachingRequestWrapper wrapper = (ContentCachingRequestWrapper) request;
byte[] content = wrapper.getContentAsByteArray();
if (content.length == 0) return "[空请求体]";
try {
String charset = Optional.ofNullable(wrapper.getCharacterEncoding())
.orElse("UTF-8");
return new String(content, charset).substring(0, Math.min(1024, content.length));
} catch (Exception ex) {
return "[解码失败:" + ex.getMessage() + "]";
}
}
}
注意事项与替代方案
-
内存开销:缓存整个 body 会增加堆内存压力,尤其大文件上传场景。应配置
ContentCachingRequestWrapper的最大缓存大小(需自定义构造),或改用流式日志(如只记录摘要、哈希); - 敏感数据脱敏:日志中务必过滤密码、token、身份证号等字段,不可直接打印原始 JSON;
-
非 Spring 环境:可手动实现
HttpServletRequestWrapper,用ByteArrayInputStream缓存字节,但需自行处理字符编码和流生命周期; - 不要在 Controller 层 try-catch 后再读 body:此时 body 很可能已被框架提前读取,缓存为空——必须依赖前置 Filter 的统一包装。
不复杂但容易忽略:关键在于「缓存时机必须早于任何 body 消费」,而不是在 catch 里想办法“重新打开流”。


















