一.默認映射
我們在做Web應用的時候,請求處理過程中發生錯誤是非常常見的情況。Spring Boot提供了一個默認的映射:/error
,當處理中拋出異常之后,會轉到該請求中處理,並且該請求有一個全局的錯誤頁面用來展示異常內容。
選擇一個之前實現過的Web應用為基礎,啟動該應用,訪問一個不存在的URL,或是修改處理內容,直接拋出異常,如:
1 @RequestMapping("/hello") 2 public String hello() throws Exception { 3 throw new Exception("發生錯誤"); 4 }
此時,可以看到類似下面的報錯頁面,該頁面就是Spring Boot提供的默認error映射頁面。
alt=默認的錯誤頁面
二.映射到頁面
雖然,Spring Boot中實現了默認的error映射,但是在實際應用中,上面你的錯誤頁面對用戶來說並不夠友好,我們通常需要去實現我們自己的異常提示。
下面我們以之前的Web應用例子為基礎,進行統一異常處理的改造。
- 創建全局異常處理類:通過使用
@ControllerAdvice
定義統一的異常處理類,而不是在每個Controller中逐個定義。@ExceptionHandler
用來定義函數針對的異常類型,最后將Exception對象和請求URL映射到error.html
中
1 @ControllerAdvice 2 class GlobalExceptionHandler { 3 4 public static final String DEFAULT_ERROR_VIEW = "error"; 5 6 @ExceptionHandler(value = Exception.class) 7 public ModelAndView defaultErrorHandler(HttpServletRequest req, Exception e) throws Exception { 8 ModelAndView mav = new ModelAndView(); 9 mav.addObject("exception", e); 10 mav.addObject("url", req.getRequestURL()); 11 mav.setViewName(DEFAULT_ERROR_VIEW); 12 return mav; 13 } 14 15 }
- 實現
error.html
頁面展示:在templates
目錄下創建error.html
,將請求的URL和Exception對象的message輸出。
1 <!DOCTYPE html> 2 <html> 3 <head lang="en"> 4 <meta charset="UTF-8" /> 5 <title>統一異常處理</title> 6 </head> 7 <body> 8 <h1>Error Handler</h1> 9 <div th:text="${url}"></div> 10 <div th:text="${exception.message}"></div> 11 </body> 12 </html>
啟動該應用,訪問:http://localhost:8080/hello
,可以看到如下錯誤提示頁面。
alt=自定義的錯誤頁面
通過實現上述內容之后,我們只需要在Controller
中拋出Exception
,當然我們可能會有多種不同的Exception
。然后在@ControllerAdvice
類中,根據拋出的具體Exception
類型匹配@ExceptionHandler
中配置的異常類型來匹配錯誤映射和處理。
三.返回JSON格式
在上述例子中,通過@ControllerAdvice
統一定義不同Exception映射到不同錯誤處理頁面。而當我們要實現RESTful API時,返回的錯誤是JSON格式的數據,而不是HTML頁面,這時候我們也能輕松支持。
在上述例子中,通過@ControllerAdvice
統一定義不同Exception映射到不同錯誤處理頁面。而當我們要實現RESTful API時,返回的錯誤是JSON格式的數據,而不是HTML頁面,這時候我們也能輕松支持。
本質上,只需在@ExceptionHandler
之后加入@ResponseBody
,就能讓處理函數return的內容轉換為JSON格式。
下面以一個具體示例來實現返回JSON格式的異常處理。
- 創建統一的JSON返回對象,code:消息類型,message:消息內容,url:請求的url,data:請求返回的數據
1 public class ErrorInfo<T> { 2 3 public static final Integer OK = 0; 4 public static final Integer ERROR = 100; 5 6 private Integer code; 7 private String message; 8 private String url; 9 private T data; 10 11 // 省略getter和setter 12 13 }
- 創建一個自定義異常,用來實驗捕獲該異常,並返回json
1 public class MyException extends Exception { 2 3 public MyException(String message) { 4 super(message); 5 } 6 7 }
Controller
中增加json映射,拋出MyException
異常
1 @Controller 2 public class HelloController { 3 4 @RequestMapping("/json") 5 public String json() throws MyException { 6 throw new MyException("發生錯誤2"); 7 } 8 9 }
- 為
MyException
異常創建對應的處理
1 @ControllerAdvice 2 public class GlobalExceptionHandler { 3 4 @ExceptionHandler(value = MyException.class) 5 @ResponseBody 6 public ErrorInfo<String> jsonErrorHandler(HttpServletRequest req, MyException e) throws Exception { 7 ErrorInfo<String> r = new ErrorInfo<>(); 8 r.setMessage(e.getMessage()); 9 r.setCode(ErrorInfo.ERROR); 10 r.setData("Some Data"); 11 r.setUrl(req.getRequestURL().toString()); 12 return r; 13 } 14 15 }
- 啟動應用,訪問:http://localhost:8080/json,可以得到如下返回內容:
{ code: 100, data: "Some Data", message: "發生錯誤2", url: "http://localhost:8080/json" }
代碼示例至此,已完成在Spring Boot中創建統一的異常處理,實際實現還是依靠Spring MVC的注解,更多更深入的使用可參考Spring MVC的文檔。
本文的相關例子可以查看下面倉庫中的chapter3-1-6
目錄:
- Github:https://github.com/dyc87112/SpringBoot-Learning
- Gitee:https://gitee.com/didispace/SpringBoot-Learning
附:
當一個Controller中有方法加了@ExceptionHandler之后,這個Controller其他方法中沒有捕獲的異常就會以參數的形式傳入加了@ExceptionHandler注解的那個方法中。
首先需要為自己的系統設計一個自定義的異常類,通過它來傳遞狀態碼。
1 /** * Created by liuruijie. 2 * 自定義異常 3 */ 4 public class SystemException extends RuntimeException{ 5 private String code;//狀態碼 6 public SystemException(String message, String code) { 7 super(message); 8 this.code = code; 9 } 10 public String getCode() { 11 return code; 12 } 13 }
第一種思路,設計一個基類。
1 /** 2 * Created by liuruijie. 3 * 處理異常的類,需要處理異常的Controller直接繼承這個類 4 */ 5 public class BaseController { 6 /** 7 * 處理Controller拋出的異常 8 * @param e 異常實例 9 * @return Controller層的返回值 10 */ 11 @ExceptionHandler 12 @ResponseBody 13 public Object expHandler(Exception e){ 14 if(e instanceof SystemException){ 15 SystemException ex= (SystemException) e; 16 return WebResult.buildResult().status(ex.getCode()) 17 .msg(ex.getMessage()); 18 }else{ 19 e.printStackTrace(); 20 return WebResult.buildResult().status(Config.FAIL) 21 .msg("系統錯誤"); 22 } 23 } 24 }
之后所有需要異常處理的Controller都繼承這個類,從而獲取到異常處理的方法。
雖然這種方式可以解決問題,但是極其不靈活,因為動用了繼承機制就只為獲取一個默認的方法,這顯然是不好的。
第二種思路,將這個基類變為接口,提供此方法的默認實現(也就是接口中的default方法,java8開始支持接口方法的默認實現)
1 /** 2 * Created by liuruijie. 3 * 接口形式的異常處理 4 */ 5 public interface DataExceptionSolver { 6 @ExceptionHandler 7 @ResponseBody 8 default Object exceptionHandler(Exception e){ 9 try { 10 throw e; 11 } catch (SystemException systemException) { 12 systemException.printStackTrace(); 13 return WebResult.buildResult().status(systemException.getCode()) 14 .msg(systemException.getMessage()); 15 } catch (Exception e1){ 16 e1.printStackTrace(); 17 return WebResult.buildResult().status(Config.FAIL) 18 .msg("系統錯誤"); 19 } 20 } 21 }
於是可以寫一個全局的異常處理類:
1 /** 2 * Created by liuruijie on 2016/12/28. 3 * 全局異常處理,捕獲所有Controller中拋出的異常。 4 */ 5 @ControllerAdvice 6 public class GlobalExceptionHandler { 7 //處理自定義的異常 8 @ExceptionHandler(SystemException.class) 9 @ResponseBody 10 public Object customHandler(SystemException e){ 11 e.printStackTrace(); 12 return WebResult.buildResult().status(e.getCode()).msg(e.getMessage()); 13 } 14 //其他未處理的異常 15 @ExceptionHandler(Exception.class) 16 @ResponseBody 17 public Object exceptionHandler(Exception e){ 18 e.printStackTrace(); 19 return WebResult.buildResult().status(Config.FAIL).msg("系統錯誤"); 20 } 21 }
這個類中只處理了兩個異常,但是已經滿足了大部分需要,如果還有需要特殊處理的地方,可以再加上處理的方法就行了。第三種實現方式是目前我知道的最優雅的方式了。
如此,我們現在的Controller中的方法就可以很簡潔了,比如處理登陸的邏輯就可以這樣簡單的寫:
1 /** 2 * Created by liuruijie on 2016/12/28. 3 * 賬號 4 */ 5 @RestController 6 @RequestMapping("passport") 7 public class PassportController { 8 PassportService passportService; 9 @RequestMapping("login") 10 public Object doLogin(HttpSession session, String username, String password){ 11 User user = passportService.doLogin(username, password); 12 session.setAttribute("user", user); 13 return WebResult.buildResult().redirectUrl("/student/index"); 14 } 15 }
而在passprotService的doLogin方法中,可能會拋出用戶名或密碼錯誤等異常,然后就會交由GlobalExceptionHandler去處理,直接返回異常信息給前端,然后前端也不需要關心是否返回了異常,因為這些都已經定義好了。
前端js代碼只需要這樣寫:
1 //登陸 2 AJAX.POST("/passport/login", { 3 username:name, 4 password:psw 5 })
一個異常在其中流轉的過程為:
比如doLogin方法拋出了自定義異常,其code為:FAIL,message為:用戶名或密碼錯誤,由於在controller的方法中沒有捕獲這個異常,所以會將異常拋給GlobalExceptionHandler,然后GlobalExceptionHandler通過WebResult將狀態碼和提示信息返回給前端,前端通過默認的處理函數,彈框提示用戶“用戶名或密碼錯誤”。而對於這樣的一次交互,我們根本不用編寫異常處理部分的邏輯。
到這里,代碼已經簡潔了很多,而且重用性大大提高。
HTTP狀態碼
100 到199 的狀態碼代表信息,描述對於請求的處理。
200 到 299 的狀態碼表示客戶端發來的請求已經被接收並正確處理。
300 到 399 的狀態碼表示客戶端需要進一步的處理才能完成請求,比如重定向到另一個地址。
400 到 499 的狀態碼表示客戶端的請求有錯誤,需要修正。404就是這種情況。
500 到 599 的狀態碼表示服務器在處理客戶端請求時發生了內部錯誤。