深刻理解Spring異常處理

1.前言

相信咱們每一個人在SpringMVC開發中,都遇到這樣的問題:當咱們的代碼正常運行時,返回的數據是咱們預期格式,好比json或xml形式,可是一旦出現了異常(好比:NPE或者數組越界等等),返回的內容確實服務端的異常堆棧信息,從而致使返回的數據不能使客戶端正常解析; 很顯然,這些並非咱們但願的結果。html

咱們知道,一個較爲常見的系統,會涉及控制層,服務(業務)層、緩存層、存儲層以及接口調用等,其中每個環節都不可避免的會遇到各類不可預知的異常須要處理。若是每一個步驟都單獨try..catch會使系統顯的很雜亂,可讀性差,維護成本高;常見的方式就是,實現統一的異常處理,從而將各種異常從各個模塊中解耦出來;spring

2.常見全局異常處理

在Spring中常見的全局異常處理,主要有三種:json

(1)註解ExceptionHandler數組

(2)繼承HandlerExceptionResolver接口緩存

(3)註解ControllerAdvice服務器

在後面的講解中,主要以HTTP錯誤碼:400(請求無效)和500(內部服務器錯誤)爲例,先看一下測試代碼以及沒有任何處理的返回結果,以下:app

1.png

圖1:測試代碼源碼分析

2.png

圖2:沒有異常的錯誤返回測試

2.1註解ExceptionHandler

註解ExceptionHandler做用對象爲方法,最簡單的使用方法就是放在controller文件中,詳細的註解定義再也不介紹。若是項目中有多個controller文件,一般能夠在baseController中實現ExceptionHandler的異常處理,而各個contoller繼承basecontroller從而達到統一異常處理的目的。由於比較常見,簡單代碼以下:spa

3.png

圖3:Controller中的ExceptionHandler使用

在返回異常時,添加了所屬的類名,便於你們記憶理解。運行看一下結果:

4.png

圖4:添加ExceptionHandler以後的結果

  • 優勢:ExceptionHandler簡單易懂,而且對於異常處理沒有限定方法格式;

  • 缺點:因爲ExceptionHandler僅做用於方法,對於多個controller的狀況,僅爲了一個方法,全部須要異常處理的controller都繼承這個類,明明不相關的東西,強行給他們找個爹,不太好。

2.2註解ControllerAdvice

這裏雖然說是ControllerAdvice註解,實際上是其與ExceptionHandler的組合使用。在上文中能夠看到,單獨使用@ExceptionHandler時,其必須在一個Controller中,然而當其與ControllerAdvice組合使用時就徹底沒有了這個限制。換句話說,兩者的組合達到的全局的異常捕獲處理。

5.png

圖5:註解ControllerAdvice異常處理代碼

在運行以前,需將以前Controller中的ExceptionHandler註釋掉,測試結果以下:

6.png

圖6:註解ControllerAdvice異常處理結果

經過上面結果能夠看到,異常處理確實已經變動爲ExceptionHandlerAdvice類。這種方法將全部的異常處理整合到一處,去除了Controller中的繼承關係,而且達到了全局捕獲的效果,推薦使用此類方式;

2.3實現HandlerExceptionResolver接口

HandlerExceptionResolver自己SpringMVC內部的接口,其內部只有resolveException一個方法,經過實現該接口咱們能夠達到全局異常處理的目的。

7.png

圖7:實現HandlerExceptionResolver接口 一樣在執行以前,將上述兩個方法的異常處理都註釋掉,運行結果以下:

8.png

圖8:實現HandlerExceptionResolver接口運行結果

能夠看到500的異常處理已經生效了,可是400的異常處理卻沒有生效,而且根沒有異常前的返回結果同樣。這是怎麼回事呢?不是說能夠作到全局異常處理的麼?沒辦法要想知道問題的緣由,咱們只能刨根問底,往Spring的祖墳上刨,下面咱們結合Spring的源碼調試,去須要緣由。

3.Spring中異常處理源碼分析

你們都知道,在Spring中第一個收到請求的類就是DispatcherServlet,而該類中核心的方法就是doDispatch,咱們能夠在該類中打斷點,進而一步步跟進異常處理。

3.1 HandlerExceptionResolver實現類處理流程

參照以下的跟進步驟,在processHandlerException中斷點,跟蹤的結果以下圖:

9.png

10.png

圖9:processHandlerException斷點

能夠看到在圖中箭頭【1】處,在遍歷 handlerExceptionResolvers 進而來處理異常,而在箭頭【2】處,看到handlerExceptionResolvers 中共有4個元素,其中最後一個就是2.3方法定義的異常處理類

當前的請求query請求,根據上述現象能夠推測出,該異常處理應該是在前3個異常處理中被處理了,從而跳過咱們自定義的異常;帶着這樣的猜想,咱們F8繼續跟進,能夠跟蹤到該異常是被第三個,即DefaultHandlerExceptionResolver所處理。

  • DefaultHandlerExceptionResolver :SpringMVC默認裝配了DefaultHandlerExceptionResolver,該類的doResolveException方法中主要對一些特殊的異常進行處理,並將這類異常轉換爲相應的響應狀態碼。而query請求觸發的異常爲MissingServletRequestParameterException,其剛好也是被DefaultHandlerExceptionResolver所針對的異常,故會在該類中被異常捕獲。

到此真相大白了,能夠看到咱們的自定義類MyHandlerExceptionResolver確實能夠作到全局處理異常,只不過對於query請求的異常,中間被DefaultHandlerExceptionResolver插了一腳,因此就跳過了MyHandlerExceptionResolver類的處理,從而出現400的返回結果。而對於calc請求,中間沒有阻攔,因此就達到了預期效果。

3.2三類異常的處理順序

到此咱們一共介紹了3類全局異常處理,按照上面的分析能夠看出,實現HandlerExceptionResolver接口的方式是排在最後處理,那麼@ExceptionHandler和@ControllerAdvice這兩個的順序誰先誰後呢? 將三類異常處理所有打開(以前註釋掉了),運行一下看看效果:

11.png

圖10:異常處理全放開運行結果

經過現象能夠看到,Controller中單獨@ExceptionHandle異常處理排在了首位,@ControllerAdvice排在了第二位。嚴謹的童鞋能夠寫個Controller02,將query和calc複製過去,異常處理就不要了,這樣請求c02的方法時,異常捕獲的所屬類名就都是@ControllerAdvice所在類了。

以上都是咱們根據現象獲得的結論,下面去Spring源碼去找「證據」。在圖9中,handlerExceptionResolvers中有4類處理器,而@ExceptionHandler和@ControllerAdvice的處理就在第一個ExceptionHandlerExceptionResolver中(以前斷點跟進便可獲知)。繼續跟進直到進入ExceptionHandlerExceptionResolver類的doResolveHandlerMethodException方法,這裏的HandlerMethod就是Spring將HTTP請求映射到指定Controller中的方法,而Exception就是須要被捕獲的異常;繼續跟進,看看使用這兩個參數到底幹了什麼事兒。

12.png

圖11:doResolveHandlerMethodException斷點

繼續跟進getExceptionHandlerMethod方法,發現有兩個變量可能就是問題的關鍵:exceptionHandlerCache和exceptionHandlerAdviceCache。首先,二者的變量名很值得懷疑;其次,前者在代碼中看,明顯是經過類做爲key,從而獲得一個處理器(resolver),這剛好Controller中@ExceptionHandler處理規則相吻合;最後,這兩個Cache的處理順序,也符合以前的獲得的結論。正如以前猜想的那樣,Spring中確實是優先根據Controller類名去查找對應的ExceptionHandler,沒有找到的話,再進行@ControllerAdvice異常處理。

13.png

圖12:兩個異常處理Cache

若有興趣可繼續深刻挖掘Spring的源碼,這裏針對 ExceptionHandlerExceptionResolver 簡單作個總結:

  • exceptionHandlerCache中包含Controller中的ExceptionHandler異常處理,處理時經過HandlerMethod獲得Controller,進而再找到異常處理方法,須要注意的是,其是在異常處理過程當中put值的;

  • exceptionHandlerAdviceCache則是在項目啓動時初始化的,大概思路是找到帶有@ControllerAdvice註解的bean,從而緩存bean中的ExceptionHandler,在異常處理時須要對齊遍歷查找處理,進而達到全局處理的目的。

3.3鹹魚翻身

介紹了這麼多,簡單畫張圖總結一下。藍色的部分是Spring默認添加的3類異常處理器,黃色部分是咱們添加的異常處理以及其所被調用的位置和順序。看看哪裏還有不太清楚的,往回翻翻看(ResponseStatusExceptionResolver是針對@ResponseStatus註解,這裏再也不詳述)。

14.png

圖13:異常總結

若是有須要將MyHandlerExceptionResolver提早處理,甚至排在ExceptionHandlerExceptionResolver以前,能作到麼?答案是確定的,在Spring中若是想將MyHandlerExceptionResolver異常處理提早,須要再實現一個Ordered接口,實現裏面的getOrder方法便可,這裏返回-1,將其放在最上面,此次鹹魚終於能夠翻身了。

15.png

圖14:實現Ordered接口

運行看一下結果是否是符合預期,提醒一下,咱們三個異常處理都是生效的,以下圖:

16.png

圖15:實現Ordered接口運行結果

4.總結

本文主要經過介紹SpringMVC中三類常見的全局異常處理,在調試中發現了問題,進而引起去Spring源碼中去探究緣由,最終解決問題,但願你們能有所收穫。固然Spring異常處理類不止介紹的這些,有興趣的童鞋請自行探索!

參考連接:

[1] http://www.cnblogs.com/fangjian0423/p/springMVC-request-mapping.html

[2]blog.csdn.net/mll999888/a…

宜信技術學院

相關文章
相關標籤/搜索