1.前言
相信我們每個人在SpringMVC開發中,都遇到這樣的問題:當我們的程式碼正常執行時,返回的資料是我們預期格式,比如json或xml形式,但是一旦出現了異常(比如:NPE或者陣列越界等等),返回的內容確實服務端的異常堆疊資訊,從而導致返回的資料不能使客戶端正常解析; 很顯然,這些並不是我們希望的結果。
我們知道,一個較為常見的系統,會涉及控制層,服務(業務)層、快取層、儲存層以及介面呼叫等,其中每一個環節都不可避免的會遇到各種不可預知的異常需要處理。如果每個步驟都單獨try..catch會使系統顯的很雜亂,可讀性差,維護成本高;常見的方式就是,實現統一的異常處理,從而將各類異常從各個模組中解耦出來;
2.常見全域性異常處理
在Spring中常見的全域性異常處理,主要有三種:
(1)註解ExceptionHandler
(2)繼承HandlerExceptionResolver介面
(3)註解ControllerAdvice
在後面的講解中,主要以HTTP錯誤碼:400(請求無效)和500(內部伺服器錯誤)為例,先看一下測試程式碼以及沒有任何處理的返回結果,如下:
圖1:測試程式碼
圖2:沒有異常的錯誤返回
2.1註解ExceptionHandler
註解ExceptionHandler作用物件為方法,最簡單的使用方法就是放在controller檔案中,詳細的註解定義不再介紹。如果專案中有多個controller檔案,通常可以在baseController中實現ExceptionHandler的異常處理,而各個contoller繼承basecontroller從而達到統一異常處理的目的。因為比較常見,簡單程式碼如下:
圖3:Controller中的ExceptionHandler使用
在返回異常時,新增了所屬的類名,便於大家記憶理解。執行看一下結果:
圖4:新增ExceptionHandler之後的結果
優點:ExceptionHandler簡單易懂,並且對於異常處理沒有限定方法格式;
缺點:由於ExceptionHandler僅作用於方法,對於多個controller的情況,僅為了一個方法,所有需要異常處理的controller都繼承這個類,明明不相關的東西,強行給他們找個爹,不太好。
2.2註解ControllerAdvice
這裡雖說是ControllerAdvice註解,其實是其與ExceptionHandler的組合使用。在上文中可以看到,單獨使用@ExceptionHandler時,其必須在一個Controller中,然而當其與ControllerAdvice組合使用時就完全沒有了這個限制。換句話說,二者的組合達到的全域性的異常捕獲處理。
圖5:註解ControllerAdvice異常處理程式碼
在執行之前,需將之前Controller中的ExceptionHandler註釋掉,測試結果如下:
圖6:註解ControllerAdvice異常處理結果
通過上面結果可以看到,異常處理確實已經變更為ExceptionHandlerAdvice類。這種方法將所有的異常處理整合到一處,去除了Controller中的繼承關係,並且達到了全域性捕獲的效果,推薦使用此類方式;
2.3實現HandlerExceptionResolver介面
HandlerExceptionResolver本身SpringMVC內部的介面,其內部只有resolveException一個方法,通過實現該介面我們可以達到全域性異常處理的目的。
圖7:實現HandlerExceptionResolver介面 同樣在執行之前,將上述兩個方法的異常處理都註釋掉,執行結果如下:
圖8:實現HandlerExceptionResolver介面執行結果
可以看到500的異常處理已經生效了,但是400的異常處理卻沒有生效,並且根沒有異常前的返回結果一樣。這是怎麼回事呢?不是說可以做到全域性異常處理的麼?沒辦法要想知道問題的原因,我們只能刨根問底,往Spring的祖墳上刨,下面我們結合Spring的原始碼除錯,去需要原因。
3.Spring中異常處理原始碼分析
大家都知道,在Spring中第一個收到請求的類就是DispatcherServlet,而該類中核心的方法就是doDispatch,我們可以在該類中打斷點,進而一步步跟進異常處理。
3.1 HandlerExceptionResolver實現類處理流程
參照如下的跟進步驟,在processHandlerException中斷點,跟蹤的結果如下圖:
圖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這兩個的順序誰先誰後呢? 將三類異常處理全部開啟(之前註釋掉了),執行一下看看效果:
圖10:異常處理全放開執行結果
通過現象可以看到,Controller中單獨@ExceptionHandle異常處理排在了首位,@ControllerAdvice排在了第二位。嚴謹的童鞋可以寫個Controller02,將query和calc複製過去,異常處理就不要了,這樣請求c02的方法時,異常捕獲的所屬類名就都是@ControllerAdvice所在類了。
以上都是我們根據現象得到的結論,下面去Spring原始碼去找“證據”。在圖9中,handlerExceptionResolvers中有4類處理器,而@ExceptionHandler和@ControllerAdvice的處理就在第一個ExceptionHandlerExceptionResolver中(之前斷點跟進即可獲知)。繼續跟進直到進入ExceptionHandlerExceptionResolver類的doResolveHandlerMethodException方法,這裡的HandlerMethod就是Spring將HTTP請求對映到指定Controller中的方法,而Exception就是需要被捕獲的異常;繼續跟進,看看使用這兩個引數到底幹了什麼事兒。
圖11:doResolveHandlerMethodException斷點
繼續跟進getExceptionHandlerMethod方法,發現有兩個變數可能就是問題的關鍵:exceptionHandlerCache和exceptionHandlerAdviceCache。首先,兩者的變數名很值得懷疑;其次,前者在程式碼中看,明顯是通過類作為key,從而得到一個處理器(resolver),這恰好Controller中@ExceptionHandler處理規則相吻合;最後,這兩個Cache的處理順序,也符合之前的得到的結論。正如之前猜測的那樣,Spring中確實是優先根據Controller類名去查詢對應的ExceptionHandler,沒有找到的話,再進行@ControllerAdvice異常處理。
圖12:兩個異常處理Cache
如有興趣可繼續深入挖掘Spring的原始碼,這裡針對 ExceptionHandlerExceptionResolver 簡單做個總結:
exceptionHandlerCache中包含Controller中的ExceptionHandler異常處理,處理時通過HandlerMethod得到Controller,進而再找到異常處理方法,需要注意的是,其是在異常處理過程中put值的;
exceptionHandlerAdviceCache則是在專案啟動時初始化的,大概思路是找到帶有@ControllerAdvice註解的bean,從而快取bean中的ExceptionHandler,在異常處理時需要對齊遍歷查詢處理,進而達到全域性處理的目的。
3.3鹹魚翻身
介紹了這麼多,簡單畫張圖總結一下。藍色的部分是Spring預設新增的3類異常處理器,黃色部分是我們新增的異常處理以及其所被呼叫的位置和順序。看看哪裡還有不太清楚的,往回翻翻看(ResponseStatusExceptionResolver是針對@ResponseStatus註解,這裡不再詳述)。
圖13:異常總結
如果有需要將MyHandlerExceptionResolver提前處理,甚至排在ExceptionHandlerExceptionResolver之前,能做到麼?答案是肯定的,在Spring中如果想將MyHandlerExceptionResolver異常處理提前,需要再實現一個Ordered介面,實現裡面的getOrder方法即可,這裡返回-1,將其放在最上面,這次鹹魚終於可以翻身了。
圖14:實現Ordered介面
執行看一下結果是不是符合預期,提醒一下,我們三個異常處理都是生效的,如下圖:
圖15:實現Ordered介面執行結果
4.總結
本文主要通過介紹SpringMVC中三類常見的全域性異常處理,在除錯中發現了問題,進而引發去Spring原始碼中去探究原因,最終解決問題,希望大家能有所收穫。當然Spring異常處理類不止介紹的這些,有興趣的童鞋請自行探索!
參考連結:
[1] http://www.cnblogs.com/fangjian0423/p/springMVC-request-mapping.html