You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.21 04:06:17