默認行爲
根據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:
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,它不會返回任何結果,原因稍後會有說明。
本章節代碼在me.chanjar.boot.def,使用DefaultExample運行。
注意:我們必須在application.properties添加server.error.include-stacktrace=always才能夠得到stacktrace。
爲何curl text/plain資源無法獲得error
如果你在logback-spring.xml裏一樣配置了這麼一段:
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的處理過程:
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<Map<String, Object>>,會被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裏的出了修改過的屬性,還有添加的屬性:
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信息,解決這個問題有兩個關鍵點:
請求的時候指定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訪問的結果:
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有這麼幾個:
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了。
實際上,從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#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頁面。
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的使用,這裏只是在做一些補充說明:
@ExceptionHandler配合@ControllerAdvice用時,能夠應用到所有被@ControllerAdvice切到的Controller
@ExceptionHandler在Controller裏的時候,就只會對那個Controller生效
參考文檔:
Spring Boot 1.5.4.RELEASE Documentation
Spring framework 4.3.9.RELEASE Documentation
附錄I
下表列出哪些特性是Spring Boot的,哪些是Spring MVC的:
Feature | Spring Boot | Spring MVC |
---|---|---|
BasicErrorController | Yes | |
ErrorAttributes | Yes | |
ErrorViewResolver | Yes | |
@ControllerAdvice | Yes | |
@ExceptionHandler | Yes | |
@ResponseStatus | Yes | |
HandlerExceptionResolver | Yes |