https://juejin.im/post/5dd1fbcff265da0bf175d51d
https://www.cnblogs.com/muxi0407/p/11950475.html
Spring MVC提供了好幾種方法讓我來定製異常的處理。
本文參考:Exception Handling in Spring MVC
爲異常定製HTTP狀態碼
默認如果我們在controller中拋出異常,Spring MVC會給用戶響應500頁面,幷包含詳細的錯誤信息。
如果我們想修改錯誤對應的HTTP狀態碼,我們可以在對應的異常上面添加@ResponseStatus
註解,通過這個註解我們可以設置這個異常對應的HTTP狀態碼和錯誤信息,例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
@Controller public class ExceptionController { @RequestMapping("/") public void test(){ throw new NotFoundException(); } } @ResponseStatus(value = HttpStatus.NOT_FOUND, reason = "not found") public class NotFoundException extends RuntimeException{ } |
然後請求,可以發現頁面不一樣了:
Controller級別的錯誤攔截處理
通過@ResponseStatus
註解,我們雖然可以定製HTTP狀態碼和錯誤信息了,但是完全不夠用。
第一,只能設置自己寫的異常,對於已有的異常,無法進行擴展。
第二,無法定製錯誤頁面,默認的錯誤頁面我們基本是不會使用的。
對於以上兩個問題,可以在Controller裏添加方法來攔截處理異常。方法需要使用@ExceptionHandler
註解。註解後,方法會攔截當前Controller的請求處理方法(被@RequestMapping
註解的方法)所拋出的異常。同時這個異常攔截方法,可以返回視圖,該視圖用於渲染錯誤信息。同時還可以在這個異常攔截方法上,使用@ResponseStatus
來實現對已有異常的HTTP狀態碼定製,具體看例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
@Controller public class ExceptionHandlingController { // 請求處理方法 ... // 異常處理方法 // 定製一個已有異常的HTTP狀態碼 @ResponseStatus(value=HttpStatus.CONFLICT, reason="Data integrity violation") // 409 @ExceptionHandler(DataIntegrityViolationException.class) public void conflict() { // 啥也不幹 } // 指定view來渲染對應的異常 @ExceptionHandler({SQLException.class,DataAccessException.class}) public String databaseError() { // Nothing to do. Returns the logical view name of an error page, passed // to the view-resolver(s) in usual way. // Note that the exception is NOT available to this view (it is not added // to the model) but see "Extending ExceptionHandlerExceptionResolver" // below. // 啥也不幹,就返回異常頁面view的名稱 // 注意這裏的view訪問不到異常,因爲異常沒有添加到model中 return "databaseError"; } // 攔截該Controller拋出的所有異常,同時把異常信息通過ModelAndView傳給視圖 // 或者你可以繼承ExceptionHandlerExceptionResolver來實現,見下文 @ExceptionHandler(Exception.class) public ModelAndView handleError(HttpServletRequest req, Exception ex) { logger.error("Request: " + req.getRequestURL() + " raised " + ex); ModelAndView mav = new ModelAndView(); mav.addObject("exception", ex); mav.addObject("url", req.getRequestURL()); mav.setViewName("error"); return mav; } } |
注意,使用@ExceptionHandler
一定要指定處理的是哪個異常,否則會報異常:java.lang.IllegalArgumentException: No exception types mapped to {public java.lang.String XXController.exceptionHandler()}
全局異常處理
Controller級別的異常控制雖然已經夠強大了,但是我們總不可能每個Controller都寫一個handleError方法吧,所以我們一定需要一個全局的異常處理方法。藉助@ControllerAdvice
可以簡單直接的實現這個需求。
@ControllerAdvice
是Spring3.2添加的註解,和名字一樣,這個註解提供了增強Controller的功能,可把advice類中的@ExceptionHandler
、@InitBinder
、@ModelAttribute
註解的方法應用到所有的Controller中去。最常用的就是@ExceptionHandler
了。本來我們需要在每個Controller中定義@ExceptionHandler
,現在我們可以聲明一個@ControllerAdvice
類,然後定義一個統一的@ExceptionHandler
方法。
比如上面的例子,用@ControllerAdvice
的寫法如下:
1 2 3 4 5 6 7 8 |
@ControllerAdvice class GlobalControllerExceptionHandler { @ResponseStatus(HttpStatus.CONFLICT) // 409 @ExceptionHandler(DataIntegrityViolationException.class) public void handleConflict() { // 啥也不幹 } } |
如果你想攔截所有錯誤,那其實和上面的Controller級別的例子一樣,設置攔截的Exception爲Exception.class
即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
@ControllerAdvice class GlobalDefaultExceptionHandler { public static final String DEFAULT_ERROR_VIEW = "error"; @ExceptionHandler(value = Exception.class) public ModelAndView defaultErrorHandler(HttpServletRequest req, Exception e) throws Exception { // 這裏需要注意一下,因爲這個方法會攔截所有異常,包括設置了@ResponseStatus註解的異常,如果你不想攔截這些異常,可以過濾一下,然後重新拋出 if (AnnotationUtils.findAnnotation (e.getClass(), ResponseStatus.class) != null) throw e; // 組裝異常信息給視圖 ModelAndView mav = new ModelAndView(); mav.addObject("exception", e); mav.addObject("url", req.getRequestURL()); mav.setViewName(DEFAULT_ERROR_VIEW); return mav; } } |
更深層的攔截
上面說的Controller級別以及Controller Advice級別的攔截,是基於註解的,是高級特性。底層實現上,Spring使用的是HandlerExceptionResolver
。
所有定義在DispatcherServlet
應用上下文中的bean,只要是實現了HandlerExceptionResolver
接口,都會用來異常攔截處理。
看一下接口的定義:
1 2 3 4 |
public interface HandlerExceptionResolver { ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex); } |
handler
參數是拋出異常的Controller的引用。
Spring實現了幾種HandlerExceptionResolver
,這些類是上面提到的幾個特性的基礎:
ExceptionHandlerExceptionResolver
:判斷異常是否可以匹配到對應Controller或者Controller Advice中的@ExceptionHandler
方法,如果可以則觸發(前文提到的異常攔截方法的特性就是這個類實現的)ResponseStatusExceptionResolver
:判斷異常是否被@ResponseStatus
註解,如果是,則使用註解的信息來更新Response(前文提到的自定義HTTP狀態碼就是用這個特性實現的)DefaultHandlerExceptionResolver
:轉換Spring異常,並轉換爲HTTP狀態碼(Spring內部使用)
這幾個HandlerExceptionResolver
會按照這個順序來執行,也就是異常處理鏈。
這裏可以看到,resolveException
方法簽名中沒有Model
參數,所以@ExceptionHandler
方法也不能注入這個參數,所以上文中,異常攔截方法只能自己新建Model。
所以,如果你需要,你可以自己繼承HandlerExceptionResolver
來實現自己的異常處理鏈。然後再實現Ordered
接口,這樣就可以控制處理器的執行順序。
SimpleMappingExceptionResolver
Spring提供了一個很方便使用的HandlerExceptionResolver
,叫SimpleMappingExceptionResolver
。他有很多實用的功能:
- 映射異常名稱到視圖名稱(異常名稱只需要指定類名,不需要包名)
- 指定一個默認的錯誤頁面
- 把異常打印到log上
- 指定exception到視圖中的屬性名,默認的屬性名就是exception。(
@ExceptionHandler
方法指定的視圖默認沒法獲取異常,而SimpleMappingExceptionResolver
指定的視圖可以)
用法如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
<bean id="simpleMappingExceptionResolver" class= "org.springframework.web.servlet.handler.SimpleMappingExceptionResolver"> <property name="exceptionMappings"> <map> <entry key="DatabaseException" value="databaseError"/> <entry key="InvalidCreditCardException" value="creditCardError"/> </map> </property> <!-- See note below on how this interacts with Spring Boot --> <property name="defaultErrorView" value="error"/> <property name="exceptionAttribute" value="ex"/> <!-- Name of logger to use to log exceptions. Unset by default, so logging is disabled unless you set a value. --> <property name="warnLogCategory" value="example.MvcLogger"/> </bean> |
Java Configuration:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
@Configuration @EnableWebMvc // Optionally setup Spring MVC defaults (if you aren't using // Spring Boot & haven't specified @EnableWebMvc elsewhere) public class MvcConfiguration extends WebMvcConfigurerAdapter { @Bean(name="simpleMappingExceptionResolver") public SimpleMappingExceptionResolver createSimpleMappingExceptionResolver() { SimpleMappingExceptionResolver r = new SimpleMappingExceptionResolver(); Properties mappings = new Properties(); mappings.setProperty("DatabaseException", "databaseError"); mappings.setProperty("InvalidCreditCardException", "creditCardError"); r.setExceptionMappings(mappings); // None by default r.setDefaultErrorView("error"); // No default r.setExceptionAttribute("ex"); // Default is "exception" r.setWarnLogCategory("example.MvcLogger"); // No default return r; } ... } |
這裏最有用的可能就是defaultErrorView
了,他可以用於定製默認的錯誤頁面。
自己繼承SimpleMappingExceptionResolver
來擴展功能也是非常常見的
- 繼承類可以在構造函數中設置好默認配置
- 覆蓋
buildLogMessage
方法來自定義日誌信息,默認返回固定的:Handler execution resulted in exception - 覆蓋
doResolveException
方法,可以向錯誤日誌傳入更多自己需要的信息
例子如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
public class MyMappingExceptionResolver extends SimpleMappingExceptionResolver { public MyMappingExceptionResolver() { // 默認啓用日誌 setWarnLogCategory(MyMappingExceptionResolver.class.getName()); } @Override public String buildLogMessage(Exception e, HttpServletRequest req) { return "MVC exception: " + e.getLocalizedMessage(); } @Override protected ModelAndView doResolveException(HttpServletRequest req, HttpServletResponse resp, Object handler, Exception ex) { // 調用父類飛方法來獲得ModelAndView ModelAndView mav = super.doResolveException(req, resp, handler, ex); // 添加額外的字段給視圖 mav.addObject("url", request.getRequestURL()); return mav; } } |
REST異常處理
REST風格下,返回的錯誤信息是一個json而不是一個頁面,要如何做呢?特別簡單,定義一個返回信息的類:
1 2 3 4 5 6 7 8 9 |
public class ErrorInfo { public final String url; public final String ex; public ErrorInfo(String url, Exception ex) { this.url = url; this.ex = ex.getLocalizedMessage(); } } |
然後在錯誤處理函數上加上@ResponseBody
就行:
1 2 3 4 5 6 |
@ResponseStatus(HttpStatus.BAD_REQUEST) @ExceptionHandler(MyBadDataException.class) @ResponseBody ErrorInfo handleBadRequest(HttpServletRequest req, Exception ex) { return new ErrorInfo(req.getRequestURL(), ex); } |
什麼時候用什麼特效?
Spring給我們提供了很多選擇,我們要如何選擇呢?
- 如果異常是你自己聲明的,可以考慮使用
@ResponseStatus
註解 - 其他的異常可以使用
@ControllerAdvice
中的@ExceptionHandler
方法,或者用SimpleMappingExceptionResolver
- 如果Controller需要定製異常,可以在Controller中添加
@ExceptionHandler
方法。
如果你混用這幾個特性,那要注意了,Controller中的@ExceptionHandler
方法優先級比@ControllerAdvice
中的@ExceptionHandler
方法高,而如果有多個@ControllerAdvice
類,那執行順序是不確定的。
如何讓我們的異常得到期望的返回格式,這裏就需要用到了@ControllerAdvice或者RestControllerAdvice(如果全部異常處理返回json,那麼可以使用 @RestControllerAdvice 代替 @ControllerAdvice ,這樣在方法上就可以不需要添加 @ResponseBody。)。類似與@Controller與@RestController的區別