錯誤處理機制提及來是每一個網站架構開發的核心部分,不少時候咱們並無去關注他們,其實錯誤在咱們平常訪問過程當中時長出現,對錯誤機制進行了解也是開發一個好的網站所必備的技能之一。html
spring boot默認會根據不一樣的請求客戶端,返回不一樣的結果: 一、狀況一:返回一個默認的錯誤頁面java
當咱們使用web訪問出錯的時候,會跳到這樣的錯誤頁面,其信息以下所示:web
Whitelabel Error Page This application has no explicit mapping for /error, so you are seeing this as a fallback. Mon Dec 17 14:50:33 CST 2018 There was an unexpected error (type=Bad Request, status=400). Failed to convert value of type 'java.lang.String' to required type 'java.lang.Integer'; nested exception is java.lang.NumberFormatException: For input string: "aaa"
二、 狀況二:返回json信息spring
當咱們使用其餘的客戶端,例如postman模擬請求的時候,返回的信息則是json數據格式:json
{ "timestamp": "2018-12-17T06:59:00.851+0000", "status": 404, "error": "Not Found", "message": "No message available", "path": "/somepage" }
這裏要模擬一個
頁面不存在的錯誤
錯誤,最好先把登陸過濾器關掉,不然請求任何不存在的頁面都會給你過濾到登陸界面,不會出現錯誤信息。瀏覽器
咱們開發網站過程當中,顯然不會使用這些默認方式,而是要本身去定製反饋結果的。但咱們首先仍是先去了解SpringBoot的默認錯誤處理過程,瞭解一下原理。springboot
參考自動配置類ErrorMvcAutoConfiguration
。咱們看看該自動配置類爲容器中添加了以下組件:架構
查看BasicErrorController源碼app
@Controller @RequestMapping({"${server.error.path:${error.path:/error}}"}) public class BasicErrorController extends AbstractErrorController { @RequestMapping( produces = {"text/html"} ) public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) { HttpStatus status = this.getStatus(request); Map<String, Object> model = Collections.unmodifiableMap(this.getErrorAttributes(request, this.isIncludeStackTrace(request, MediaType.TEXT_HTML))); response.setStatus(status.value()); // 去哪一個頁面做爲錯誤頁面:包含頁面的地址和內容 ModelAndView modelAndView = this.resolveErrorView(request, response, status, model); return modelAndView != null ? modelAndView : new ModelAndView("error", model); } @RequestMapping public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) { Map<String, Object> body = this.getErrorAttributes(request, this.isIncludeStackTrace(request, MediaType.ALL)); HttpStatus status = this.getStatus(request); return new ResponseEntity(body, status); } }
能夠知道,該組件用於默認處理/error
請求,其中須要留意:ide
error.xml
裏配置的錯誤頁面規則。一旦系統出現4XX或者5XX之類的錯誤,ErrorPageCustomizer就會生效(定製錯誤的響應規則),使請求來到/error
。這時候,BasicErrorController控制器會處理這個請求。
觀看上述代碼咱們能夠發現,之因此出現兩種錯誤結果,無非就是對error進行處理的controller會根據不一樣的請求頭:
Accept: text/html
Accept:"*/*"
這二者進行不一樣的反饋,前者返回錯誤頁面信息,後者返回json數據。咱們來看看錯誤響應頁面的視圖解析器:
protected ModelAndView resolveErrorView(HttpServletRequest request, HttpServletResponse response, HttpStatus status, Map<String, Object> model) { Iterator var5 = this.errorViewResolvers.iterator(); ModelAndView modelAndView; do { if (!var5.hasNext()) { return null; } ErrorViewResolver resolver = (ErrorViewResolver)var5.next(); modelAndView = resolver.resolveErrorView(request, status, model); } while(modelAndView == null); return modelAndView; }
這段代碼拿到了異常視圖解析器(ErrorViewResolvers
)類型來進行處理,當前咱們註冊的是DefaultErrorViewResolver
,查看源碼:
public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) { ModelAndView modelAndView = this.resolve(String.valueOf(status.value()), model); if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) { //status.series() 狀態碼 modelAndView = this.resolve((String)SERIES_VIEWS.get(status.series()), model); } return modelAndView; } private ModelAndView resolve(String viewName, Map<String, Object> model) { String errorViewName = "error/" + viewName; TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName, this.applicationContext); return provider != null ? new ModelAndView(errorViewName, model) : this.resolveResource(errorViewName, model); }
默認spring boot會去找到某個頁面:error/狀態碼.html。
跳轉到模板頁面:error/狀態碼,也就是說,咱們若是想自定義錯誤頁面的話,將錯誤頁面命名爲狀態碼.html,並放在模板文件夾(templates)的error文件夾下,發生此狀態碼的錯誤就會來到對應的頁面;查看源碼咱們也能夠發現,命名爲4xx.html(5xx.html)則能夠處理全部以4(5)開頭的錯誤碼錯誤,即均可以跳到該頁面;不過spring boot會優先尋找直接對應的錯誤頁面,若是404錯誤會優先選取404.html做爲錯誤頁面;
錯誤頁面能獲取到的信息(DefaultErrorAttributes
):
即咱們能夠在錯誤頁面裏獲取到錯誤信息,示例以下:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <h1>This is 4xx error page:</h1> <p>*timestamp: [[${timestamp}]]</p> <p>*status: [[${status}]]</p> </body> </html>
即咱們沒有對應的error錯誤文件夾,也能夠放在靜態資源文件夾下。
例如,放在static文件夾下,一樣能夠來到該頁面,只不過不能被模板引擎渲染而已。
這種狀況會來到SpringBoot默認的錯誤提示頁面,該視圖對象的信息咱們能夠經過查看源碼獲悉,其位置位於ErrorMvcAutoConfiguration
中:
@Configuration @ConditionalOnProperty( prefix = "server.error.whitelabel", name = {"enabled"}, matchIfMissing = true ) @Conditional({ErrorMvcAutoConfiguration.ErrorTemplateMissingCondition.class}) protected static class WhitelabelErrorViewConfiguration { private final ErrorMvcAutoConfiguration.StaticView defaultErrorView = new ErrorMvcAutoConfiguration.StaticView(); protected WhitelabelErrorViewConfiguration() { } @Bean( name = {"error"} ) @ConditionalOnMissingBean( name = {"error"} ) public View defaultErrorView() { return this.defaultErrorView; } @Bean @ConditionalOnMissingBean public BeanNameViewResolver beanNameViewResolver() { BeanNameViewResolver resolver = new BeanNameViewResolver(); resolver.setOrder(2147483637); return resolver; } }
spring boot會根據請求頭給予不一樣的返回類型數據。上一節講到的是定義錯誤頁面,還差一種方式:即其餘客戶端訪問狀況下返回json數據的問題,這一節,來處理這個問題。即如何定製錯誤的json數據。
咱們先自定義一種異常,例如用戶不存在的異常:
package com.zhaoyi.springboot.restweb.exception; public class UserNotExistException extends RuntimeException{ public UserNotExistException(){ super("用戶不存在"); } }
而後在應用程序的某個地方拋出該異常:
@RequestMapping({"user"}) public String index(@RequestParam("user") String user){ if(user.equals("aaa")){ throw new UserNotExistException(); } return "index"; }
經過訪問
/user?user=aaa
觸發該異常。
顯然,若是咱們不作任何處理,SpringBoot會默認將錯誤處理到咱們以前配置過的頁面,運行時錯誤對應的是500,即會跳轉到咱們的5xx頁面:
this is 5xx error page: status ------ 500 message ------ 用戶不存在
那麼,咱們該如何將此錯誤自定義呢,能夠運用springMVC的知識,在controller下面定義個異常處理器:
package com.zhaoyi.springboot.restweb.controller; import com.zhaoyi.springboot.restweb.exception.UserNotExistException; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseBody; import java.util.HashMap; import java.util.Map; @ControllerAdvice public class MyExcetionHandler { @ResponseBody @ExceptionHandler(UserNotExistException.class) public Map<String,Object> handlerException(Exception e){ Map<String, Object> map = new HashMap<>(); map.put("myCode", "custom code"); map.put("message", e.getMessage()); return map; } }
咱們在此訪問一樣的觸發異常的地址,就能夠如願的獲得本身想要的自定義錯誤信息了:
{"myCode":"custom code","message":"用戶不存在"}
但這種方式有點問題,沒有自適應效果,也就是咱們用瀏覽器也好,其餘的客戶端也好,返回的都是這段json數據。那麼,咱們如何想springboot那樣作到異常返回的自適應呢?(瀏覽器返回錯誤頁面,其餘客戶端返回json數據)。很簡單,轉發到/error
,交由SpringBoot處理便可。
注意註釋掉以前的處理代碼。
這時候,咱們若是換用不一樣的客戶端訪問就會獲得相應的反饋了,好比用瀏覽器能夠獲得以下的返回數據:
<html> <body> <h1>Whitelabel Error Page</h1> <p>This application has no explicit mapping for /error, so you are seeing this as a fallback. </p> <div id='created'>Mon Dec 17 16:15:30 CST 2018</div> <div>There was an unexpected error (type=OK, status=200).</div><div>?????</div></body></html>
可是新的問題又出現了,即:咱們自定義的頁面並無獲得解析,springboot仍是默認使用了以前分析過的,什麼都沒有定義的時候跳轉到空白錯誤頁面的狀況。
仔細觀察咱們會發現,實際上是錯誤狀態碼有問題,這裏是錯誤碼已經由500變爲了200.問題出在哪裏呢?出在咱們在轉發的時候,沒有設置一個錯誤狀態碼:所以,咱們還須要在轉發以前設置狀態碼。如何設置,先來查看springboot相關處理錯誤信息的源碼
@RequestMapping( produces = {"text/html"} ) public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) { // 經過此處獲取錯誤碼 HttpStatus status = this.getStatus(request); Map<String, Object> model = Collections.unmodifiableMap(this.getErrorAttributes(request, this.isIncludeStackTrace(request, MediaType.TEXT_HTML))); response.setStatus(status.value()); ModelAndView modelAndView = this.resolveErrorView(request, response, status, model); return modelAndView != null ? modelAndView : new ModelAndView("error", model); }
繼續定位源碼this.getStatus(request);
:
protected HttpStatus getStatus(HttpServletRequest request) { Integer statusCode = (Integer)request.getAttribute("javax.servlet.error.status_code"); if (statusCode == null) { return HttpStatus.INTERNAL_SERVER_ERROR; } else { try { return HttpStatus.valueOf(statusCode); } catch (Exception var4) { return HttpStatus.INTERNAL_SERVER_ERROR; } } }
所以,咱們只須要在request
域中添加一個javax.servlet.error.status_code
屬性,就能夠以最優先的級別狀況設置狀態碼了。因此,改造後的代碼應該以下所示:
@ExceptionHandler(UserNotExistException.class) public String handlerException(Exception e, HttpServletRequest request){ Map<String, Object> map = new HashMap<>(); request.setAttribute("javax.servlet.error.status_code", 500); map.put("myCode", "custom code"); map.put("message", e.getMessage()); return "forward:/error"; }
這時候在運行發現能夠調到咱們自定義的5xx.html錯誤頁面了。顯示以下:
this is 5xx error page: status ------ 500 message ------ 用戶不存在 myCode ------
問題仍是有,咱們會發現,咱們自定義的數據不見了(myCode
),錯誤頁面只能獲取到SpringBoot默認寫入的信息。所以咱們還得繼續探索,如何才能既能調到自定義錯誤頁面,又能攜帶咱們自定義的錯誤數據。
咱們知道,出現錯誤之後,會相應到/error請求,同時交由BasicErrorController進行處理,他在處理錯誤的時候進行了自適應處理,響應回來並能夠獲取的數據是getErrorAttributes(是BasicErrorController的父類AbstractErrorController中定義的)
獲得的。
咱們則徹底能夠編寫一個ErrorController的實現類(或者繼承BasicErrorController),放在容器中。想一想有點麻煩,固然,還有選擇。
第二種辦法,注意第一種方法的某句話errorAttributes.getErrorAttributes....
,頁面上能用的數據,或者是json返回能用的數據都是經過他來獲得的。查看ErrorAttribute的來源:
@Bean @ConditionalOnMissingBean( value = {ErrorAttributes.class}, search = SearchStrategy.CURRENT ) public DefaultErrorAttributes errorAttributes() { return new DefaultErrorAttributes(this.serverProperties.getError().isIncludeException()); }
容器中的DefaultErrorAttributes來進行數據處理的,因此,咱們本身配置一個實現了ErrorAttributes類型接口這樣的Bean就能夠了,可是爲了方便,咱們最好繼承spring boot默認使用的DefaultErrorAttributes
來實現便可。
自定義ErrorAttribute,改變默認行爲
package com.zhaoyi.springboot.restweb.component; import org.springframework.boot.web.servlet.error.DefaultErrorAttributes; import org.springframework.boot.web.servlet.error.ErrorAttributes; import org.springframework.web.context.request.WebRequest; import java.util.Map; @Componet public class MyErrorAttributes extends DefaultErrorAttributes { @Override public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) { Map<String, Object> errorAttributes = super.getErrorAttributes(webRequest, includeStackTrace); // 這裏隨便寫一個本身的 errorAttributes.put("someCode", "attribute add atrribute"); // 從request請求域中獲取ext的值 errorAttributes.put("ext", webRequest.getAttribute("ext", WebRequest.SCOPE_REQUEST)); return errorAttributes; } }
注意:這是一個組件,不要忘記添加
@Componet
註解,否則沒法加入到容器中。
在這裏咱們使用request域傳遞信息,而且經過WebRequest.getAttribute("param", SCOPE)
獲取其信息,WebRequest.SCOPE_REQUEST
的取值對應什麼,點進WebRequest代碼內容就能夠看到對應信息了。這樣,咱們還須要修改異常處理器的代碼,以下所示:
@ExceptionHandler(UserNotExistException.class) public String handlerException(Exception e, HttpServletRequest request){ Map<String, Object> map = new HashMap<>(); request.setAttribute("javax.servlet.error.status_code", 500); map.put("myCode", "custom code"); map.put("message", e.getMessage()); request.setAttribute("ext", map); return "forward:/error"; }
使用postman訪問異常頁面,返回結果以下:
{ "timestamp": "2018-12-17T08:54:29.906+0000", "status": 500, "error": "Internal Server Error", "message": "用戶不存在", "path": "/user", "someCode": "attribute add atrribute", "ext": { "myCode": "custom code", "message": "用戶不存在" } }
錯誤先關的知識就到這裏爲止了,咱們還得繼續往下探索,下面的內容會愈來愈有意思,他是什麼呢?
—— 嵌入式Servlet容器配置修改。