前言
在 Web 開發中, 我們經常會需要處理(lǐ)各種異常, 這(zhè)是一件棘手的(de)事情, 對(duì)于很多(duō)人(rén)來(lái)說, 可(kě)能對(duì)異常處理(lǐ)有以下(xià)幾個(gè)問題:
什(shén)麽時(shí)候需要捕獲(try-catch)異常, 什(shén)麽時(shí)候需要抛出(throws)異常到上層.
在 dao 層捕獲還(hái)是在 service 捕獲, 還(hái)是在 controller 層捕獲.
抛出異常後要怎麽處理(lǐ). 怎麽返回給頁面錯誤信息.
異常處理(lǐ)反例
既然談到異常, 我們先來(lái)說一下(xià)異常處理(lǐ)的(de)反例, 也(yě)是很多(duō)人(rén)容易犯的(de)錯誤, 這(zhè)裏我們同時(shí)講到前端處理(lǐ)和(hé)後端處理(lǐ) :
捕獲異常後隻輸出到控制台
前端代碼
$.ajax({
type: "GET",
url: "/user/add",
dataType: "json",
success: function(data){
alert("添加成功");
}
});
後端代碼
try {
// do something
} catch (Exception e) {
e.printStackTrace();
}
這(zhè)是見過最多(duō)的(de)異常處理(lǐ)方式了(le), 如果這(zhè)是一個(gè)添加商品的(de)方法, 前台通(tōng)過 ajax 發送請求到後端, 期望返回 json 信息表示添加結果. 但如果這(zhè)段代碼出現了(le)異常:
那麽用(yòng)戶看到的(de)場(chǎng)景就是點擊了(le)添加按鈕, 但沒有任何反應(其實是返回了(le) 500 錯誤頁面, 但這(zhè)裏前端沒有監聽(tīng) error 事件, 隻監聽(tīng)了(le) success 事件. 但即使加上了(le)error: function(data) {alert("添加失敗");}) 又如何呢(ne)? 到底因爲啥失敗了(le)呢(ne), 用(yòng)戶也(yě)不得(de)而知.
後台 e.printStackTrace() 打印在控制台的(de)日志也(yě)會在漫漫的(de)日志中被埋沒, 很可(kě)能會看不到輸出的(de)異常. 但這(zhè)并不是最糟的(de)情況, 更糟糕的(de)事情是連 e.printStackTrace() 都沒有, catch 塊中是空的(de), 這(zhè)樣後端的(de)控制台中更是什(shén)麽都看不到了(le), 這(zhè)段代碼會像一個(gè)隐形的(de)炸彈一樣一直埋伏在系統中.
混亂的(de)返回方式
前端代碼
$.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;
}
這(zhè)種方式捕獲異常後, 返回了(le)錯誤信息, 且前台做(zuò)了(le)一定的(de)處理(lǐ), 看起來(lái)很完善? 但用(yòng) HashMap 中的(de) flag 和(hé) message 這(zhè)種字符串來(lái)當鍵很容易處理(lǐ), 例如你這(zhè)裏叫 message, 别人(rén)起名叫 msg, 甚至有時(shí)手抖打錯了(le), 怎麽辦? 前台再改成 msg 或其他(tā)的(de)字符?, 前端後端這(zhè)樣一直來(lái)回改?
更有甚者在情況 A 的(de)情況下(xià), 返回 json, 在情況 B 的(de)情況下(xià), 重定向到某個(gè)頁面, 這(zhè)就更亂了(le). 對(duì)于這(zhè)種不統一的(de)結構處理(lǐ)起來(lái)非常麻煩.
異常處理(lǐ)規範
既然要進行統一異常處理(lǐ), 那麽肯定要有一個(gè)規範, 不能亂來(lái). 這(zhè)個(gè)規範包含前端和(hé)後端.
不要捕獲任何異常
對(duì)的(de), 不要在業務代碼中進行捕獲異常, 即 dao、service、controller 層的(de)所以異常都全部抛出到上層. 這(zhè)樣不會導緻業務代碼中的(de)一堆 try-catch 會混亂業務代碼.
統一返回結果集
不要使用(yòng) Map 來(lái)返回結果, Map 不易控制且容易犯錯, 應該定義一個(gè) Java 實體類. 來(lái)表示統一結果來(lái)返回, 如定義實體類:
public class ResultBean
@RequestMapping("/goods/add")
@ResponseBody
public ResultBean
一般隻有查詢方法需要調用(yòng) ResultBean.success(Collection
前台接受到的(de)信息爲:
{
"code": 0,
"message": "success",
"data": [
{
"name": "商品1",
"price": 50.00,
},
{
"name": "商品2",
"price": 99.99,
}
]
}
抛出異常: 抛出異常後, 我們應該調用(yòng) ResultBean.error(int code, String message), 來(lái)将狀态碼和(hé)錯誤信息返回, 我們約定 code 爲 0 表示操作成功, 1 或 2 等正數表示用(yòng)戶輸入錯誤, -1, -2 等負數表示系統錯誤.
前台接受到的(de)信息爲:
{
"code": -1,
"message": "XXX 參數有問題, 請重新填寫",
"data": null
}
前端統一處理(lǐ):
返回的(de)結果集規範後, 前端就很好處理(lǐ)了(le):
/**
* 顯示錯誤信息
* @param result: 錯誤信息
*/
function showError(s) {
alert(s);
}
/**
* 處理(lǐ) ajax 請求結果
* @param result: ajax 返回的(de)結果
* @param fn: 成功的(de)處理(lǐ)函數 ( 傳入data: fn(result.data) )
*/
function handlerResult(result, fn) {
// 成功執行操作,失敗提示原因
if (result.code == 0) {
fn(result.data);
}
// 用(yòng)戶操作異常, 這(zhè)裏可(kě)以對(duì) 1 或 2 等錯誤碼進行單獨處理(lǐ), 也(yě)可(kě)以 result.code > 0 來(lái)粗粒度的(de)處理(lǐ), 根據業務而定.
else if (result.code == 1) {
showError(result.message);
}
// 系統異常, 這(zhè)裏可(kě)以對(duì) -1 或 -2 等錯誤碼進行單獨處理(lǐ), 也(yě)可(kě)以 result.code > 0 來(lái)粗粒度的(de)處理(lǐ), 根據業務而定.
else if (result.code == -1) {
showError(result.message);
}
// 如果進行細粒度的(de)狀态碼判斷, 那麽就應該重點注意這(zhè)裏沒出現過的(de)狀态碼. 這(zhè)個(gè)判斷僅建議(yì)在開發階段保留用(yòng)來(lái)發現未定義的(de)狀态碼.
else {
showError("出現未定義的(de)狀态碼:" + result.code);
}
}
/**
* 根據 id 删除商品
*/
function deleteGoods(id) {
$.ajax({
type: "DELETE",
url: "/goods/delete",
dataType: "json",
success: function(result){
handlerResult(result, deleteDone);
}
});
}
function deleteDone(data) {
alert("删除成功");
}
showError 和(hé) handlerResult 是公共方法, 分(fēn)别用(yòng)來(lái)顯示錯誤和(hé)統一處理(lǐ)結果集.
然後将主要精力放在發送請求和(hé)處理(lǐ)正确結果的(de)方法上即可(kě), 如這(zhè)裏的(de) deleteDone 函數, 用(yòng)來(lái)處理(lǐ)操作成功給用(yòng)戶的(de)提示信息, 正所謂各司其職, 前端負責操作成功的(de)消息提示更合理(lǐ), 而錯誤信息隻有後台知道, 所以需要後台來(lái)返回.
後端統一處理(lǐ)異常
說了(le)這(zhè)麽多(duō), 還(hái)沒講到後端不在業務層捕獲任何異常的(de)事, 既然所有業務層都沒有捕獲異常, 那麽所有的(de)異常都會抛出到 Controller 層, 我們隻需要用(yòng) AOP 對(duì) Controller 層的(de)所有方法處理(lǐ)即可(kě).
好在 Spring 爲我們提供了(le)一個(gè)注解, 用(yòng)來(lái)統一處理(lǐ)異常:
@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("發生了(le)未知異常", e);
// 發送郵件通(tōng)知技術人(rén)員(yuán).
return ResultBean.error(-99, "系統出現錯誤, 請聯系網站管理(lǐ)員(yuán)!");
}
}
在這(zhè)裏統一配置需要處理(lǐ)的(de)異常, 同樣, 對(duì)于未知的(de)異常, 一定要及時(shí)發現, 并進行處理(lǐ). 推薦出現未知異常後發送郵件, 提示技術人(rén)員(yuán).
總結
總結一下(xià)統一異常處理(lǐ)的(de)方法:
不使用(yòng)随意返回各種數據類型, 要統一返回值規範.
不在業務代碼中捕獲任何異常, 全部交由 @ControllerAdvice 來(lái)處理(lǐ).