Spring MVC的異常處理

使用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

當然,如果上面的實現都滿足不了需求,你也可以自己實現HandlerExceptionResolver,并使用order控制他與其它實現的執行優先順序。

通過@ExceptionHandler注解處理異常

相對于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之外的異常處理

上面說的都是在Spring MVC之內的異常處理,但是在DispatcherServlet之外也需要處理異常,比如filter Exception和HttpServletResponse.sendError產生的異常response status,這些如何處理呢?

servlet error-page

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的缺省異常頁面。

以上,歡迎討論和指正。(* ̄︶ ̄)

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,156評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,401評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,069評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,873評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,635評論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,128評論 1 323
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,203評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,365評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,881評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,733評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,935評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,475評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,172評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,582評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,821評論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,595評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,908評論 2 372

推薦閱讀更多精彩內容