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

Spring Boot MVC如何正确记录含500在内的所有响应状态?

Spring Boot MVC请求日志误记500为200的原因及修复方案

问题成因

大多数教程里的请求日志实现依赖HandlerInterceptor,并在postHandle方法中读取响应状态码。但这里有个关键问题:当Spring Boot抛出未捕获的异常时,DispatcherServlet会跳过postHandle的执行,直接把异常交给HandlerExceptionResolver(比如全局异常处理器)处理。

此时,postHandle里拿到的还是响应对象初始化时的默认状态码200,而客户端实际收到的500是异常处理器生成的全新响应——这部分响应完全没被拦截器的postHandle捕获到,导致日志记录和实际响应不符。

修复方案

方案一:使用OncePerRequestFilter(推荐)

过滤器的执行时机早于拦截器,且覆盖了整个请求生命周期,包括异常处理阶段。配合ContentCachingResponseWrapper可以缓存响应内容,确保在响应提交后仍能获取到最终状态码和响应体。

代码示例:

@Component
public class RequestLoggingFilter extends OncePerRequestFilter {

    private static final Logger logger = LoggerFactory.getLogger(RequestLoggingFilter.class);

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // 包装请求和响应,用于缓存内容
        ContentCachingRequestWrapper wrappedRequest = new ContentCachingRequestWrapper(request);
        ContentCachingResponseWrapper wrappedResponse = new ContentCachingResponseWrapper(response);

        try {
            // 执行后续请求链
            filterChain.doFilter(wrappedRequest, wrappedResponse);
        } finally {
            // 记录请求与响应信息
            int status = wrappedResponse.getStatus();
            String requestBody = new String(wrappedRequest.getContentAsByteArray(), wrappedRequest.getCharacterEncoding());
            String responseBody = new String(wrappedResponse.getContentAsByteArray(), wrappedResponse.getCharacterEncoding());
            
            logger.info("Request: {} {} | Status: {} | Request Body: {} | Response Body: {}",
                    request.getMethod(), request.getRequestURI(), status, requestBody, responseBody);
            
            // 必须调用此方法,否则缓存的响应内容不会发送给客户端
            wrappedResponse.copyBodyToResponse();
        }
    }
}

方案二:结合拦截器与全局异常处理器

如果坚持使用HandlerInterceptor,可以在全局异常处理器中把实际响应状态码存入请求属性,再在拦截器的afterCompletion方法中读取:

  1. 全局异常处理器:
@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(Exception.class)
    public ResponseEntity<String> handleGenericException(Exception ex, HttpServletRequest request) {
        // 将实际状态码存入请求属性
        request.setAttribute("actualResponseStatus", HttpStatus.INTERNAL_SERVER_ERROR.value());
        return new ResponseEntity<>("服务器内部错误", HttpStatus.INTERNAL_SERVER_ERROR);
    }
}
  1. 拦截器的afterCompletion方法:
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
    // 优先取异常处理器存入的状态码,否则用响应对象的状态码
    Integer status = (Integer) request.getAttribute("actualResponseStatus");
    if (status == null) {
        status = response.getStatus();
    }
    
    logger.info("Request: {} {} | Status: {}", request.getMethod(), request.getRequestURI(), status);
}

注意:这种方案需要手动覆盖所有异常场景,不如过滤器方案全面,仅适合已有拦截器体系的场景。

总结

最稳妥的方式是使用OncePerRequestFilter配合ContentCachingResponseWrapper,它能覆盖请求处理的全流程,包括异常处理阶段,确保日志记录的状态码与客户端实际收到的完全一致。

内容的提问来源于stack exchange,提问作者vivi

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.28 11:36:05