Spring应用中如何在反序列化前记录JSON请求?
Spring应用JSON请求前置记录方案(无需重复序列化)
针对你遇到的HandlerInterceptor流错误、ContentCachingRequestWrapper使用异常、LoggingFilter时机滞后、ControllerAdvice重复序列化等问题,以下是满足进入RestController前记录且无需二次序列化请求对象的解决方案:
核心思路
通过自定义高优先级Filter,结合ContentCachingRequestWrapper缓存请求体,在Spring请求处理链的最前端读取原始JSON请求体并记录,同时保证后续Controller能正常读取请求体,且MDC上下文不会丢失。
实现步骤
1. 自定义请求日志Filter
这个Filter会优先于Spring的请求处理组件执行,确保在进入Controller前完成日志记录:
import jakarta.servlet.*; import jakarta.servlet.http.HttpServletRequest; import org.slf4j.MDC; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.http.MediaType; import org.springframework.stereotype.Component; import org.springframework.util.StreamUtils; import java.io.IOException; import java.nio.charset.StandardCharsets; @Component @Order(Ordered.HIGHEST_PRECEDENCE) // 确保在所有Spring内置Filter前执行 public class PreControllerRequestLoggingFilter implements Filter { // 可根据需求调整最大缓存大小,避免大请求占用过多内存 private static final int MAX_REQUEST_BODY_SIZE = 1024 * 1024; // 1MB @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest httpRequest = (HttpServletRequest) request; // 仅处理JSON类型的POST/PUT/PATCH等带请求体的请求 boolean isJsonRequest = MediaType.APPLICATION_JSON_VALUE.equalsIgnoreCase(httpRequest.getContentType()) && !"GET".equalsIgnoreCase(httpRequest.getMethod()); if (isJsonRequest) { // 包装请求,缓存请求体以支持重复读取 ContentCachingRequestWrapper wrappedRequest = new ContentCachingRequestWrapper(httpRequest, MAX_REQUEST_BODY_SIZE); // 读取原始请求体(此时会自动缓存,后续Controller读取的是缓存内容) String requestBody = StreamUtils.copyToString(wrappedRequest.getInputStream(), StandardCharsets.UTF_8); // 初始化MDC(示例:从请求头获取Trace ID) String traceId = httpRequest.getHeader("X-Trace-ID"); if (traceId != null) { MDC.put("traceId", traceId); } // 记录请求日志(可替换为SLF4J的logger.info) System.out.printf("[%s] Pre-Controller Request: %s %s | Request Body: %s%n", MDC.get("traceId"), httpRequest.getMethod(), httpRequest.getRequestURI(), requestBody); // 传递包装后的请求给后续处理链 chain.doFilter(wrappedRequest, response); // 清理MDC,避免上下文污染 MDC.remove("traceId"); } else { // 非JSON请求直接放行 chain.doFilter(request, response); } } }
2. 关键细节说明
- 解决流读取问题:
ContentCachingRequestWrapper会将第一次读取的请求体缓存到内存中,后续任何组件(包括Controller)读取请求体时,都会从缓存中获取,不会出现流关闭或I/O错误。 - 前置记录:通过
@Order(Ordered.HIGHEST_PRECEDENCE)确保Filter在Spring的RequestMappingHandlerAdapter之前执行,完全满足进入Controller前记录的要求。 - 无需重复序列化:直接读取原始请求体的JSON字符串,不需要对Controller中已反序列化的参数对象进行二次序列化,避免性能损耗和序列化不一致问题。
- MDC上下文保留:在Filter中初始化MDC,后续Controller和其他组件可以复用该上下文,日志链路一致,不会丢失Trace ID等信息。
3. 可选优化:限制缓存大小
如果存在大请求体场景,直接使用默认的ContentCachingRequestWrapper可能导致内存溢出,可自定义包装类限制缓存大小(如上述代码中已通过构造参数指定),或实现更复杂的磁盘缓存逻辑。
方案优势
- 完全覆盖需求:前置记录、无需二次序列化请求对象
- 解决所有历史问题:避免流I/O错误、请求体缺失、MDC丢失、重复序列化等问题
- 轻量高效:基于Spring原生组件实现,无需引入额外依赖
- 可扩展性强:可根据需求添加请求头过滤、敏感信息脱敏等逻辑
内容的提问来源于stack exchange,提问作者Paul Marcelin Bejan
相关产品推荐
相关产品推荐

