摘要: SpringBoot異常處理。
原文:Spring MVC/Boot 統一異常處理最佳實踐
作者:趙俊
前言在 Web 開發中, 我們經常會需要處理各種異常, 這是一件棘手的事情, 對于很多人來說, 可能對異常處理有以下幾個問題:
什么時候需要捕獲(try-catch)異常, 什么時候需要拋出(throws)異常到上層.
在 dao 層捕獲還是在 service 捕獲, 還是在 controller 層捕獲.
拋出異常后要怎么處理. 怎么返回給頁面錯誤信息.
異常處理反例既然談到異常, 我們先來說一下異常處理的反例, 也是很多人容易犯的錯誤, 這里我們同時講到前端處理和后端處理 :
捕獲異常后只輸出到控制臺前端代碼
$.ajax({ type: "GET", url: "/user/add", dataType: "json", success: function(data){ alert("添加成功"); } });
后端代碼
try { // do something } catch (Exception e) { e.printStackTrace(); }
這是見過最多的異常處理方式了, 如果這是一個添加商品的方法, 前臺通過 ajax 發送請求到后端, 期望返回 json 信息表示添加結果. 但如果這段代碼出現了異常:
那么用戶看到的場景就是點擊了添加按鈕, 但沒有任何反應(其實是返回了 500 錯誤頁面, 但這里前端沒有監聽 error 事件, 只監聽了 success 事件. 但即使加上了error: function(data) {alert("添加失敗");}) 又如何呢? 到底因為啥失敗了呢, 用戶也不得而知.
后臺 e.printStackTrace() 打印在控制臺的日志也會在漫漫的日志中被埋沒, 很可能會看不到輸出的異常. 但這并不是最糟的情況, 更糟糕的事情是連 e.printStackTrace() 都沒有, catch 塊中是空的, 這樣后端的控制臺中更是什么都看不到了, 這段代碼會像一個隱形的炸彈一樣一直埋伏在系統中.
混亂的返回方式前端代碼
$.ajax({ type: "GET", url: "/goods/add", dataType: "json", success: function(data) { if (data.flag) { alert("添加成功"); } else { alert(data.message); } }, error: function(data){ alert("添加失敗"); } });
后端代碼
@RequestMapping("/goods/add") @ResponseBody public Map add(Goods goods) { Map map = new HashMap(); try { // do something map.put(flag, true); } catch (Exception e) { e.printStackTrace(); map.put("flag", false); map.put("message", e.getMessage()); } reutrn map; }
這種方式捕獲異常后, 返回了錯誤信息, 且前臺做了一定的處理, 看起來很完善? 但用 HashMap 中的 flag 和 message 這種字符串來當鍵很容易處理, 例如你這里叫 message, 別人起名叫 msg, 甚至有時手抖打錯了, 怎么辦? 前臺再改成 msg 或其他的字符?, 前端后端這樣一直來回改?
更有甚者在情況 A 的情況下, 返回 json, 在情況 B 的情況下, 重定向到某個頁面, 這就更亂了. 對于這種不統一的結構處理起來非常麻煩.
異常處理規范既然要進行統一異常處理, 那么肯定要有一個規范, 不能亂來. 這個規范包含前端和后端.
不要捕獲任何異常對的, 不要在業務代碼中進行捕獲異常, 即 dao、service、controller 層的所以異常都全部拋出到上層. 這樣不會導致業務代碼中的一堆 try-catch 會混亂業務代碼.
統一返回結果集不要使用 Map 來返回結果, Map 不易控制且容易犯錯, 應該定義一個 Java 實體類. 來表示統一結果來返回, 如定義實體類:
public class ResultBean{ private int code; private String message; private Collection data; private ResultBean() { } public static ResultBean error(int code, String message) { ResultBean resultBean = new ResultBean(); resultBean.setCode(code); resultBean.setMessage(message); return resultBean; } public static ResultBean success() { ResultBean resultBean = new ResultBean(); resultBean.setCode(0); resultBean.setMessage("success"); return resultBean; } public static ResultBean success(Collection data) { ResultBean resultBean = new ResultBean(); resultBean.setCode(0); resultBean.setMessage("success"); resultBean.setData(data); return resultBean; } // getter / setter 略 }
正常情況: 調用 ResultBean.success() 或 ResultBean.success(Collection
@RequestMapping("/goods/add") @ResponseBody public ResultBeangetAllGoods() { List goods = goodsService.findAll(); return ResultBean.success(goods); } @RequestMapping("/goods/update") @ResponseBody public ResultBean updateGoods(Goods goods) { goodsService.update(goods); return ResultBean.success(); }
一般只有查詢方法需要調用 ResultBean.success(Collection
前臺接受到的信息為:
{ "code": 0, "message": "success", "data": [ { "name": "商品1", "price": 50.00, }, { "name": "商品2", "price": 99.99, } ] }
拋出異常: 拋出異常后, 我們應該調用 ResultBean.error(int code, String message), 來將狀態碼和錯誤信息返回, 我們約定 code 為 0 表示操作成功, 1 或 2 等正數表示用戶輸入錯誤, -1, -2 等負數表示系統錯誤.
前臺接受到的信息為:
{ "code": -1, "message": "XXX 參數有問題, 請重新填寫", "data": null }前端統一處理:
返回的結果集規范后, 前端就很好處理了:
/** * 顯示錯誤信息 * @param result: 錯誤信息 */ function showError(s) { alert(s); } /** * 處理 ajax 請求結果 * @param result: ajax 返回的結果 * @param fn: 成功的處理函數 ( 傳入data: fn(result.data) ) */ function handlerResult(result, fn) { // 成功執行操作,失敗提示原因 if (result.code == 0) { fn(result.data); } // 用戶操作異常, 這里可以對 1 或 2 等錯誤碼進行多帶帶處理, 也可以 result.code > 0 來粗粒度的處理, 根據業務而定. else if (result.code == 1) { showError(result.message); } // 系統異常, 這里可以對 -1 或 -2 等錯誤碼進行多帶帶處理, 也可以 result.code > 0 來粗粒度的處理, 根據業務而定. else if (result.code == -1) { showError(result.message); } // 如果進行細粒度的狀態碼判斷, 那么就應該重點注意這里沒出現過的狀態碼. 這個判斷僅建議在開發階段保留用來發現未定義的狀態碼. else { showError("出現未定義的狀態碼:" + result.code); } } /** * 根據 id 刪除商品 */ function deleteGoods(id) { $.ajax({ type: "GET", url: "/goods/delete", dataType: "json", success: function(result){ handlerResult(result, deleteDone); } }); } function deleteDone(data) { alert("刪除成功"); }
showError 和 handlerResult 是公共方法, 分別用來顯示錯誤和統一處理結果集.
然后將主要精力放在發送請求和處理正確結果的方法上即可, 如這里的 deleteDone 函數, 用來處理操作成功給用戶的提示信息, 正所謂各司其職, 前端負責操作成功的消息提示更合理, 而錯誤信息只有后臺知道, 所以需要后臺來返回.
后端統一處理異常說了這么多, 還沒講到后端不在業務層捕獲任何異常的事, 既然所有業務層都沒有捕獲異常, 那么所有的異常都會拋出到 Controller 層, 我們只需要用 AOP 對 Controller 層的所有方法處理即可.
好在 Spring 為我們提供了一個注解, 用來統一處理異常:
@ControllerAdvice @ResponseBody public class WebExceptionHandler { private static final Logger log = LoggerFactory.getLogger(WebExceptionHandler.class); @ExceptionHandler public ResultBean unknownAccount(UnknownAccountException e) { log.error("賬號不存在", e); return ResultBean.error(1, "賬號不存在"); } @ExceptionHandler public ResultBean incorrectCredentials(IncorrectCredentialsException e) { log.error("密碼錯誤", e); return ResultBean.error(-2, "密碼錯誤"); } @ExceptionHandler public ResultBean unknownException(Exception e) { log.error("發生了未知異常", e); // 發送郵件通知技術人員. return ResultBean.error(-99, "系統出現錯誤, 請聯系網站管理員!"); } }
在這里統一配置需要處理的異常, 同樣, 對于未知的異常, 一定要及時發現, 并進行處理. 推薦出現未知異常后發送郵件, 提示技術人員.
總結總結一下統一異常處理的方法:
不使用隨意返回各種數據類型, 要統一返回值規范.
不在業務代碼中捕獲任何異常, 全部交由 @ControllerAdvice 來處理.
一個簡單的演示項目: https://github.com/zhaojun1998/exception-handler-demo
本文作者: 趙俊
本文鏈接: http://www.zhaojun.im/springboot-exception/
版權聲明: 本博客所有文章除特別聲明外,均采用BY-NC-SA許可協議。轉載請注明出處!
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/73040.html
摘要:定制特定異常返回結果根據官方文檔的例子,可以使用和對特定異常返回特定的結果。下面是用瀏覽器和訪問的結果無輸出注意上方表格的錯誤,產生這個的原因前面已經講過。不過需要注意的是,無法通過設定,由或者容器決定里一律是。 github:https://github.com/chanjarste... 參考文檔: Spring Boot 1.5.4.RELEASE Documentation ...
摘要:挺多人咨詢的,異常處理用切面注解去實現去全局異常處理。全局異常處理類,代碼如下代碼解析如下抽象類是用來處理全局錯誤時進行擴展和實現注解標記的切面排序,值越小擁有越高的優先級,這里設置優先級偏高。 本文內容 為什么要全局異常處理? WebFlux REST 全局異常處理實戰 小結 摘錄:只有不斷培養好習慣,同時不斷打破壞習慣,我們的行為舉止才能夠自始至終都是正確的。 一、為什么要全局...
摘要:對的配置和行為進行定制修改匹配路由請求規則注冊自定義的和添加靜態資源處理器添加自定義視圖控制器添加自定義方法參數處理器配置消息轉換器清空所有轉換器做一個好人。博客園掘金簡書頭條知乎 一個大的系統,在代碼的復用肯定是必不可少的,它能解決: 統一的響應處理(可以對外提供統一的響應對象包裝) showImg(https://segmentfault.com/img/remote/146000...
摘要:來源是最流行的用于開發微服務的框架。以下依次列出了最佳實踐,排名不分先后。這非常有助于避免可怕的地獄。推薦使用構造函數注入這一條實踐來自的項目負責人。保持業務邏輯免受代碼侵入的一種方法是使用構造函數注入。 showImg(https://mmbiz.qpic.cn/mmbiz_jpg/R3InYSAIZkHQ40ly9Oztiart2lESCyjCH0JwFRp3oErlYobhibM...
摘要:前言最近在優化自己之前基于的統一響應體的實現方案。但是的狀態碼數量有限,而隨著業務的增長,狀態碼無法很好地表示業務中遇到的異常情況。 前言 最近在優化自己之前基于Spring AOP的統一響應體的實現方案。 什么是統一響應體呢?在目前的前后端分離架構下,后端主要是一個RESTful API的數據接口。 但是HTTP的狀態碼數量有限,而隨著業務的增長,HTTP狀態碼無法很好地表示業務中遇...
閱讀 702·2021-09-29 09:34
閱讀 2554·2019-08-30 15:53
閱讀 3361·2019-08-29 17:17
閱讀 761·2019-08-29 16:08
閱讀 1120·2019-08-29 13:03
閱讀 951·2019-08-27 10:54
閱讀 688·2019-08-26 13:39
閱讀 2859·2019-08-26 13:34