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


分类: Java
评论: 0

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



分类: Java
评论: 0

默认错误处理

重要组件

配置类:ErrorMvcAutoConfiguration

重要组件:

DefaultErrorAttributes

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

@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);
}

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

如下图,使用 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 格式的数据。同时,还可以获取在出现这些错误情况的时候,能获取到哪些数据。

定制错误页面

定制错误 JSON 数据

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

使用 @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";
    }
}

缺点:

继承 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 配置

扩展