大数据、Java EE 学习资料请关注 B 站:https://space.bilibili.com/204792350

处理 SpringBoot2.x 异常的返回页面或者 JSON 数据

默认错误处理

重要组件

配置类:ErrorMvcAutoConfiguration

重要组件:

DefaultErrorAttributes

作为设置 Model 数据存在, 在错误页面中共享数据,也就是说可以使用这些数据。

  • timestamp
  • status
  • error
  • exception
  • message
  • path
  • errors:与数据校验 JSR 303 有关
@Override
public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
    Map<String, Object> errorAttributes = new LinkedHashMap<>();
    errorAttributes.put("timestamp", new Date());
    addStatus(errorAttributes, webRequest);
    addErrorDetails(errorAttributes, webRequest, includeStackTrace);
    addPath(errorAttributes, webRequest);
    return errorAttributes;
}

BasicErrorController

这是一个控制器,由 SpringBoot 自动注册到容易中的。从名字中可以看出来,这是一个基础的错误控制器。

从实际代码看起来,可以在配置文件中配置错误请求路由:

@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {
    // ......
}

首先获取配置项:server.error.path,若是没有则获取:error.path,若是没有则默认为:/error

而默认的:/error 以及 error.path 配置项,正是 ErrorPageCustomizer 这个组件访问的默认的路径。

SpringBoot 对不同客户端的错误请求会呈现不同的处理结果,若是浏览器,手机等可视化的客户端,将会返回 text/html 文件,若是像 PostMan,其它特定需求的客户端,则返回 json 格式的数据。

@RequestMapping(produces = MediaType.TEXT_HTML_VALUE) // TEXT_HTML_VALUE = "text/html"
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
    HttpStatus status = getStatus(request);
    Map<String, Object> model = Collections
        .unmodifiableMap(getErrorAttributes(request, isIncludeStackTrace(request, MediaType.TEXT_HTML)));
    response.setStatus(status.value());
    ModelAndView modelAndView = resolveErrorView(request, response, status, model);
    return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
}

@RequestMapping
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
    HttpStatus status = getStatus(request);
    if (status == HttpStatus.NO_CONTENT) {
        return new ResponseEntity<>(status);
    }
    Map<String, Object> body = getErrorAttributes(request, isIncludeStackTrace(request, MediaType.ALL));
    return new ResponseEntity<>(body, status);
}

对于错误请求,响应有两种方式:

  • text/html
  • application/json

如下图,使用 PostMan 访问:

Snipaste_2020-05-24_20-37-53

使用浏览器访问:

Snipaste_2020-05-24_20-38-25

呈现的结果就不同。

是如何渲染错误页面?

@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
    HttpStatus status = getStatus(request);
    Map<String, Object> model = Collections
        .unmodifiableMap(getErrorAttributes(request, isIncludeStackTrace(request, MediaType.TEXT_HTML)));
    response.setStatus(status.value());
    ModelAndView modelAndView = resolveErrorView(request, response, status, model);
    return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
}

ErrorPageCustomizer

  • 根据需要,使用给定的注册表注册页面
/**
* Path of the error controller.
*/
@Value("${error.path:/error}")
private String path = "/error";

定制错误处理

通过上面的源码解析,可以知道如何自定义错误页面以及响应 Json 格式的数据。同时,还可以获取在出现这些错误情况的时候,能获取到哪些数据。

定制错误页面

  • 有模板引擎的时候,就是在模板引擎的规则下搜索。

    • 模板引擎的 prefix + /error/状态码 + 模板引擎的 suffix
    • 例如说:templates/error/404.html
  • 默认规则

    • 在能找到对应页面的时候,优先匹配精准的页面(4**,5**)

    • 若是没有找到精准的页面,那么会根据状态码除于 100 若是等于 4、5,则搜索 error 目录下的 4xx、5xx 页面。

    • 若是以上规则都不满足,则直接返回 error 作为错误视图名称

      • 要么从模板引擎规则下搜索到
      • 要么从SpringBoot 默认的静态资源路径下搜索到
      • 要么是 SpringBoot 内置的 error 错误页面
      @Configuration(proxyBeanMethods = false)
      @ConditionalOnProperty(prefix = "server.error.whitelabel", name = "enabled", matchIfMissing = true)
      @Conditional(ErrorTemplateMissingCondition.class)
      protected static class WhitelabelErrorViewConfiguration {
      
          private final StaticView defaultErrorView = new StaticView();
      
          // 标注四
          @Bean(name = "error")
          @ConditionalOnMissingBean(name = "error")
          public View defaultErrorView() {
              return this.defaultErrorView;
          }
      }
      

      标注四的位置,这个就是 SpringBoot 的默认 error 页面,其实本质就是 StaticView 类创建出来的一段代码字符串。

      @Override
      public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response)
          throws Exception {
          if (response.isCommitted()) {
              String message = getMessage(model);
              logger.error(message);
              return;
          }
          response.setContentType(TEXT_HTML_UTF8.toString());
          StringBuilder builder = new StringBuilder();
          Date timestamp = (Date) model.get("timestamp");
          Object message = model.get("message");
          Object trace = model.get("trace");
          if (response.getContentType() == null) {
              response.setContentType(getContentType());
          }
          builder.append("<html><body><h1>Whitelabel Error Page</h1>").append(
              "<p>This application has no explicit mapping for /error, so you are seeing this as a fallback.</p>")
              .append("<div id='created'>").append(timestamp).append("</div>")
              .append("<div>There was an unexpected error (type=").append(htmlEscape(model.get("error")))
              .append(", status=").append(htmlEscape(model.get("status"))).append(").</div>");
          if (message != null) {
              builder.append("<div>").append(htmlEscape(message)).append("</div>");
          }
          if (trace != null) {
              builder.append("<div style='white-space:pre-wrap;'>").append(htmlEscape(trace)).append("</div>");
          }
          builder.append("</body></html>");
          response.getWriter().append(builder.toString());
      }
      

定制错误 JSON 数据

实现返回的错误 JSON 格式的数据有几种方式:

  • 使用环切注解:@ControllerAdvice,同时搭配:@ExceptionHandler
  • 自定义异常处理控制器,可以继承 BasicErrorController
  • 自定义异常属性类,可以继承 DefaultErrorAttributes 类

使用 @ControllerAdvice

创建要给标注为 @ControllerAdvice 注解的类,并且同时在方法上标注 @ExceptionHandler 注解,指定处理哪些异常。

可以搭配 @ModelAttribute 注解,将数据放入请求域中。

@ControllerAdvice
public class UserExceptionHandler {
    
    @ModelAttribute
    public Model test(Model map) {
        map.addAttribute("error", "test");

        return map;
    }

    @ExceptionHandler(RuntimeException.class)
    public String handleException(Exception e) {
        return "forward:/error";
    }
}

缺点:

  • @ModelAttribute 注解也无法重写错误信息里面的格式,也就是无法覆盖。所以,返回的 JSON 格式还是一样 SpringBoot 返回。

继承 BasicErrorController

这个有些复杂,暂时不考虑,可以从将其中的两个方法重写即可:

@RequestMapping(produces = MediaType.TEXT_HTML_VALUE) // text/html
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
   HttpStatus status = getStatus(request);
   Map<String, Object> model = Collections
         .unmodifiableMap(getErrorAttributes(request, isIncludeStackTrace(request, MediaType.TEXT_HTML)));
   response.setStatus(status.value());
   ModelAndView modelAndView = resolveErrorView(request, response, status, model);
   return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
}

@RequestMapping
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
   HttpStatus status = getStatus(request);
   if (status == HttpStatus.NO_CONTENT) {
      return new ResponseEntity<>(status);
   }
   Map<String, Object> body = getErrorAttributes(request, isIncludeStackTrace(request, MediaType.ALL));
   return new ResponseEntity<>(body, status);
}

继承 DefaultErrorAttributes 类

重写 getErrorAttributes 方法,然后调用父类的方法,只做一些略微的数据修改,就很简单啦。

同时,这里可以接受 request 中的数据,也就是在环切中的 @ExceptionHandler 方法中可以使用 request 存储数据,然后在这里取出来使用。

@Component
public class UserErrorAttributes extends DefaultErrorAttributes {


    @Override
    public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
        Map<String, Object> map = super.getErrorAttributes(webRequest, includeStackTrace);

        map.remove("timestamp");
        map.put("code", map.get("status"));
        map.remove("status");
        map.remove("error");

        return map;
    }
}

注意

对于上面的拦截方式,其中 @ControllerAdvice 注解的 @ExceptionHandler 方法是无法拦截请求不存在 url 或者 filter 过滤器等一些特殊情况的异常。

获取不了自定义异常信息,可以在配置文件中添加 server.error.include-exception=true 配置

扩展

# Java   SpringBoot  

评论

公众号:mumuser

企鹅群:932154986

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×