【问题标题】:Log Spring Boot controller exceptions without changing the error response body or status code在不更改错误响应正文或状态码的情况下记录 Spring Boot 控制器异常
【发布时间】:2020-08-12 12:39:50
【问题描述】:

感谢DefaultErrorAttributes,Spring Boot 默认情况下会返回一个响应正文,用于开箱即用地满足我的需求:

{
  "timestamp": 1588022957431,
  "status": 400,
  "error": "Bad Request",
  "exception": "org.springframework.web.bind.MethodArgumentNotValidException",
  "errors": [
    {
      "codes": [
        "NotEmpty.myResource.label",
        "NotEmpty.label",
        "NotEmpty.java.lang.String",
        "NotEmpty"
      ],
      "arguments": [
        {
          "codes": [
            "myResource.label",
            "label"
          ],
          "arguments": null,
          "defaultMessage": "label",
          "code": "label"
        }
      ],
      "defaultMessage": "must not be empty",
      "objectName": "myResource",
      "field": "label",
      "rejectedValue": null,
      "bindingFailure": false,
      "code": "NotEmpty"
    }
  ],
  "message": "Validation failed for object='myResource'. Error count: 1"
}

此外,在DefaultHandlerExceptionResolver 提供的内置异常处理和我自己的自定义错误处理之间,我有异常返回我希望我的 REST API 公开的响应代码。

我想做的是记录抛出的异常以用于调试目的。如果异常导致 4xx 响应,我想将其记录为调试级别,因为这是客户端请求的问题,而不是我通常需要在生产中登录的东西(但在调试特定问题或测试代码的正确性)。如果它导致 5xx 异常,我想将它记录在警告级别,因为这表明服务器出现了意外问题,我可能希望了解它。

在保留默认 Spring Boot 响应正文以及我的自定义状态代码映射的同时,我没有看到添加此日志记录的良好机制。如何在保留当前响应正文和状态代码的同时根据状态代码执行此条件日志记录?更好的是,有没有一种方法也适用于我拥有的少数端点,这些端点使用自己定制的@ExceptionHandler 方法返回不同的响应正文?

【问题讨论】:

    标签: java spring spring-boot spring-mvc exception


    【解决方案1】:

    我使用javax.servlet.Filter 解决了这个问题,它从ErrorAttributes 检索抛出的异常:

    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.web.servlet.error.ErrorAttributes;
    import org.springframework.boot.web.servlet.filter.OrderedFilter;
    import org.springframework.http.HttpStatus;
    import org.springframework.stereotype.Component;
    import org.springframework.web.context.request.ServletWebRequest;
    import org.springframework.web.filter.OncePerRequestFilter;
    
    import javax.servlet.FilterChain;
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    import java.util.Optional;
    
    @Slf4j
    @Component
    public class ExceptionLoggingFilter extends OncePerRequestFilter implements OrderedFilter {
        @Autowired
        private ErrorAttributes errorAttributes;
    
        @Override
        public int getOrder() {
            return OrderedFilter.REQUEST_WRAPPER_FILTER_MAX_ORDER;
        }
    
        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
                throws ServletException, IOException {
            filterChain.doFilter(request, response);
    
            logError(request, response);
        }
    
        private void logError(HttpServletRequest request, HttpServletResponse response) {
            Throwable error = errorAttributes.getError(new ServletWebRequest(request));
            if (error == null) {
                return;
            }
    
            int statusCode = response.getStatus();
            String reasonPhrase = Optional.ofNullable(HttpStatus.resolve(statusCode))
                    .map(HttpStatus::getReasonPhrase).orElse("<nonstandard status code>");
    
            String uriString = request.getRequestURI()
                    + Optional.ofNullable(request.getQueryString()).map(q -> "?" + q).orElse("");
    
            HttpStatus.Series series = HttpStatus.Series.resolve(statusCode);
            if (HttpStatus.Series.SERVER_ERROR.equals(series)) {
                log.warn("{} {} response for {}", statusCode, reasonPhrase, uriString, error);
            } else if (HttpStatus.Series.CLIENT_ERROR.equals(series)){
                log.debug("{} {} response for {}", statusCode, reasonPhrase, uriString, error);
            }
        }
    }
    

    【讨论】:

    • 你能提供一种方法来测试这个解决方案吗?
    【解决方案2】:

    我想出的一种处理DefaultErrorAttributes 情况的解决方案,但不是自定义@ExceptionHandler 一种,是覆盖内置的Spring Boot 错误控制器(由ErrorMvcAutoConfiguration.basicErrorController() 定义)以记录返回结果之前的异常:

    import lombok.extern.slf4j.Slf4j;
    import org.springframework.boot.autoconfigure.web.ErrorProperties;
    import org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController;
    import org.springframework.boot.autoconfigure.web.servlet.error.ErrorViewResolver;
    import org.springframework.boot.web.servlet.error.ErrorAttributes;
    import org.springframework.http.HttpStatus;
    import org.springframework.http.MediaType;
    import org.springframework.http.ResponseEntity;
    import org.springframework.web.context.request.ServletWebRequest;
    import org.springframework.web.servlet.ModelAndView;
    
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.util.List;
    import java.util.Map;
    
    @Slf4j
    public class LoggingErrorController extends BasicErrorController {
        private final ErrorAttributes errorAttributes;
    
        public LoggingErrorController(ErrorAttributes errorAttributes, ErrorProperties errorProperties,
                                      List<ErrorViewResolver> errorViewResolvers) {
            super(errorAttributes, errorProperties, errorViewResolvers);
            this.errorAttributes = errorAttributes;
        }
    
        @Override
        public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
            logError(request, isIncludeStackTrace(request, MediaType.TEXT_HTML));
            return super.errorHtml(request, response);
        }
    
        @Override
        public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
            logError(request, isIncludeStackTrace(request, MediaType.ALL));
            return super.error(request);
        }
    
        private void logError(HttpServletRequest request, boolean includeStackTrace) {
            Throwable error = errorAttributes.getError(new ServletWebRequest(request));
            Map<String, Object> attributesMap = getErrorAttributes(request, includeStackTrace);
    
            Object path = attributesMap.getOrDefault("path", "<unknown>");
    
            HttpStatus status = getStatus(request);
            if (status.is4xxClientError()) {
                log.debug("{} {} error for path {}", status.value(), status.getReasonPhrase(), path, error);
            } else {
                log.warn("{} {} error for path {}", status.value(), status.getReasonPhrase(), path, error);
            }
        }
    }
    

    我觉得它有点 hacky,并且非常依赖于当前的 Spring Boot 实现,但它似乎可以工作并且感觉非常可靠。

    【讨论】:

      猜你喜欢
      • 2017-07-01
      • 2020-08-08
      • 1970-01-01
      • 1970-01-01
      • 2016-09-09
      • 1970-01-01
      • 2015-06-11
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多