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

Spring Boot 2.7中请求日志过滤器无法捕获JSON解析异常的解决方案咨询

Spring Boot 2.7中请求日志过滤器无法捕获JSON解析异常的解决方案咨询

这个问题我之前也碰到过,核心原因是Spring MVC的异常处理机制会拦截这类参数解析异常,不会把它抛回你的过滤器链。下面给你几个适合Spring Boot 2.7的实用解决方案:


为什么你的过滤器抓不到这类异常?

像HttpMessageNotReadableException这类JSON解析异常,是在Spring MVC的请求参数绑定阶段抛出的。Spring MVC默认的HandlerExceptionResolver(比如DefaultHandlerExceptionResolver)会自动捕获这类异常,将其转换为对应的HTTP错误响应(比如400 Bad Request),并不会把异常传播到你自定义Filter的doFilter方法的catch块中。所以你的过滤器里的异常捕获逻辑根本拿不到这个异常,自然也就无法记录对应的请求详情。


解决方案1:用@ControllerAdvice专门捕获JSON解析异常

这种方式最直观,专门针对参数解析类异常做处理,同时可以完整获取请求的所有详情(方法、URI、请求体、耗时等)来记录日志。

步骤1:创建全局异常处理类

import jakarta.servlet.http.HttpServletRequest;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.util.ContentCachingRequestWrapper;

import java.nio.charset.StandardCharsets;

@ControllerAdvice
public class GlobalRequestExceptionLogger {

    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(GlobalRequestExceptionLogger.class);

    @ExceptionHandler(HttpMessageNotReadableException.class)
    public void handleJsonParseError(HttpMessageNotReadableException ex, HttpServletRequest request) {
        // 包装请求,确保能重复读取请求体(原始请求流只能读一次)
        ContentCachingRequestWrapper requestWrapper =
                request instanceof ContentCachingRequestWrapper ?
                        (ContentCachingRequestWrapper) request :
                        new ContentCachingRequestWrapper(request);

        // 从请求属性中获取过滤器存入的开始时间(需要在你原有的过滤器里添加这一步)
        long startTime = (Long) requestWrapper.getAttribute("requestStartTime");
        long duration = System.currentTimeMillis() - startTime;

        // 提取请求核心信息
        String method = requestWrapper.getMethod();
        String uri = requestWrapper.getRequestURI();
        String queryString = requestWrapper.getQueryString();

        // 格式化请求体(和你原有过滤器的逻辑保持一致)
        String bodyFormatted = "No Body";
        byte[] requestBody = requestWrapper.getContentAsByteArray();
        if (requestBody != null && requestBody.length > 0) {
            bodyFormatted = new String(requestBody, StandardCharsets.UTF_8)
                    .replace("\n", "")
                    .replace("\r", "")
                    .trim();
        }

        // 记录带异常栈的错误日志
        log.error(
                "Request {} for {} FAILED (JSON Parse Error), duration: {}ms, queryString: {}, requestBody: {}",
                method, uri, duration, queryString, bodyFormatted, ex
        );

        // 如果你需要返回自定义响应,这里可以添加逻辑,比如:
        // return ResponseEntity.badRequest().body("Invalid request JSON format");
    }
}

步骤2:修改原过滤器,存入请求开始时间

在你原有的Filter的doFilter方法里,创建requestWrapper之后,添加一行代码存入请求开始时间:

// 在chain.doFilter之前添加
requestWrapper.setAttribute("requestStartTime", System.currentTimeMillis());

解决方案2:扩展HandlerExceptionResolver统一处理所有Spring MVC异常

如果你想统一记录所有Spring MVC捕获的异常(不止JSON解析异常),可以扩展默认的异常解析器,在处理异常时自动记录请求日志。

步骤1:自定义异常解析器

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver;
import org.springframework.web.util.ContentCachingRequestWrapper;

import java.nio.charset.StandardCharsets;

public class LoggingHandlerExceptionResolver extends DefaultHandlerExceptionResolver {

    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(LoggingHandlerExceptionResolver.class);

    @Override
    protected ModelAndView doResolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        // 先记录请求错误日志
        if (request instanceof ContentCachingRequestWrapper requestWrapper) {
            long startTime = (Long) requestWrapper.getAttribute("requestStartTime");
            long duration = System.currentTimeMillis() - startTime;

            String method = requestWrapper.getMethod();
            String uri = requestWrapper.getRequestURI();
            String queryString = requestWrapper.getQueryString();

            String bodyFormatted = "No Body";
            byte[] requestBody = requestWrapper.getContentAsByteArray();
            if (requestBody != null && requestBody.length > 0) {
                bodyFormatted = new String(requestBody, StandardCharsets.UTF_8)
                        .replace("\n", "")
                        .replace("\r", "")
                        .trim();
            }

            log.error(
                    "Request {} for {} FAILED, duration: {}ms, queryString: {}, requestBody: {}",
                    method, uri, duration, queryString, bodyFormatted, ex
            );
        }

        // 调用父类方法,继续生成默认的错误响应
        return super.doResolveException(request, response, handler, ex);
    }
}

步骤2:注册自定义解析器到Spring容器

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.HandlerExceptionResolver;

@Configuration
public class WebMvcConfig {

    @Bean
    public HandlerExceptionResolver loggingHandlerExceptionResolver() {
        return new LoggingHandlerExceptionResolver();
    }
}

同样,需要在你的原过滤器里添加requestStartTime的存入逻辑,和方案1的步骤2一致。


解决方案3:基于响应状态码判断请求是否失败(简单版)

如果你的需求只是记录失败请求的基本信息,不需要异常栈,可以直接修改原过滤器的判断逻辑:不再依赖success布尔值,而是通过响应状态码判断请求是否成功(比如2xx视为成功,非2xx视为失败)。

修改原过滤器的finally块逻辑:

finally {
    long duration = System.currentTimeMillis() - startTime;
    HttpServletResponse httpResponse = (HttpServletResponse) response;
    boolean isSuccess = httpResponse.getStatus() / 100 == 2; // 2xx状态码视为成功

    if (isSuccess) {
        log.info("Request {} {} took {} ms", requestWrapper.getMethod(), requestWrapper.getRequestURI(), duration);
    } else {
        // 这里复用你原有的请求详情格式化逻辑
        String query = requestWrapper.getQueryString();
        byte[] requestBody = requestWrapper.getContentAsByteArray();
        String bodyFormatted = "No Body";
        if (requestBody != null && requestBody.length > 0) {
            bodyFormatted = new String(requestBody, StandardCharsets.UTF_8)
                    .replace("\n", "")
                    .replace("\r", "")
                    .trim();
        }
        log.error("Request {} for {} FAILED, duration: {}, statusCode: {}, queryString: {}, requestBody: {}",
                requestWrapper.getMethod(), requestWrapper.getRequestURI(), duration,
                httpResponse.getStatus(), query, bodyFormatted);
    }
}

这种方式的优点是不需要修改额外代码,直接改过滤器逻辑;缺点是无法获取异常的栈信息,只能知道请求失败了。


关键注意点:确保请求体可重复读取

不管用哪种方案,都必须在你的过滤器里优先把原始请求包装成ContentCachingRequestWrapper,并将包装后的请求传入过滤器链:

ContentCachingRequestWrapper requestWrapper = new ContentCachingRequestWrapper((HttpServletRequest) request);
// 存入开始时间
requestWrapper.setAttribute("requestStartTime", System.currentTimeMillis());
// 传入包装后的请求
chain.doFilter(requestWrapper, response);

因为原始HttpServletRequest的输入流只能读取一次,包装后才能让后续的Spring MVC和你的日志逻辑重复读取请求体。

内容来源于stack exchange

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.08 03:10:20