Spring MVC error handling example
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
doc
src/main
.gitignore
LICENSE
README.md
pom.xml

README.md

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

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

  1. http://localhost:8080/return-model-and-view
  2. http://localhost:8080/return-view-name
  3. http://localhost:8080/return-view
  4. http://localhost:8080/return-text-plain
  5. http://localhost:8080/return-json-1
  6. http://localhost:8080/return-json-2

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

但是如果你使用curl -i -s -X GET访问上述地址,那么返回的都是如下的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。

Spring MVC处理请求的总体流程

总体流程

分析为何浏览器访问都Whitelabel Error Page

浏览器访问

分析为何curl text/plain资源却没有返回结果

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

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

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

... TRACE 13387 --- [nio-8080-exec-2] .w.s.m.m.a.ServletInvocableHandlerMethod : Invoking 'org.springframework.boot.autoconfigure.web.BasicErrorController.error' with arguments [org.apache.catalina.core.ApplicationHttpRequest@1408b81]
... TRACE 13387 --- [nio-8080-exec-2] .w.s.m.m.a.ServletInvocableHandlerMethod : Method [org.springframework.boot.autoconfigure.web.BasicErrorController.error] returned [<500 Internal Server Error,{timestamp=Thu Nov 09 13:20:15 CST 2017, status=500, error=Internal Server Error, exception=me.chanjar.exception.SomeException, message=No message available, trace=..., path=/return-text-plain, {}>]
... TRACE 13387 --- [nio-8080-exec-2] .w.s.m.m.a.ServletInvocableHandlerMethod : Error handling return value [type=org.springframework.http.ResponseEntity] [value=<500 Internal Server Error,{timestamp=Thu Nov 09 13:20:15 CST 2017, status=500, error=Internal Server Error, exception=me.chanjar.exception.SomeException, message=No message available, trace=..., path=/return-text-plain, {}>]
HandlerMethod details: 
Controller [org.springframework.boot.autoconfigure.web.BasicErrorController]
Method [public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> org.springframework.boot.autoconfigure.web.BasicErrorController.error(javax.servlet.http.HttpServletRequest)]
org.springframework.web.HttpMediaTypeNotAcceptableException: Could not find acceptable representation
...

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

curl访问

那么这个问题怎么解决呢?我会在自定义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的代码就知道,只需注册一个名字叫做errorView类型的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 -i -s -X GET访问相关地址可以看到,返回的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

在前面提到了curl -i -s -X GET http://localhost:8080/return-text-plain得不到error信息,解决这个问题有两个关键点:

  1. 请求的时候指定Accept头,避免匹配到BasicErrorController.error方法。比如:curl -i -s -X GET -H 'Accept: text/plain' http://localhost:8080/return-text-plain
  2. 提供自定义的ErrorController提供一个path=/error procudes=text/plain的方法。

其实还有另一种方式:提供一个Object->String转换的HttpMessageConverter,这个方法本文不展开。

下面将如何提供自定义的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统统返回ResponseEntity

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

url Browser curl -i -s -X GET
http://localhost:8080/bar/html-a some-ex-error.html some-ex-error.html
http://localhost:8080/bar/html-b error(json) error(json)
http://localhost:8080/bar/json-a some-ex-error.html some-ex-error.html
http://localhost:8080/bar/json-b error(json) error(json)
http://localhost:8080/bar/text-plain-a some-ex-error.html some-ex-error.html
http://localhost:8080/bar/text-plain-b Could not find acceptable representation(White Error Page) Could not find acceptable representation(无输出)

注意上方表格的Could not find acceptable representation错误,产生这个的原因前面已经讲过。

不过需要注意的是流程稍微有点不同,在前面的例子里的流程是这样的:

  1. 访问url
  2. 抛出异常
  3. forward到 /error
  4. BasicErrorController.error方法返回的ResponseEntity没有办法转换成String

本章节例子的异常是这样的:

  1. 访问url
  2. 抛出异常
  3. @ExceptionHandler处理
  4. AnotherException的@ExceptionHander返回的ResponseEntity没有办法转换成String,被算作没有被处理成功
  5. forward到 /error
  6. BasicErrorController.error方法返回的ResponseEntity没有办法转换成String

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

定制不同Status Code的错误页面

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

我们可以将不同的Status Code的页面放在classpath: public/errorclasspath: templates/error目录下,比如400.html5xx.html400.ftl5xx.ftl

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

url Result
http://localhost:8080/loo/error-403 static resource: public/error/403.html
http://localhost:8080/loo/error-406 thymeleaf view: templates/error/406.html
http://localhost:8080/loo/error-600 Whitelabel error page
http://localhost:8080/loo/error-601 thymeleaf view: templates/error/6xx.html

注意/loo/error-600返回的是Whitelabel error page,但是/loo/error-403loo/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标准异常

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

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

Attribute name When throw up to Tomcat Handled by HandlerExceptionResolver
DefaultErrorAttributes.ERROR Has value Has Value
DispatcherServlet.EXCEPTION No value Has Value
javax.servlet.error.exception Has value No Value

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

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

解决办法有两个:

  1. SomeException添加@ResponseStatus,但是这个方法有两个局限:
    1. 如果这个异常不是你能修改的,比如在第三方的Jar包里
    2. 如果@ResponseStatus使用HttpStatus作为参数,但是这个枚举定义的Status Code数量有限
  2. 使用@ExceptionHandler,不过得注意自己决定view以及status code。 这个办法很麻烦,因为你得小心的处理请求头里的Accept,以此返回相应的结果,并且因为它的处理方式脱离了框架,容易造成返回信息不一致。

第二种解决办法的例子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";
}

补充:从Spring Framework 5.0开始,提供了ResponseStatusException,你可以直接在抛出异常处定义Status Code、Reason,能够很好的解决第三方Jar包的问题。用法类似于这样:

@PutMapping("/actor/{id}/{name}")
public String updateActorName(
  @PathVariable("id") int id, 
  @PathVariable("name") String name) {
  
    try {
        return actorService.updateActor(id, name);
    } catch (ActorNotFoundException ex) {
        throw new ResponseStatusException(
          HttpStatus.BAD_REQUEST, "Provide correct Actor Id", ex);
    }
}

总结:

  1. 没有被HandlerExceptionResolverresolve到的异常会交给容器处理。已知的实现有(按照顺序):
    1. DefaultErrorAttributes,只负责把异常记录在Request attributes中,name是org.springframework.boot.autoconfigure.web.DefaultErrorAttributes.ERROR
    2. ExceptionHandlerExceptionResolver,根据@ExceptionHandler resolve
    3. ResponseStatusExceptionResolver,根据@ResponseStatus resolve
    4. DefaultHandlerExceptionResolver,负责处理Spring MVC标准异常
  2. @ResponseStatus用来规定异常对应的Status Code,其他异常的Status Code由容器决定,在Tomcat里都认定为500(StandardHostValve#L317StandardHostValve#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生效

最佳实践

前面讲了那么多种方式,那么在Spring MVC中处理异常的最佳实践是什么?在回答这个问题前我先给出一个好的异常处理应该是什么样子的:

  1. 返回的异常信息能够适配各种Accept,比如Accept:text/html返回html页面,Accept:application/json返回json。
  2. 统一的异常信息schema,且可自定义,比如只包含timestamperrormessage等信息。
  3. 能够自定义部分信息,比如可以自定义errormessage的内容。

要达成以上目标我们可以采取的方法:

  1. 达成第1条:自定义ErrorController,扩展BasicErrorController,支持更多的Accept类型。
  2. 达成第2条:自定义ErrorAttributes
  3. 达成第3条:
    1. 使用@ResponseStatusResponseStatusException(since 5.0)
    2. 前一种方式不适用时,自定义ErrorAttributes,在里面写代码,针对特定异常返回特定信息。推荐使用配置的方式来做,比如配置文件里写XXXException的message是YYYY。

Spring MVC对于从Controller抛出的异常是不打印到console的,解决办法是提供一个HandlerExceptionResolver,比如这样:

@Order(Ordered.HIGHEST_PRECEDENCE)
public class ErrorLogger implements HandlerExceptionResolver {

  private static final Logger LOGGER = LoggerFactory.getLogger(ErrorLogger.class);

  @Override
  public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler,
      Exception ex) {
    LOGGER.error("Exception happened at [{}]: {}", request.getRequestURI(), ExceptionUtils.getStackTrace(ex));
    return null;
  }

}

附录I

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

Feature Spring Boot Spring MVC
BasicErrorController Yes
ErrorAttributes Yes
ErrorViewResolver Yes
@ControllerAdvice Yes
@ExceptionHandler Yes
@ResponseStatus Yes
HandlerExceptionResolver Yes