前言
在 Web 開發(fā)中, 我們經(jīng)常會(huì)需要處理各種異常, 這是一件棘手的事情, 對(duì)于很多人來說, 可能對(duì)異常處理有以下幾個(gè)問題:
異常處理反例
既然談到異常, 我們先來說一下異常處理的反例, 也是很多人容易犯的錯(cuò)誤, 這里我們同時(shí)講到前端處理和后端處理 :
捕獲異常后只輸出到控制臺(tái)
前端代碼
$.ajax({ type: "GET", url: "/user/add", dataType: "json", success: function(data){ alert("添加成功"); }});
后端代碼
try { // do something} catch (Exception e) { e.printStackTrace();}
這是見過最多的異常處理方式了, 如果這是一個(gè)添加商品的方法, 前臺(tái)通過 ajax 發(fā)送請(qǐng)求到后端, 期望返回 json 信息表示添加結(jié)果. 但如果這段代碼出現(xiàn)了異常:
混亂的返回方式
前端代碼
$.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")@ResponseBodypublic 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;}
這種方式捕獲異常后, 返回了錯(cuò)誤信息, 且前臺(tái)做了一定的處理, 看起來很完善? 但用 HashMap 中的 flag 和 message 這種字符串來當(dāng)鍵很容易處理, 例如你這里叫 message, 別人起名叫 msg, 甚至有時(shí)手抖打錯(cuò)了, 怎么辦? 前臺(tái)再改成 msg 或其他的字符?, 前端后端這樣一直來回改?
更有甚者在情況 A 的情況下, 返回 json, 在情況 B 的情況下, 重定向到某個(gè)頁面, 這就更亂了. 對(duì)于這種不統(tǒng)一的結(jié)構(gòu)處理起來非常麻煩.
異常處理規(guī)范
既然要進(jìn)行統(tǒng)一異常處理, 那么肯定要有一個(gè)規(guī)范, 不能亂來. 這個(gè)規(guī)范包含前端和后端.
不要捕獲任何異常
對(duì)的, 不要在業(yè)務(wù)代碼中進(jìn)行捕獲異常, 即 dao、service、controller 層的所以異常都全部拋出到上層. 這樣不會(huì)導(dǎo)致業(yè)務(wù)代碼中的一堆 try-catch 會(huì)混亂業(yè)務(wù)代碼.
統(tǒng)一返回結(jié)果集
不要使用 Map 來返回結(jié)果, Map 不易控制且容易犯錯(cuò), 應(yīng)該定義一個(gè) Java 實(shí)體類. 來表示統(tǒng)一結(jié)果來返回, 如定義實(shí)體類:
public class ResultBean<T> { private int code; private String message; private Collection<T> 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 <V> ResultBean<V> success(Collection<V> data) { ResultBean resultBean = new ResultBean(); resultBean.setCode(0); resultBean.setMessage("success"); resultBean.setData(data); return resultBean; } // getter / setter 略}
正常情況: 調(diào)用 ResultBean.success() 或 ResultBean.success(Collection<V> data), 不需要返回?cái)?shù)據(jù), 即調(diào)用前者, 需要返回?cái)?shù)據(jù), 調(diào)用后者. 如:
@RequestMapping("/goods/add")@ResponseBodypublic ResultBean<Goods> getAllGoods() { List<Goods> goods = goodsService.findAll(); return ResultBean.success(goods);}
@RequestMapping("/goods/update")@ResponseBodypublic ResultBean updateGoods(Goods goods) { goodsService.update(goods); return ResultBean.success();}
一般只有查詢方法需要調(diào)用 ResultBean.success(Collection<V> data) 來返回 N 條數(shù)據(jù), 其他諸如刪除, 修改等方法都應(yīng)該調(diào)用 ResultBean.success(), 即在業(yè)務(wù)代碼中只處理正確的功能, 不對(duì)異常做任何判斷. 也不需要對(duì) update 或 delete 的更新條數(shù)做判斷(個(gè)人建議, 實(shí)際需要根據(jù)業(yè)務(wù)). 只要沒有拋出異常, 我們就認(rèn)為用戶操作成功了. 且操作成功的提示信息在前端處理, 不要后臺(tái)返回 “操作成功” 等字段.
前臺(tái)接受到的信息為:
{ "code": 0, "message": "success", "data": [ { "name": "商品1", "price": 50.00, }, { "name": "商品2", "price": 99.99, } ]}
拋出異常: 拋出異常后, 我們應(yīng)該調(diào)用 ResultBean.error(int code, String message), 來將狀態(tài)碼和錯(cuò)誤信息返回, 我們約定 code 為 0 表示操作成功, 1 或 2 等正數(shù)表示用戶輸入錯(cuò)誤, -1, -2 等負(fù)數(shù)表示系統(tǒng)錯(cuò)誤.
前臺(tái)接受到的信息為:
{ "code": -1, "message": "XXX 參數(shù)有問題, 請(qǐng)重新填寫", "data": null}
前端統(tǒng)一處理:
返回的結(jié)果集規(guī)范后, 前端就很好處理了:
/** * 顯示錯(cuò)誤信息 * @param result: 錯(cuò)誤信息 */function showError(s) { alert(s);}/** * 處理 ajax 請(qǐng)求結(jié)果 * @param result: ajax 返回的結(jié)果 * @param fn: 成功的處理函數(shù) ( 傳入data: fn(result.data) ) */function handlerResult(result, fn) { // 成功執(zhí)行操作,失敗提示原因 if (result.code == 0) { fn(result.data); } // 用戶操作異常, 這里可以對(duì) 1 或 2 等錯(cuò)誤碼進(jìn)行單獨(dú)處理, 也可以 result.code > 0 來粗粒度的處理, 根據(jù)業(yè)務(wù)而定. else if (result.code == 1) { showError(result.message); } // 系統(tǒng)異常, 這里可以對(duì) -1 或 -2 等錯(cuò)誤碼進(jìn)行單獨(dú)處理, 也可以 result.code > 0 來粗粒度的處理, 根據(jù)業(yè)務(wù)而定. else if (result.code == -1) { showError(result.message); } // 如果進(jìn)行細(xì)粒度的狀態(tài)碼判斷, 那么就應(yīng)該重點(diǎn)注意這里沒出現(xiàn)過的狀態(tài)碼. 這個(gè)判斷僅建議在開發(fā)階段保留用來發(fā)現(xiàn)未定義的狀態(tài)碼. else { showError("出現(xiàn)未定義的狀態(tài)碼:" + result.code); }}/** * 根據(jù) id 刪除商品 */function deleteGoods(id) { $.ajax({ type: "GET", url: "/goods/delete", dataType: "json", success: function(result){ handlerResult(result, deleteDone); } });}function deleteDone(data) { alert("刪除成功");}
showError
和 handlerResult
是公共方法, 分別用來顯示錯(cuò)誤和統(tǒng)一處理結(jié)果集.
然后將主要精力放在發(fā)送請(qǐng)求和處理正確結(jié)果的方法上即可, 如這里的 deleteDone 函數(shù), 用來處理操作成功給用戶的提示信息, 正所謂各司其職, 前端負(fù)責(zé)操作成功的消息提示更合理, 而錯(cuò)誤信息只有后臺(tái)知道, 所以需要后臺(tái)來返回.
后端統(tǒng)一處理異常
說了這么多, 還沒講到后端不在業(yè)務(wù)層捕獲任何異常的事, 既然所有業(yè)務(wù)層都沒有捕獲異常, 那么所有的異常都會(huì)拋出到 Controller 層, 我們只需要用 AOP 對(duì) Controller 層的所有方法處理即可.
好在 Spring 為我們提供了一個(gè)注解, 用來統(tǒng)一處理異常:
@ControllerAdvice@ResponseBodypublic class WebExceptionHandler { private static final Logger log = LoggerFactory.getLogger(WebExceptionHandler.class); @ExceptionHandler public ResultBean unknownAccount(UnknownAccountException e) { log.error("賬號(hào)不存在", e); return ResultBean.error(1, "賬號(hào)不存在"); } @ExceptionHandler public ResultBean incorrectCredentials(IncorrectCredentialsException e) { log.error("密碼錯(cuò)誤", e); return ResultBean.error(-2, "密碼錯(cuò)誤"); } @ExceptionHandler public ResultBean unknownException(Exception e) { log.error("發(fā)生了未知異常", e); // 發(fā)送郵件通知技術(shù)人員. return ResultBean.error(-99, "系統(tǒng)出現(xiàn)錯(cuò)誤, 請(qǐng)聯(lián)系網(wǎng)站管理員!"); }}
在這里統(tǒng)一配置需要處理的異常, 同樣, 對(duì)于未知的異常, 一定要及時(shí)發(fā)現(xiàn), 并進(jìn)行處理. 推薦出現(xiàn)未知異常后發(fā)送郵件, 提示技術(shù)人員.
總結(jié)
總結(jié)一下統(tǒng)一異常處理的方法:
一個(gè)簡單的演示項(xiàng)目: https://github.com/zhaojun1998/exception-handler-demo
以上就是本文的全部內(nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持VeVb武林網(wǎng)。
新聞熱點(diǎn)
疑難解答
圖片精選