Spring Boot & Spring MVC 異常處理的N種方法

姓名:祝雙 ? ? 學(xué)號(hào):16040520067?

文章轉(zhuǎn)載自http://www.importnew.com/27186.html

原文出處:chanjarster

【嵌牛導(dǎo)讀】:可以讓你學(xué)會(huì)如何處理這種情況

【嵌牛鼻子】:java

【嵌牛提問(wèn)】:這些處理方法哪種效率最高

【嵌牛正文】:

默認(rèn)行為

根據(jù)Spring Boot官方文檔的說(shuō)法:

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

也就是說(shuō),當(dāng)發(fā)生異常時(shí):

如果請(qǐng)求是從瀏覽器發(fā)送出來(lái)的,那么返回一個(gè)Whitelabel Error Page

如果請(qǐng)求是從machine客戶端發(fā)送出來(lái)的,那么會(huì)返回相同信息的json

你可以在瀏覽器中依次訪問(wèn)以下地址:

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

會(huì)發(fā)現(xiàn)FooController和FooRestController返回的結(jié)果都是一個(gè)Whitelabel Error Page也就是html。

但是如果你使用curl訪問(wèn)上述地址,那么返回的都是如下的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":"..."

}

但是有一個(gè)URL除外:http://localhost:8080/return-text-plain,它不會(huì)返回任何結(jié)果,原因稍后會(huì)有說(shuō)明。

本章節(jié)代碼在me.chanjar.boot.def,使用DefaultExample運(yùn)行。

注意:我們必須在application.properties添加server.error.include-stacktrace=always才能夠得到stacktrace。

為何curl text/plain資源無(wú)法獲得error

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

1

那么你就能在日志文件里發(fā)現(xiàn)這么一個(gè)異常:

1

2

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

...

要理解這個(gè)異常是怎么來(lái)的,那我們來(lái)簡(jiǎn)單分析以下Spring MVC的處理過(guò)程:

curl http://localhost:8080/return-text-plain,會(huì)隱含一個(gè)請(qǐng)求頭Accept: */*,會(huì)匹配到FooController.returnTextPlain(produces=text/plain)方法,注意:如果請(qǐng)求頭不是Accept: */*或Accept: text/plain,那么是匹配不到FooController.returnTextPlain的。

RequestMappingHandlerMapping根據(jù)url匹配到了(見(jiàn)AbstractHandlerMethodMapping.lookupHandlerMethod#L341)FooController.returnTextPlan(produces=text/plain)。

方法拋出了異常,forward到/error。

RequestMappingHandlerMapping根據(jù)url匹配到了(見(jiàn)AbstractHandlerMethodMapping.lookupHandlerMethod#L341)BasicErrorController的兩個(gè)方法errorHtml(produces=text/html)和error(produces=null,相當(dāng)于produces=*/*)。

因?yàn)檎?qǐng)求頭Accept: */*,所以會(huì)匹配error方法上(見(jiàn)AbstractHandlerMethodMapping#L352,RequestMappingInfo.compareTo,ProducesRequestCondition.compareTo)。

error方法返回的是ResponseEntity>,會(huì)被HttpEntityMethodProcessor.handleReturnValue處理。

HttpEntityMethodProcessor進(jìn)入AbstractMessageConverterMethodProcessor.writeWithMessageConverters,發(fā)現(xiàn)請(qǐng)求要求*/*(Accept: */*),而能夠產(chǎn)生text/plain(FooController.returnTextPlan produces=text/plain),那它會(huì)去找能夠?qū)ap轉(zhuǎn)換成String的HttpMessageConverter(text/plain代表String),結(jié)果是找不到。

AbstractMessageConverterMethodProcessor拋出HttpMediaTypeNotAcceptableException。

那么為什么瀏覽器訪問(wèn)http://localhost:8080/return-text-plain就可以呢?你只需打開(kāi)瀏覽器的開(kāi)發(fā)者模式看看請(qǐng)求頭就會(huì)發(fā)現(xiàn)Accept:text/html,…,所以在第4步會(huì)匹配到BasicErrorController.errorHtml方法,那結(jié)果自然是沒(méi)有問(wèn)題了。

那么這個(gè)問(wèn)題怎么解決呢?我會(huì)在自定義ErrorController里說(shuō)明。

自定義Error頁(yè)面

前面看到了,Spring Boot針對(duì)瀏覽器發(fā)起的請(qǐng)求的error頁(yè)面是Whitelabel Error Page,下面講解如何自定義error頁(yè)面。

注意2:自定義Error頁(yè)面不會(huì)影響machine客戶端的輸出結(jié)果

方法1

根據(jù)Spring Boot官方文檔,如果想要定制這個(gè)頁(yè)面只需要:

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

這句話講的不是很明白,其實(shí)只要看ErrorMvcAutoConfiguration.WhitelabelErrorViewConfiguration的代碼就知道,只需注冊(cè)一個(gè)名字叫做error的View類型的Bean就行了。

本例的CustomDefaultErrorViewConfiguration注冊(cè)將error頁(yè)面改到了templates/custom-error-page/error.html上。

本章節(jié)代碼在me.chanjar.boot.customdefaulterrorview,使用CustomDefaultErrorViewExample運(yùn)行。

方法2

方法2比方法1簡(jiǎn)單很多,在Spring官方文檔中沒(méi)有說(shuō)明。其實(shí)只需要提供error View所對(duì)應(yīng)的頁(yè)面文件即可。

比如在本例里,因?yàn)槭褂玫氖荰hymeleaf模板引擎,所以在classpath /templates放一個(gè)自定義的error.html就能夠自定義error頁(yè)面了。

本章節(jié)就不提供代碼了,有興趣的你可以自己嘗試。

自定義Error屬性

前面看到了不論error頁(yè)面還是error json,能夠得到的屬性就只有:timestamp、status、error、exception、message、trace、path。

如果你想自定義這些屬性,可以如Spring Boot官方文檔所說(shuō)的:

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

在ErrorMvcAutoConfiguration.errorAttributes提供了DefaultErrorAttributes,我們也可以參照這個(gè)提供一個(gè)自己的CustomErrorAttributes覆蓋掉它。

如果使用curl訪問(wèn)相關(guān)地址可以看到,返回的json里的出了修改過(guò)的屬性,還有添加的屬性:

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

}

本章節(jié)代碼在me.chanjar.boot.customerrorattributes,使用CustomErrorAttributesExample運(yùn)行。

自定義ErrorController

在前面提到了curl http://localhost:8080/return-text-plain得不到error信息,解決這個(gè)問(wèn)題有兩個(gè)關(guān)鍵點(diǎn):

請(qǐng)求的時(shí)候指定Accept頭,避免匹配到BasicErrorController.error方法。比如:curl -H ‘Accept: text/plain’ http://localhost:8080/return-text-plain

提供自定義的ErrorController。

下面將如何提供自定義的ErrorController。按照Spring Boot官方文檔的說(shuō)法:

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.

所以我們提供了一個(gè)CustomErrorController,并且通過(guò)CustomErrorControllerConfiguration將其注冊(cè)為Bean。

本章節(jié)代碼在me.chanjar.boot.customerrorcontroller,使用CustomErrorControllerExample運(yùn)行。

ControllerAdvice定制特定異常返回結(jié)果

根據(jù)Spring Boot官方文檔的例子,可以使用@ControllerAdvice和@ExceptionHandler對(duì)特定異常返回特定的結(jié)果。

我們?cè)谶@里定義了一個(gè)新的異常:AnotherException,然后在BarControllerAdvice中對(duì)SomeException和AnotherException定義了不同的@ExceptionHandler:

SomeException都返回到controlleradvice/some-ex-error.html上

AnotherException統(tǒng)統(tǒng)返回JSON

在BarController中,所有*-a都拋出SomeException,所有*-b都拋出AnotherException。下面是用瀏覽器和curl訪問(wèn)的結(jié)果:

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錯(cuò)誤,產(chǎn)生這個(gè)的原因和之前為何curl text/plain資源無(wú)法獲得error是一樣的:無(wú)法將@ExceptionHandler返回的數(shù)據(jù)轉(zhuǎn)換@RequestMapping.produces所要求的格式。

所以你會(huì)發(fā)現(xiàn)如果使用@ExceptionHandler,那就得自己根據(jù)請(qǐng)求頭Accept的不同而輸出不同的結(jié)果了,辦法就是定義一個(gè)void @ExceptionHandler,具體見(jiàn)@ExceptionHandler javadoc。

定制不同Status Code的錯(cuò)誤頁(yè)面

Spring Boot 官方文檔提供了一種簡(jiǎn)單的根據(jù)不同Status Code跳到不同error頁(yè)面的方法,見(jiàn)這里

我們可以將不同的Status Code的頁(yè)面放在classpath: public/error或classpath: templates/error目錄下,比如400.html、5xx.html、400.ftl、5xx.ftl。

打開(kāi)瀏覽器訪問(wèn)以下url會(huì)獲得不同的結(jié)果:

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能夠返回我們期望的錯(cuò)誤頁(yè)面,這是為什么?先來(lái)看看代碼。

在loo/error-403中,我們拋出了異常Exception403:

1

2

@ResponseStatus(HttpStatus.FORBIDDEN)

publicclassException403extendsRuntimeException

在loo/error-406中,我們拋出了異常Exception406:

1

2

@ResponseStatus(NOT_ACCEPTABLE)

publicclassException406extendsRuntimeException

注意到這兩個(gè)異常都有@ResponseStatus注解,這個(gè)是注解標(biāo)明了這個(gè)異常所對(duì)應(yīng)的Status Code。 但是在loo/error-600中拋出的SomeException沒(méi)有這個(gè)注解,而是嘗試在Response.setStatus(600)來(lái)達(dá)到目的,但結(jié)果是失敗的,這是為什么呢?:

1

2

3

4

5

6

@RequestMapping("/error-600")

publicString error600(HttpServletRequest request, HttpServletResponse response)throwsSomeException {

request.setAttribute(WebUtils.ERROR_STATUS_CODE_ATTRIBUTE,600);

response.setStatus(600);

thrownewSomeException();

}

要了解為什么就需要知道Spring MVC對(duì)于異常的處理機(jī)制,下面簡(jiǎn)單講解一下:

Spring MVC處理異常的地方在DispatcherServlet.processHandlerException,這個(gè)方法會(huì)利用HandlerExceptionResolver來(lái)看異常應(yīng)該返回什么ModelAndView。

目前已知的HandlerExceptionResolver有這么幾個(gè):

DefaultErrorAttributes,只負(fù)責(zé)把異常記錄在Request attributes中,name是org.springframework.boot.autoconfigure.web.DefaultErrorAttributes.ERROR

ExceptionHandlerExceptionResolver,根據(jù)@ExceptionHandler resolve

ResponseStatusExceptionResolver,根據(jù)@ResponseStatus resolve

DefaultHandlerExceptionResolver,負(fù)責(zé)處理Spring MVC標(biāo)準(zhǔn)異常

Exception403和Exception406都有被ResponseStatusExceptionResolver處理了,而SomeException沒(méi)有任何Handler處理,這樣DispatcherServlet就會(huì)將這個(gè)異常往上拋至到容器處理(見(jiàn)DispatcherServlet#L1243),以Tomcat為例,它在StandardHostValve#L317、StandardHostValve#L345會(huì)將Status Code設(shè)置成500,然后跳轉(zhuǎn)到/error,結(jié)果就是BasicErrorController處理時(shí)就看到Status Code=500,然后按照500去找error page找不到,就只能返回White error page了。

實(shí)際上,從Request的attributes角度來(lái)看,交給BasicErrorController處理時(shí),和容器自己處理時(shí),有幾個(gè)相關(guān)屬性的內(nèi)部情況時(shí)這樣的:

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

解決辦法有兩個(gè):

1.給SomeException添加@ResponseStatus,但是這個(gè)方法有兩個(gè)局限:

如果這個(gè)異常不是你能修改的,比如在第三方的Jar包里

如果@ResponseStatus使用HttpStatus作為參數(shù),但是這個(gè)枚舉定義的Status Code數(shù)量有限

2. 使用@ExceptionHandler,不過(guò)得注意自己決定view以及status code

第二種解決辦法的例子loo/error-601,對(duì)應(yīng)的代碼:

1

2

3

4

5

6

7

8

9

10

11

12

13

@RequestMapping("/error-601")

publicString error601(HttpServletRequest request, HttpServletResponse response)throwsAnotherException {

thrownewAnotherException();

}

@ExceptionHandler(AnotherException.class)

String handleAnotherException(HttpServletRequest request, HttpServletResponse response, Model model)

throwsIOException {

// 需要設(shè)置Status Code,否則響應(yīng)結(jié)果會(huì)是200

response.setStatus(601);

model.addAllAttributes(errorAttributes.getErrorAttributes(newServletRequestAttributes(request),true));

return"error/6xx";

}

總結(jié):

1. 沒(méi)有被HandlerExceptionResolverresolve到的異常會(huì)交給容器處理。已知的實(shí)現(xiàn)有(按照順序):

DefaultErrorAttributes,只負(fù)責(zé)把異常記錄在Request attributes中,name是org.springframework.boot.autoconfigure.web.DefaultErrorAttributes.ERROR

ExceptionHandlerExceptionResolver,根據(jù)@ExceptionHandler resolve

ResponseStatusExceptionResolver,根據(jù)@ResponseStatus resolve

DefaultHandlerExceptionResolver,負(fù)責(zé)處理Spring MVC標(biāo)準(zhǔn)異常

2. @ResponseStatus用來(lái)規(guī)定異常對(duì)應(yīng)的Status Code,其他異常的Status Code由容器決定,在Tomcat里都認(rèn)定為500(StandardHostValve#L317、StandardHostValve#L345)

3. @ExceptionHandler處理的異常不會(huì)經(jīng)過(guò)BasicErrorController,需要自己決定如何返回頁(yè)面,并且設(shè)置Status Code(如果不設(shè)置就是200)

4. BasicErrorController會(huì)嘗試根據(jù)Status Code找error page,找不到的話就用Whitelabel error page

本章節(jié)代碼在me.chanjar.boot.customstatuserrorpage,使用CustomStatusErrorPageExample運(yùn)行。

利用ErrorViewResolver來(lái)定制錯(cuò)誤頁(yè)面

前面講到BasicErrorController會(huì)根據(jù)Status Code來(lái)跳轉(zhuǎn)對(duì)應(yīng)的error頁(yè)面,其實(shí)這個(gè)工作是由DefaultErrorViewResolver完成的。

實(shí)際上我們也可以提供自己的ErrorViewResolver來(lái)定制特定異常的error頁(yè)面。

1

2

3

4

5

6

7

8

9

@Component

publicclassSomeExceptionErrorViewResolverimplementsErrorViewResolver {

@Override

publicModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map model) {

returnnewModelAndView("custom-error-view-resolver/some-ex-error", model);

}

}

不過(guò)需要注意的是,無(wú)法通過(guò)ErrorViewResolver設(shè)定Status Code,Status Code由@ResponseStatus或者容器決定(Tomcat里一律是500)。

本章節(jié)代碼在me.chanjar.boot.customerrorviewresolver,使用CustomErrorViewResolverExample運(yùn)行。

@ExceptionHandler 和 @ControllerAdvice

前面的例子中已經(jīng)有了對(duì)@ControllerAdvice和@ExceptionHandler的使用,這里只是在做一些補(bǔ)充說(shuō)明:

@ExceptionHandler配合@ControllerAdvice用時(shí),能夠應(yīng)用到所有被@ControllerAdvice切到的Controller

@ExceptionHandler在Controller里的時(shí)候,就只會(huì)對(duì)那個(gè)Controller生效

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,363評(píng)論 6 532
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,497評(píng)論 3 416
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 176,305評(píng)論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 62,962評(píng)論 1 311
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,727評(píng)論 6 410
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 55,193評(píng)論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,257評(píng)論 3 441
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 42,411評(píng)論 0 288
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,945評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,777評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,978評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,519評(píng)論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,216評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 34,642評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 35,878評(píng)論 1 286
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,657評(píng)論 3 391
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,960評(píng)論 2 373

推薦閱讀更多精彩內(nèi)容