干货: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

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

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访问上述地址,那么返回的都是如下的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里一样配置了这么一段:

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

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

...

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

curl http://localhost:8080/return-text-plain,会隐含一个请求头Accept: */*,会匹配到FooController.returnTextPlain(produces=text/plain)方法,注意:如果请求头不是Accept: */*或Accept: text/plain,那么是匹配不到FooController.returnTextPlain的。

RequestMappingHandlerMapping根据url匹配到了(见AbstractHandlerMethodMapping.lookupHandlerMethod#L341)FooController.returnTextPlan(produces=text/plain)。

方法抛出了异常,forward到/error。

RequestMappingHandlerMapping根据url匹配到了(见AbstractHandlerMethodMapping.lookupHandlerMethod#L341)BasicErrorController的两个方法errorHtml(produces=text/html)和error(produces=null,相当于produces=*/*)。

因为请求头Accept: */*,所以会匹配error方法上(见AbstractHandlerMethodMapping#L352,RequestMappingInfo.compareTo,ProducesRequestCondition.compareTo)。

error方法返回的是ResponseEntity>,会被HttpEntityMethodProcessor.handleReturnValue处理。

HttpEntityMethodProcessor进入AbstractMessageConverterMethodProcessor.writeWithMessageConverters,发现请求要求*/*(Accept: */*),而能够产生text/plain(FooController.returnTextPlan produces=text/plain),那它会去找能够将Map转换成String的HttpMessageConverter(text/plain代表String),结果是找不到。

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里的出了修改过的属性,还有添加的属性:

{ "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信息,解决这个问题有两个关键点:

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

提供自定义的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访问的结果:

urlBrowsercurl

http://localhost:8080/bar/html-asome-ex-error.htmlsome-ex-error.html

http://localhost:8080/bar/html-bNo converter found for return value of type: class AnotherExceptionErrorMessageAbstractMessageConverterMethodProcessor#L187error(json)

http://localhost:8080/bar/json-asome-ex-error.htmlsome-ex-error.html

http://localhost:8080/bar/json-bCould not find acceptable representationerror(json)

http://localhost:8080/bar/text-plain-asome-ex-error.htmlsome-ex-error.html

http://localhost:8080/bar/text-plain-bCould not find acceptable representationCould 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会获得不同的结果:

urlResult

http://localhost:8080/loo/error-403static resource: public/error/403.html

http://localhost:8080/loo/error-406thymeleaf view: templates/error/406.html

http://localhost:8080/loo/error-600Whitelabel error page

http://localhost:8080/loo/error-601thymeleaf 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,然后跳转到/error,结果就是BasicErrorController处理时就看到Status Code=500,然后按照500去找error page找不到,就只能返回White error page了。

我特意整理了一下,里面的技术不是靠几句话就能讲清楚,所以干脆找朋友录制了一些视频,很多问题其实答案很简单,但是背后的思考和逻辑不简单,要做到知其然还要知其所以然。如果想学习Java工程化、高性能及分布式、深入浅出。性能调优、Spring,MyBatis,Netty源码分析的朋友可以加我新建的Java群:650385180,群里有阿里大牛直播讲解技术,以及Java大型互联网技术的视频免费分享给大家。

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

Attribute nameWhen throw up to TomcatHandled by HandlerExceptionResolver

DefaultErrorAttributes.ERRORHas valueHas Value

DispatcherServlet.EXCEPTIONNo valueHas Value

javax.servlet.error.exceptionHas valueNo 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,对应的代码:@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(newServletRequestAttributes(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 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的使用,这里只是在做一些补充说明:

@ExceptionHandler配合@ControllerAdvice用时,能够应用到所有被@ControllerAdvice切到的Controller

@ExceptionHandler在Controller里的时候,就只会对那个Controller生效

参考文档:

Spring Boot 1.5.4.RELEASE Documentation

Spring framework 4.3.9.RELEASE Documentation

Exception Handling in Spring MVC


注:喜欢的小伙伴可以点赞关注,一起学习进步

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容