優(yōu)雅的全局異常處理方式

在?Web?開發(fā)中, 我們經(jīng)常會需要處理各種異常, 這是一件棘手的事情, 對于很多人來說, 可能對異常處理有以下幾個問題:

什么時候需要捕獲(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 發(fā)送請求到后端, 期望返回 json 信息表示添加結(jié)果. 但如果這段代碼出現(xiàn)了異常:

那么用戶看到的場景就是點擊了添加按鈕, 但沒有任何反應(yīng)(其實是返回了 500 錯誤頁面, 但這里前端沒有監(jiān)聽 error 事件, 只監(jiān)聽了 success 事件. 但即使加上了error: function(data) {alert("添加失敗");}) 又如何呢? 到底因為啥失敗了呢, 用戶也不得而知.

后臺?e.printStackTrace()?打印在控制臺的日志也會在漫漫的日志中被埋沒, 很可能會看不到輸出的異常. 但這并不是最糟的情況, 更糟糕的事情是連?e.printStackTrace()?都沒有,?catch?塊中是空的, 這樣后端的控制臺中更是什么都看不到了, 這段代碼會像一個隱形的炸彈一樣一直埋伏在系統(tǒng)中.

混亂的返回方式

前端代碼

$.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

publicMapadd(Goods goods){

Map map =newHashMap();

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?這種字符串來當(dāng)鍵很容易處理, 例如你這里叫?message, 別人起名叫?msg, 甚至有時手抖打錯了, 怎么辦? 前臺再改成?msg?或其他的字符?, 前端后端這樣一直來回改?

更有甚者在情況 A 的情況下, 返回 json, 在情況 B 的情況下, 重定向到某個頁面, 這就更亂了. 對于這種不統(tǒng)一的結(jié)構(gòu)處理起來非常麻煩.

異常處理規(guī)范

既然要進(jìn)行統(tǒng)一異常處理, 那么肯定要有一個規(guī)范, 不能亂來. 這個規(guī)范包含前端和后端.

不要捕獲任何異常

對的, 不要在業(yè)務(wù)代碼中進(jìn)行捕獲異常, 即 dao暴凑、service臭墨、controller 層的所以異常都全部拋出到上層. 這樣不會導(dǎo)致業(yè)務(wù)代碼中的一堆?try-catch?會混亂業(yè)務(wù)代碼.

統(tǒng)一返回結(jié)果集

不要使用 Map 來返回結(jié)果, Map 不易控制且容易犯錯, 應(yīng)該定義一個 Java 實體類. 來表示統(tǒng)一結(jié)果來返回, 如定義實體類:

publicclassResultBean{

privateintcode;

privateString message;

privateCollection data;

privateResultBean(){

? ? }

publicstaticResultBeanerror(intcode, String message){

ResultBean resultBean =newResultBean();

? ? ? ? resultBean.setCode(code);

? ? ? ? resultBean.setMessage(message);

returnresultBean;

? ? }

publicstaticResultBeansuccess(){

ResultBean resultBean =newResultBean();

resultBean.setCode(0);

resultBean.setMessage("success");

returnresultBean;

? ? }

publicstaticResultBeansuccess(Collection<V> data){

ResultBean resultBean =newResultBean();

resultBean.setCode(0);

resultBean.setMessage("success");

? ? ? ? resultBean.setData(data);

returnresultBean;

? ? }

// getter / setter 略

}

正常情況: 調(diào)用?ResultBean.success()?或?ResultBean.success(Collection<V> data), 不需要返回數(shù)據(jù), 即調(diào)用前者, 需要返回數(shù)據(jù), 調(diào)用后者. 如:

@RequestMapping("/goods/add")

@ResponseBody

publicResultBeangetAllGoods(){

? ? List<Goods> goods = goodsService.findAll();

returnResultBean.success(goods);

}

@RequestMapping("/goods/update")

@ResponseBody

publicResultBeanupdateGoods(Goods goods){

? ? goodsService.update(goods);

returnResultBean.success();

}

一般只有查詢方法需要調(diào)用?ResultBean.success(Collection<V> data)?來返回 N 條數(shù)據(jù), 其他諸如刪除, 修改等方法都應(yīng)該調(diào)用?ResultBean.success(), 即在業(yè)務(wù)代碼中只處理正確的功能, 不對異常做任何判斷. 也不需要對 update 或 delete 的更新條數(shù)做判斷(個人建議, 實際需要根據(jù)業(yè)務(wù)). 只要沒有拋出異常, 我們就認(rèn)為用戶操作成功了. 且操作成功的提示信息在前端處理, 不要后臺返回 “操作成功” 等字段.

前臺接受到的信息為:

{

"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)碼和錯誤信息返回, 我們約定?code?為 0 表示操作成功,?1?或?2?等正數(shù)表示用戶輸入錯誤,?-1,?-2?等負(fù)數(shù)表示系統(tǒng)錯誤.

前臺接受到的信息為:

{

"code":-1,

"message":"XXX 參數(shù)有問題, 請重新填寫",

"data":null

}

前端統(tǒng)一處理:

返回的結(jié)果集規(guī)范后, 前端就很好處理了:

/**

* 顯示錯誤信息

*@param result: 錯誤信息

*/

functionshowError(s){

? ? alert(s);

}

/**

* 處理 ajax 請求結(jié)果

*@param result: ajax 返回的結(jié)果

*@param fn: 成功的處理函數(shù) ( 傳入data: fn(result.data) )

*/

functionhandlerResult(result, fn){

// 成功執(zhí)行操作,失敗提示原因

if(result.code ==0) {

? ? ? ? fn(result.data);

? ? }

// 用戶操作異常, 這里可以對 1 或 2 等錯誤碼進(jìn)行單獨處理, 也可以 result.code > 0 來粗粒度的處理, 根據(jù)業(yè)務(wù)而定.

elseif(result.code ==1) {

? ? ? ? showError(result.message);

? ? }

// 系統(tǒng)異常, 這里可以對 -1 或 -2 等錯誤碼進(jìn)行單獨處理, 也可以 result.code > 0 來粗粒度的處理, 根據(jù)業(yè)務(wù)而定.

elseif(result.code ==-1) {

? ? ? ? showError(result.message);

? ? }

// 如果進(jìn)行細(xì)粒度的狀態(tài)碼判斷, 那么就應(yīng)該重點注意這里沒出現(xiàn)過的狀態(tài)碼. 這個判斷僅建議在開發(fā)階段保留用來發(fā)現(xiàn)未定義的狀態(tài)碼.

else{

showError("出現(xiàn)未定義的狀態(tài)碼:"+ result.code);

? ? }

}

/**

* 根據(jù) id 刪除商品

*/

functiondeleteGoods(id){

? ? $.ajax({

type:"GET",

url:"/goods/delete",

dataType:"json",

success:function(result){

? ? ? ? ? ? handlerResult(result, deleteDone);

? ? ? ? }

? ? });

}

functiondeleteDone(data){

alert("刪除成功");

}

showError?和?handlerResult?是公共方法, 分別用來顯示錯誤和統(tǒng)一處理結(jié)果集.

然后將主要精力放在發(fā)送請求和處理正確結(jié)果的方法上即可, 如這里的 deleteDone 函數(shù), 用來處理操作成功給用戶的提示信息, 正所謂各司其職, 前端負(fù)責(zé)操作成功的消息提示更合理, 而錯誤信息只有后臺知道, 所以需要后臺來返回.

后端統(tǒng)一處理異常

說了這么多, 還沒講到后端不在業(yè)務(wù)層捕獲任何異常的事, 既然所有業(yè)務(wù)層都沒有捕獲異常, 那么所有的異常都會拋出到 Controller 層, 我們只需要用 AOP 對 Controller 層的所有方法處理即可.

好在 Spring 為我們提供了一個注解, 用來統(tǒng)一處理異常:

@ControllerAdvice

@ResponseBody

publicclassWebExceptionHandler{

privatestaticfinalLogger log = LoggerFactory.getLogger(WebExceptionHandler.class);

@ExceptionHandler

publicResultBeanunknownAccount(UnknownAccountException e){

log.error("賬號不存在", e);

returnResultBean.error(1,"賬號不存在");

? ? }

@ExceptionHandler

publicResultBeanincorrectCredentials(IncorrectCredentialsException e){

log.error("密碼錯誤", e);

returnResultBean.error(-2,"密碼錯誤");

? ? }

@ExceptionHandler

publicResultBeanunknownException(Exception e){

log.error("發(fā)生了未知異常", e);

// 發(fā)送郵件通知技術(shù)人員.

returnResultBean.error(-99,"系統(tǒng)出現(xiàn)錯誤, 請聯(lián)系網(wǎng)站管理員!");

? ? }

}

在這里統(tǒng)一配置需要處理的異常, 同樣, 對于未知的異常, 一定要及時發(fā)現(xiàn), 并進(jìn)行處理. 推薦出現(xiàn)未知異常后發(fā)送郵件, 提示技術(shù)人員.

總結(jié)

總結(jié)一下統(tǒng)一異常處理的方法:

不使用隨意返回各種數(shù)據(jù)類型, 要統(tǒng)一返回值規(guī)范.

不在業(yè)務(wù)代碼中捕獲任何異常, 全部交由?@ControllerAdvice?來處理.

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末皮假,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子骂维,更是在濱河造成了極大的恐慌惹资,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,744評論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件航闺,死亡現(xiàn)場離奇詭異褪测,居然都是意外死亡,警方通過查閱死者的電腦和手機潦刃,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,505評論 3 392
  • 文/潘曉璐 我一進(jìn)店門侮措,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人乖杠,你說我怎么就攤上這事分扎。” “怎么了胧洒?”我有些...
    開封第一講書人閱讀 163,105評論 0 353
  • 文/不壞的土叔 我叫張陵畏吓,是天一觀的道長墨状。 經(jīng)常有香客問我,道長庵佣,這世上最難降的妖魔是什么歉胶? 我笑而不...
    開封第一講書人閱讀 58,242評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮巴粪,結(jié)果婚禮上通今,老公的妹妹穿的比我還像新娘。我一直安慰自己肛根,他們只是感情好辫塌,可當(dāng)我...
    茶點故事閱讀 67,269評論 6 389
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著派哲,像睡著了一般臼氨。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上芭届,一...
    開封第一講書人閱讀 51,215評論 1 299
  • 那天储矩,我揣著相機與錄音,去河邊找鬼褂乍。 笑死持隧,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的逃片。 我是一名探鬼主播屡拨,決...
    沈念sama閱讀 40,096評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼褥实!你這毒婦竟也來了呀狼?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,939評論 0 274
  • 序言:老撾萬榮一對情侶失蹤损离,失蹤者是張志新(化名)和其女友劉穎哥艇,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體草冈,經(jīng)...
    沈念sama閱讀 45,354評論 1 311
  • 正文 獨居荒郊野嶺守林人離奇死亡她奥,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,573評論 2 333
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了怎棱。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片哩俭。...
    茶點故事閱讀 39,745評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖拳恋,靈堂內(nèi)的尸體忽然破棺而出凡资,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 35,448評論 5 344
  • 正文 年R本政府宣布隙赁,位于F島的核電站垦藏,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏伞访。R本人自食惡果不足惜掂骏,卻給世界環(huán)境...
    茶點故事閱讀 41,048評論 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望厚掷。 院中可真熱鬧弟灼,春花似錦、人聲如沸冒黑。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,683評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽抡爹。三九已至掩驱,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間冬竟,已是汗流浹背欧穴。 一陣腳步聲響...
    開封第一講書人閱讀 32,838評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留泵殴,地道東北人苔可。 一個月前我還...
    沈念sama閱讀 47,776評論 2 369
  • 正文 我出身青樓,卻偏偏與公主長得像袋狞,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子映屋,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,652評論 2 354

推薦閱讀更多精彩內(nèi)容