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

默认行为

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

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 formatjava

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

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

你能够在浏览器中依次访问如下地址:github

  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

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

可是若是你使用curl访问上述地址,那么返回的都是以下的json:面试

1
2
3
4
5
6
7
8
9
{
"timestamp" : 1498886969426 ,
"status" : 500 ,
"error" : "Internal Server Error" ,
"exception" : "me.chanjar.exception.SomeException" ,
"message" : "..." ,
"trace" : "..." ,
"path" : "..."
}

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

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

注意:咱们必须在application.properties添加server.error.include-stacktrace=always才可以获得stacktrace。浏览器

为什么curl text/plain资源没法得到error

若是你在logback-spring.xml里同样配置了这么一段:spring-mvc

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

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

1
2
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。

那么为何浏览器访问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里的出了修改过的属性,还有添加的属性:

1
2
3
4
5
6
7
8
9
10
{
"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 http://localhost:8080/return-text-plain得不到error信息,解决这个问题有两个关键点:

  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访问的结果:

url Browser curl
http://localhost:8080/bar/html-a some-ex-error.html some-ex-error.html
http://localhost:8080/bar/html-b No converter found for return value of type: class AnotherExceptionErrorMessageAbstractMessageConverterMethodProcessor#L187 error(json)
http://localhost:8080/bar/json-a some-ex-error.html some-ex-error.html
http://localhost:8080/bar/json-b Could not find acceptable representation 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 Could not find acceptable representation

注意上方表格的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会得到不一样的结果:

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-403和loo/error-406可以返回咱们指望的错误页面,这是为何?先来看看代码。

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

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

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

1
2
@ResponseStatus (NOT_ACCEPTABLE)
public class Exception406 extends RuntimeException

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

1
2
3
4
5
6
@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#L31七、StandardHostValve#L345会将Status Code设置成500,而后跳转到/error,结果就是BasicErrorController处理时就看到Status Code=500,而后按照500去找error page找不到,就只能返回White error page了。

具备1-5工做经验的,面对目前流行的技术不知从何下手,须要突破技术瓶颈的能够加群。在公司待久了,过得很安逸,但跳槽时面试碰壁。须要在短期内进修、跳槽拿高薪的能够加群。若是没有工做经验,但基础很是扎实,对java工做机制,经常使用设计思想,经常使用java开发框架掌握熟练的能够加群。java架构群:591240817 一块儿交流。

实际上,从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,可是这个方法有两个局限:

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
@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#L31七、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页面。

1
2
3
4
5
6
7
8
9
@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的:

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

能够关注个人微信公众号得到更多学习交流的机会

相关文章
相关标签/搜索