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方法中读取:
- 全局异常处理器:
@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); } }
- 拦截器的
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
相关产品推荐
相关产品推荐

