• 近期将进行后台系统升级,如有访问不畅,请稍后再试!
  • 极客文库-知识库上线!
  • 极客文库小编@勤劳的小蚂蚁,为您推荐每日资讯,欢迎关注!
  • 每日更新优质编程文章!
  • 更多功能模块开发中。。。

Spring Boot & Spring MVC 异常处理的 N 种方法


默认行为

根据 Spring Boot 官方文档的说法:

For machine clients it will produce a JSON response with details of the error, the HTTP status and the exception message. For browser clients there is a ‘whitelabel’ error view that renders the same data in HTML format

也就是说,当发生异常时:

  • 如果请求是从浏览器发送出来的,那么返回一个 Whitelabel Error Page
  • 如果请求是从 machine 客户端发送出来的,那么会返回相同信息的 json

你可以在浏览器中依次访问以下地址:


会发现 FooController 和 FooRestController 返回的结果都是一个 Whitelabel Error Page 也就是 html。

但是如果你使用 curl 访问上述地址,那么返回的都是如下的 json:

{
  “timestamp”: 1498886969426,
  “status”: 500,
  “error”: “Internal Server Error”,
  “exception”: “me.chanjar.exception.SomeException”,
  “message”: “…”,
  “trace”: “…”,
  “path”: “…”
}

但是有一个 URL 除外:http://localhost:8080/return-text-plain,它不会返回任何结果,原因稍后会有说明。

本章节代码在me.chanjar.boot.def,使用 DefaultExample 运行。


注意:我们必须在 application.properties 添加 server.error.include-stacktrace=always 才能够得到 stacktrace。

为何 curl text/plain 资源无法获得 error

如果你在 logback-spring.xml 里一样配置了这么一段:

<logger name=”org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod” level=”TRACE”/>

那么你就能在日志文件里发现这么一个异常:

org.springframework.web.HttpMediaTypeNotAcceptableException: Could not find acceptable representation

要理解这个异常是怎么来的,那我们来简单分析以下 Spring MVC 的处理过程:

  1. curl http://localhost:8080/return-text-plain,会隐含一个请求头 Accept: */*,会匹配到 FooController.returnTextPlain(produces=text/plain)方法,注意:如果请求头不是 Accept: */*或 Accept: text/plain,那么是匹配不到 FooController.returnTextPlain 的。
  2. RequestMappingHandlerMapping 根据 url 匹配到了(见 AbstractHandlerMethodMapping.lookupHandlerMethod#L341)FooController.returnTextPlan(produces=text/plain)。
  3. 方法抛出了异常,forward 到/error。
  4. RequestMappingHandlerMapping 根据 url 匹配到了(见 AbstractHandlerMethodMapping.lookupHandlerMethod#L341)BasicErrorController 的两个方法 errorHtml(produces=text/html)和 error(produces=null,相当于 produces=*/*)。
  5. 因为请求头 Accept: */*,所以会匹配 error 方法上(见 AbstractHandlerMethodMapping#L352,RequestMappingInfo.compareTo,ProducesRequestCondition.compareTo)。
  6. error 方法返回的是 ResponseEntity<Map<String, Object>>,会被 HttpEntityMethodProcessor.handleReturnValue 处理。
  7. HttpEntityMethodProcessor 进入 AbstractMessageConverterMethodProcessor.writeWithMessageConverters,发现请求要求*/*(Accept: */*),而能够产生 text/plain(FooController.returnTextPlan produces=text/plain),那它会去找能够将 Map 转换成 String 的 HttpMessageConverter(text/plain 代表 String),结果是找不到。
  8. AbstractMessageConverterMethodProcessor 抛出 HttpMediaTypeNotAcceptableException。
  9. 那么为什么浏览器访问 http://localhost:8080/return-text-plain 就可以呢?你只需打开浏览器的开发者模式看看请求头就会发现 Accept:text/html,…,所以在第 4 步会匹配到 BasicErrorController.errorHtml 方法,那结果自然是没有问题了。

那么这个问题怎么解决呢?我会在自定义 ErrorController 里说明。

自定义 Error 页面

前面看到了,Spring Boot 针对浏览器发起的请求的 error 页面是 Whitelabel Error Page,下面讲解如何自定义 error 页面。

注意 2:自定义 Error 页面不会影响 machine 客户端的输出结果

方法 1

根据 Spring Boot 官方文档,如果想要定制这个页面只需要:

to customize it just add a View that resolves to ‘error’

这句话讲的不是很明白,其实只要看 ErrorMvcAutoConfiguration.WhitelabelErrorViewConfiguration 的代码就知道,只需注册一个名字叫做 error 的 View 类型的 Bean 就行了。

本例的 CustomDefaultErrorViewConfiguration 注册将 error 页面改到了 templates/custom-error-page/error.html 上。

本章节代码在me.chanjar.boot.customdefaulterrorview,使用 CustomDefaultErrorViewExample 运行。


方法 2

方法 2 比方法 1 简单很多,在 Spring 官方文档中没有说明。其实只需要提供 error View 所对应的页面文件即可。

比如在本例里,因为使用的是 Thymeleaf 模板引擎,所以在 classpath /templates 放一个自定义的 error.html 就能够自定义 error 页面了。

本章节就不提供代码了,有兴趣的你可以自己尝试。

自定义 Error 属性

前面看到了不论 error 页面还是 error json,能够得到的属性就只有:timestamp、status、error、exception、message、trace、path。

如果你想自定义这些属性,可以如 Spring Boot 官方文档所说的:

simply add a bean of type ErrorAttributes to use the existing mechanism but replace the contents

在 ErrorMvcAutoConfiguration.errorAttributes 提供了 DefaultErrorAttributes,我们也可以参照这个提供一个自己的 CustomErrorAttributes 覆盖掉它。

如果使用 curl 访问相关地址可以看到,返回的 json 里的出了修改过的属性,还有添加的属性:

{
  “exception”: “customized exception”,
  “add-attribute”: “add-attribute”,
  “path”: “customized path”,
  “trace”: “customized trace”,
  “error”: “customized error”,
  “message”: “customized message”,
  “timestamp”: 1498892609326,
  “status”: 100
}

本章节代码在me.chanjar.boot.customerrorattributes,使用 CustomErrorAttributesExample 运行。


自定义 ErrorController


  1. 请求的时候指定 Accept 头,避免匹配到 BasicErrorController.error 方法。比如:curl -H ‘Accept: text/plain’ http://localhost:8080/return-text-plain
  2. 提供自定义的 ErrorController。

下面将如何提供自定义的 ErrorController。按照 Spring Boot 官方文档的说法:

To do that just extend BasicErrorController and add a public method with a @RequestMapping that has a produces attribute, and create a bean of your new type.

所以我们提供了一个 CustomErrorController,并且通过 CustomErrorControllerConfiguration 将其注册为 Bean。

本章节代码在me.chanjar.boot.customerrorcontroller,使用 CustomErrorControllerExample 运行。


ControllerAdvice 定制特定异常返回结果

根据 Spring Boot 官方文档的例子,可以使用@ControllerAdvice 和@ExceptionHandler 对特定异常返回特定的结果。

我们在这里定义了一个新的异常:AnotherException,然后在 BarControllerAdvice 中对 SomeException 和 AnotherException 定义了不同的@ExceptionHandler:

  • SomeException 都返回到 controlleradvice/some-ex-error.html 上
  • AnotherException 统统返回 JSON

在 BarController 中,所有*-a 都抛出 SomeException,所有*-b 都抛出 AnotherException。下面是用浏览器和 curl 访问的结果:


注意上方表格的 Could not find acceptable representation 错误,产生这个的原因和之前为何 curl text/plain 资源无法获得 error 是一样的:无法将@ExceptionHandler 返回的数据转换@RequestMapping.produces 所要求的格式。

所以你会发现如果使用@ExceptionHandler,那就得自己根据请求头 Accept 的不同而输出不同的结果了,办法就是定义一个 void @ExceptionHandler,具体见@ExceptionHandler javadoc。

定制不同 Status Code 的错误页面

Spring Boot 官方文档提供了一种简单的根据不同 Status Code 跳到不同 error 页面的方法,见这里。

我们可以将不同的 Status Code 的页面放在 classpath: public/error 或 classpath: templates/error 目录下,比如 400.html、5xx.html、400.ftl、5xx.ftl。

打开浏览器访问以下 url 会获得不同的结果:


注意/loo/error-600 返回的是 Whitelabel error page,但是/loo/error-403 和 loo/error-406 能够返回我们期望的错误页面,这是为什么?先来看看代码。

在 loo/error-403 中,我们抛出了异常 Exception403:

@ResponseStatus(HttpStatus.FORBIDDEN)
public class Exception403 extends RuntimeException

在 loo/error-406 中,我们抛出了异常 Exception406:

@ResponseStatus(NOT_ACCEPTABLE)
public class Exception406 extends RuntimeException

注意到这两个异常都有@ResponseStatus 注解,这个是注解标明了这个异常所对应的 Status Code。 但是在 loo/error-600 中抛出的 SomeException 没有这个注解,而是尝试在 Response.setStatus(600)来达到目的,但结果是失败的,这是为什么呢?:

@RequestMapping(“/error-600”)
public String error600(HttpServletRequest request, HttpServletResponse response) throws SomeException {
  request.setAttribute(WebUtils.ERROR_STATUS_CODE_ATTRIBUTE, 600);
  response.setStatus(600);
  throw new SomeException();
}

要了解为什么就需要知道 Spring MVC 对于异常的处理机制,下面简单讲解一下:

Spring MVC 处理异常的地方在 DispatcherServlet.processHandlerException,这个方法会利用 HandlerExceptionResolver 来看异常应该返回什么 ModelAndView。

目前已知的 HandlerExceptionResolver 有这么几个:

  1. DefaultErrorAttributes,只负责把异常记录在 Request attributes 中,name 是 org.springframework.boot.autoconfigure.web.DefaultErrorAttributes.ERROR
  2. ExceptionHandlerExceptionResolver,根据@ExceptionHandler resolve
  3. ResponseStatusExceptionResolver,根据@ResponseStatus resolve
  4. DefaultHandlerExceptionResolver,负责处理 Spring MVC 标准异常

Exception403 和 Exception406 都有被 ResponseStatusExceptionResolver 处理了,而 SomeException 没有任何 Handler 处理,这样 DispatcherServlet 就会将这个异常往上抛至到容器处理(见 DispatcherServlet#L1243),以 Tomcat 为例,它在 StandardHostValve#L317、StandardHostValve#L345 会将 Status Code 设置成 500,然后跳转到/error,结果就是 BasicErrorController 处理时就看到 Status Code=500,然后按照 500 去找 error page 找不到,就只能返回 White error page 了。

实际上,从 Request 的 attributes 角度来看,交给 BasicErrorController 处理时,和容器自己处理时,有几个相关属性的内部情况时这样的:


PS. DefaultErrorAttributes.ERROR = org.springframework.boot.autoconfigure.web.DefaultErrorAttributes.ERROR

PS. DispatcherServlet.EXCEPTION = org.springframework.web.servlet.DispatcherServlet.EXCEPTION

解决办法有两个:

1.给 SomeException 添加@ResponseStatus,但是这个方法有两个局限:

  • 如果这个异常不是你能修改的,比如在第三方的 Jar 包里
  • 如果@ResponseStatus 使用 HttpStatus 作为参数,但是这个枚举定义的 Status Code 数量有限

2. 使用@ExceptionHandler,不过得注意自己决定 view 以及 status code

第二种解决办法的例子 loo/error-601,对应的代码:

@RequestMapping(“/error-601”)
public String error601(HttpServletRequest request, HttpServletResponse response) throws AnotherException {
  throw new AnotherException();
}
 
@ExceptionHandler(AnotherException.class)
String handleAnotherException(HttpServletRequest request, HttpServletResponse response, Model model)
    throws IOException {
  // 需要设置 Status Code,否则响应结果会是 200
  response.setStatus(601);
  model.addAllAttributes(errorAttributes.getErrorAttributes(new ServletRequestAttributes(request), true));
  return “error/6xx”;
}

总结:

1. 没有被 HandlerExceptionResolverresolve 到的异常会交给容器处理。已知的实现有(按照顺序):

  • DefaultErrorAttributes,只负责把异常记录在 Request attributes 中,name 是 org.springframework.boot.autoconfigure.web.DefaultErrorAttributes.ERROR
  • ExceptionHandlerExceptionResolver,根据@ExceptionHandler resolve
  • ResponseStatusExceptionResolver,根据@ResponseStatus resolve
  • DefaultHandlerExceptionResolver,负责处理 Spring MVC 标准异常

2. @ResponseStatus 用来规定异常对应的 Status Code,其他异常的 Status Code 由容器决定,在 Tomcat 里都认定为 500(StandardHostValve#L317、StandardHostValve#L345)
3. @ExceptionHandler 处理的异常不会经过 BasicErrorController,需要自己决定如何返回页面,并且设置 Status Code(如果不设置就是 200)
4. BasicErrorController 会尝试根据 Status Code 找 error page,找不到的话就用 Whitelabel error page

本章节代码在me.chanjar.boot.customstatuserrorpage,使用 CustomStatusErrorPageExample 运行。


利用 ErrorViewResolver 来定制错误页面

前面讲到 BasicErrorController 会根据 Status Code 来跳转对应的 error 页面,其实这个工作是由 DefaultErrorViewResolver 完成的。

实际上我们也可以提供自己的 ErrorViewResolver 来定制特定异常的 error 页面。

@Component
public class SomeExceptionErrorViewResolver implements ErrorViewResolver {
 
  @Override
  public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) {
    return new ModelAndView(“custom-error-view-resolver/some-ex-error”, model);
  }
 
}

不过需要注意的是,无法通过 ErrorViewResolver 设定 Status Code,Status Code 由@ResponseStatus 或者容器决定(Tomcat 里一律是 500)。

本章节代码在me.chanjar.boot.customerrorviewresolver,使用 CustomErrorViewResolverExample 运行。


@ExceptionHandler 和 @ControllerAdvice

前面的例子中已经有了对@ControllerAdvice 和@ExceptionHandler 的使用,这里只是在做一些补充说明:

  1. @ExceptionHandler 配合@ControllerAdvice 用时,能够应用到所有被@ControllerAdvice 切到的 Controller
  2. @ExceptionHandler 在 Controller 里的时候,就只会对那个 Controller 生效

参考文档:


附录 I

下表列出哪些特性是 Spring Boot 的,哪些是 Spring MVC 的:



丨极客文库, 版权所有丨如未注明 , 均为原创丨
本网站采用知识共享署名-非商业性使用-相同方式共享 3.0 中国大陆许可协议进行授权
转载请注明原文链接:Spring Boot & Spring MVC 异常处理的 N 种方法
喜欢 (0)
[247507792@qq.com]
分享 (0)
勤劳的小蚂蚁
关于作者:
温馨提示:本文来源于网络,转载文章皆标明了出处,如果您发现侵权文章,请及时向站长反馈删除。

欢迎 注册账号 登录 发表评论!

  • 精品技术教程
  • 编程资源分享
  • 问答交流社区
  • 极客文库知识库

客服QQ


QQ:2248886839


工作时间:09:00-23:00