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

