使用Spring MVC搭建一個web應用時,我們有很多種辦法處理異常并返回異常視圖給browser,下面我們分別介紹幾種異常的處理方式。
通過HandlerExceptionResolver處理異常
該接口是DispatcherServlet提供的唯一的異常處理機制,在Spring MVC內部所有的異常處理方式都是基于該機制實現的,包括@ExceptionHandler注解。
當一個未捕獲的Exception在DispatcherServlet處理請求的過程中發生時,Spring會使用該接口的實現來處理Exception。該接口唯一的方法resolveException抽象了Exception轉換為ModelAndView的過程,方法簽名是這樣的:
ModelAndViewresolveException(HttpServletRequest request,HttpServletResponse response,Object handler,Exception ex)
Spring允許多個該接口的實現同時工作,Spring會將已注冊的實現根據order排序后順序調用,直到某一個實現返回了非空結果,這時Spring會終止調用鏈并返回ModelAndView。
缺省情況下,ExceptionHandlerExceptionResolver、ResponseStatusExceptionResolver、DefaultHandlerExceptionResolver(排名根據優先級從高到低)這三個實現會被注冊到Spring。
Spring內置的HandlerExceptionResolver實現
Spring一共有以下4個典型的HandlerExceptionResolver實現:
SimpleMappingExceptionResolver
該實現需要你配置一個Exception類名到視圖的映射清單,他會基于你的配置將Exception映射為視圖并返回browser。
你的配置看起是這樣的,
java.sql.SqlException=sql_error_view
BizException=biz_error_view
除此之外,該實現還允許你定義視圖和response status的映射、要排除的Exception、缺省異常視圖、缺省response status等。(注意,這里的response status不用于HttpServletResponse.sendError,只用來HttpServletResponse.setStatus)
缺省情況下該實現并沒有注冊到Spring,你需要手動將他注冊到Spring并進行必要的配置才可使用。
ResponseStatusExceptionResolver
該實現并沒有明確指定返回什么視圖給browser,只是根據拋出的Exception類的@ResponseStatus注解,調用HttpServletResponse.sendError方法通知servlet容器處理該response status。
你可以這樣聲明一個自定義Exception,并在用戶無權限時在Controller中拋出:
@ResponseStatus(value=HttpStatus.FORBIDDEN)publicclassAuthzExceptionextendsRuntimeException{//...}
需要注意的是,這時如果你沒有配置servlet容器的error-page,servlet容器會返回缺省的異常頁面給browser。
這往往不是我們希望看到的,所以在使用@ResponseStatus注解時,我們一般要配合error-page或反向代理使用,在下面會有相關的介紹。
DefaultHandlerExceptionResolver
這個類實現了Spring內部的Exception如何映射到response status,并調用HttpServletResponse.sendError方法通知servlet容器處理。
如Spring會在請求的http method和@RequestMapping聲明的都不匹配時拋出org.springframework.web.HttpRequestMethodNotSupportedException,該實現收到后會將異常轉換為response status 405并調用HttpServletResponse.sendError。
有的同學就會有疑問了,這里Spring自己為什么不用@ResponseStatus注解?觀察代碼就會發現,這里不僅僅是將Exception簡單的映射到response status,還會針對不同的Exception有不同的處理(response.setHeader)和選擇性的記錄日志,這些是@ResponseStatus注解不能滿足的。
因為該實現和上面ResponseStatusExceptionResolver一樣只是調用HttpServletResponse.sendError方法通知servlet容器處理,所以你同樣需要考慮配合error-page或反向代理返回自定義異常視圖給browser。
亦或者你想改變sendError這一處理方式,比如直接返回自定義視圖給browser(其實完全可以在error-page中再統一處理,除非你很在意這點性能的話)。這時你可以通過@ExceptionHandler注解(因為優先級的原因,@ExceptionHandler用于處理Spring內部異常時優先級高于該實現)或繼承ResponseEntityExceptionHandler(也是基于@ExceptionHandler實現)自己實現Spring內部Exception的處理。
ExceptionHandlerExceptionResolver
這個就是@ExceptionHandler注解的處理實現類,它是一個high-level的實現,下面會專門說。
當然,如果上面的實現都滿足不了需求,你也可以自己實現HandlerExceptionResolver,并使用order控制他與其它實現的執行優先順序。
相對于HandlerExceptionResolver來言,這是一個high-level的處理方式。因為你基本不再需要和HttpServletRequest、HttpServletResponse這種底層API打交道,而是像編寫Controller方法一樣使用Spring Controller的幾乎所有注解來處理并返回異常(比如@ResponseBody)。這就意味著,不管是根據http請求頭的accept返回不同的content type,還是讀寫request、session都將變的非常簡單。
需要注意的是,@ExceptionHandler方法的位置決定了他的作用范圍,如果寫在Controller中那么他的作用域就是當前Controller,如果寫在ControllerAdvice中那么他的作用域就是ControllerAdvice的作用域(未特殊指定的ControllerAdvice就表示作用于全部Controller)。
/**
* 處理RestController產生的異常,返回json。
* @see ErrorController 處理非RestController產生的異常,返回html視圖。
*
* @author zaoheng.lb
*/@ControllerAdvice(annotations=RestController.class)publicclassRestErrorController{/**
? ? * 根據異常類型匹配處理spring mvc拋出的指定異常。
? ? *
? ? * 處理下述情況:
? ? *? 1、spring mvc內部異常(如conversion-service、jsr-303 validator)
? ? *? 2、Controller中業務代碼的BusinessException異常。
? ? *
? ? * @param ex
? ? * @return
? ? */@ExceptionHandler({TypeMismatchException.class,BindException.class,BusinessException.class})@ResponseBodypublicResponsehandleException(Exception ex){Response response=createResponse(ex);returnresponse;}}
上面說的都是在Spring MVC之內的異常處理,但是在DispatcherServlet之外也需要處理異常,比如filter Exception和HttpServletResponse.sendError產生的異常response status,這些如何處理呢?
servlet規范中的error-page就是設計用來處理拋出到容器級別的Exception和異常response status的。他支持異常類型和異常response status到異常處理url的配置,也支持缺省的異常處理url配置(用來兜底處理未配置的異常類型和異常response status)。
這是一個用web.xml來配置error-page的示例:
404/404java.sql.SqlException/sqlError/error
你可以編寫一個Controller響應“/error”這個url來統一的處理Exception和異常response status,Exception對象等信息可以通過request attribute拿到(如有)。
Spring boot應用
如果你的應用是Spring boot應用,那么恭喜你,你不再需要自己配置error-page和實現異常處理,因為這些Spring都幫你實現好了(包括根據accept返回html或json)。你需要做的僅僅是在視圖文件夾(velocity的話就是spring.velocity.resource-loader-path這個配置)下新建一個error文件夾,再將編寫好的異常頁面根據response status命名后放到這里即可。
例如你的視圖文件夾是templates的話,你的異常視圖文件結構應該是這樣的:
src/
+- main/
? ? +- java/
? ? |? +
? ? +- resources/
? ? ? ? +- templates/
? ? ? ? ? ? +- error/
? ? ? ? ? ? |? +- 404.vm
? ? ? ? ? ? |? +- 5xx.vm
? ? ? ? ? ? +-
當然如果Spring boot的默認實現不滿足你的需求(比如json屬性名稱不滿足),你可以繼承并修改他的行為。詳見org.springframework.boot.autoconfigure.web.ErrorMvcAutoConfiguration。
nginx等反向代理可以在頁面返回browser之前對頁面進行修改,所以在nginx中配置error_page也可以達到將異常response status轉換為異常頁面返回browser的目的。但在nginx中,你無法方便的獲取到Java Exception對象等信息。
error_page 404? ? ? ? /404.html;
error_page 502 503? ? /5xx.html;
個人認為,最佳實踐是多種方式配合使用,達到完善的異常處理效果。
方式處理Exception處理異常response status
HandlerExceptionResolver(包括@ExceptionHandler)支持不支持
servlet error-page支持支持
反向代理不支持支持
使用@ExceptionHandler注解處理Controller的Exception:在ExceptionHandler里我們一定可以拿到Exception對象,所以你可以根據Exception對象返回異常視圖給browser。
使用servlet error-page兜底處理非Controller Exception和sendError產生的異常response status:此時不一定有Exception對象(如404),所以你可以根據response status返回異常視圖給browser。
使用nginx配置一些特殊的異常response status:如502的異常頁面,配置后可以防止servlet容器在重啟時用戶看到nginx的缺省異常頁面。
以上,歡迎討論和指正。(* ̄︶ ̄)