参考文档:
- Spring Boot 1.5.4.RELEASE Documentation
- Spring framework 4.3.9.RELEASE Documentation
- Exception Handling in Spring MVC
根据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
你可以在浏览器中依次访问以下地址:
- http://localhost:8080/return-model-and-view
- http://localhost:8080/return-view-name
- http://localhost:8080/return-view
- http://localhost:8080/return-text-plain
- http://localhost:8080/return-json-1
- http://localhost:8080/return-json-2
会发现FooController和FooRestController返回的结果都是一个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。
如果你在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的处理过程:
那么这个问题怎么解决呢?我会在自定义ErrorController里说明。
前面看到了,Spring Boot针对浏览器发起的请求的error页面是Whitelabel Error Page
,下面讲解如何自定义error页面。
注意2:自定义Error页面不会影响machine客户端的输出结果
根据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比方法1简单很多,在Spring官方文档中没有说明。其实只需要提供error
View
所对应的页面文件即可。
比如在本例里,因为使用的是Thymeleaf模板引擎,所以在classpath /templates
放一个自定义的error.html
就能够自定义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运行。
在前面提到了curl -i -s -X GET http://localhost:8080/return-text-plain
得不到error信息,解决这个问题有两个关键点:
- 请求的时候指定
Accept
头,避免匹配到BasicErrorController.error方法。比如:curl -i -s -X GET -H 'Accept: text/plain' http://localhost:8080/return-text-plain
- 提供自定义的
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 aproduces
attribute, and create a bean of your new type.
所以我们提供了一个CustomErrorController,并且通过CustomErrorControllerConfiguration将其注册为Bean。
本章节代码在me.chanjar.boot.customerrorcontroller,使用CustomErrorControllerExample运行。
根据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
错误,产生这个的原因前面已经讲过。
不过需要注意的是流程稍微有点不同,在前面的例子里的流程是这样的:
- 访问url
- 抛出异常
- forward到 /error
- BasicErrorController.error方法返回的ResponseEntity没有办法转换成String
本章节例子的异常是这样的:
- 访问url
- 抛出异常
- 被
@ExceptionHandler
处理 - AnotherException的
@ExceptionHander
返回的ResponseEntity没有办法转换成String,被算作没有被处理成功 - forward到 /error
- BasicErrorController.error方法返回的ResponseEntity没有办法转换成String
所以你会发现如果使用@ExceptionHandler,那就得自己根据请求头Accept
的不同而输出不同的结果了,办法就是定义一个void @ExceptionHandler
,具体见@ExceptionHandler javadoc。
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
:
@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有这么几个:
- DefaultErrorAttributes,只负责把异常记录在Request attributes中,name是
org.springframework.boot.autoconfigure.web.DefaultErrorAttributes.ERROR
- ExceptionHandlerExceptionResolver,根据@ExceptionHandler resolve
- ResponseStatusExceptionResolver,根据@ResponseStatus resolve
- DefaultHandlerExceptionResolver,负责处理Spring MVC标准异常
Exception403
和Exception406
都有被ResponseStatusExceptionResolver处理了,而SomeException
没有任何Handler处理,这样DispatcherServlet
就会将这个异常往上抛至到容器处理(见DispatcherServlet#L1243),以Tomcat为例,它在StandardHostValve#L317、StandardHostValve#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
解决办法有两个:
- 给
SomeException
添加@ResponseStatus
,但是这个方法有两个局限:- 如果这个异常不是你能修改的,比如在第三方的Jar包里
- 如果
@ResponseStatus
使用HttpStatus作为参数,但是这个枚举定义的Status Code数量有限
- 使用@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);
}
}
总结:
- 没有被HandlerExceptionResolverresolve到的异常会交给容器处理。已知的实现有(按照顺序):
- DefaultErrorAttributes,只负责把异常记录在Request attributes中,name是
org.springframework.boot.autoconfigure.web.DefaultErrorAttributes.ERROR
- ExceptionHandlerExceptionResolver,根据@ExceptionHandler resolve
- ResponseStatusExceptionResolver,根据@ResponseStatus resolve
- DefaultHandlerExceptionResolver,负责处理Spring MVC标准异常
- DefaultErrorAttributes,只负责把异常记录在Request attributes中,name是
- @ResponseStatus用来规定异常对应的Status Code,其他异常的Status Code由容器决定,在Tomcat里都认定为500(StandardHostValve#L317、StandardHostValve#L345)
- @ExceptionHandler处理的异常不会经过BasicErrorController,需要自己决定如何返回页面,并且设置Status Code(如果不设置就是200)
- BasicErrorController会尝试根据Status Code找error page,找不到的话就用Whitelabel error page
本章节代码在me.chanjar.boot.customstatuserrorpage,使用CustomStatusErrorPageExample运行。
前面讲到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运行。
前面的例子中已经有了对@ControllerAdvice和@ExceptionHandler的使用,这里只是在做一些补充说明:
@ExceptionHandler
配合@ControllerAdvice
用时,能够应用到所有被@ControllerAdvice
切到的Controller@ExceptionHandler
在Controller里的时候,就只会对那个Controller生效
前面讲了那么多种方式,那么在Spring MVC中处理异常的最佳实践是什么?在回答这个问题前我先给出一个好的异常处理应该是什么样子的:
- 返回的异常信息能够适配各种
Accept
,比如Accept:text/html
返回html页面,Accept:application/json
返回json。 - 统一的异常信息schema,且可自定义,比如只包含
timestamp
、error
、message
等信息。 - 能够自定义部分信息,比如可以自定义
error
、message
的内容。
要达成以上目标我们可以采取的方法:
- 达成第1条:自定义
ErrorController
,扩展BasicErrorController
,支持更多的Accept
类型。 - 达成第2条:自定义
ErrorAttributes
- 达成第3条:
- 使用
@ResponseStatus
或ResponseStatusException
(since 5.0) - 前一种方式不适用时,自定义
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;
}
}
下表列出哪些特性是Spring Boot的,哪些是Spring MVC的:
Feature | Spring Boot | Spring MVC |
---|---|---|
BasicErrorController | Yes | |
ErrorAttributes | Yes | |
ErrorViewResolver | Yes | |
@ControllerAdvice | Yes | |
@ExceptionHandler | Yes | |
@ResponseStatus | Yes | |
HandlerExceptionResolver | Yes |