优雅的全局异常处理方式

在 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

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 这种字符串来当键很容易处理, 例如你这里叫 message, 别人起名叫 msg, 甚至有时手抖打错了, 怎么办? 前台再改成 msg 或其他的字符?, 前端后端这样一直来回改?

更有甚者在情况 A 的情况下, 返回 json, 在情况 B 的情况下, 重定向到某个页面, 这就更乱了. 对于这种不统一的结构处理起来非常麻烦.

异常处理规范

既然要进行统一异常处理, 那么肯定要有一个规范, 不能乱来. 这个规范包含前端和后端.

不要捕获任何异常

对的, 不要在业务代码中进行捕获异常, 即 dao、service、controller 层的所以异常都全部抛出到上层. 这样不会导致业务代码中的一堆 try-catch 会混乱业务代码.

统一返回结果集

不要使用 Map 来返回结果, Map 不易控制且容易犯错, 应该定义一个 Java 实体类. 来表示统一结果来返回, 如定义实体类:

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 略

}

正常情况: 调用 ResultBean.success() 或 ResultBean.success(Collection<V> data), 不需要返回数据, 即调用前者, 需要返回数据, 调用后者. 如:

@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();

}

一般只有查询方法需要调用 ResultBean.success(Collection<V> data) 来返回 N 条数据, 其他诸如删除, 修改等方法都应该调用 ResultBean.success(), 即在业务代码中只处理正确的功能, 不对异常做任何判断. 也不需要对 update 或 delete 的更新条数做判断(个人建议, 实际需要根据业务). 只要没有抛出异常, 我们就认为用户操作成功了. 且操作成功的提示信息在前端处理, 不要后台返回 “操作成功” 等字段.

前台接受到的信息为:

{

"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: 错误信息

*/

functionshowError(s){

    alert(s);

}

/**

* 处理 ajax 请求结果

*@param result: ajax 返回的结果

*@param fn: 成功的处理函数 ( 传入data: fn(result.data) )

*/

functionhandlerResult(result, fn){

// 成功执行操作,失败提示原因

if(result.code ==0) {

        fn(result.data);

    }

// 用户操作异常, 这里可以对 1 或 2 等错误码进行单独处理, 也可以 result.code > 0 来粗粒度的处理, 根据业务而定.

elseif(result.code ==1) {

        showError(result.message);

    }

// 系统异常, 这里可以对 -1 或 -2 等错误码进行单独处理, 也可以 result.code > 0 来粗粒度的处理, 根据业务而定.

elseif(result.code ==-1) {

        showError(result.message);

    }

// 如果进行细粒度的状态码判断, 那么就应该重点注意这里没出现过的状态码. 这个判断仅建议在开发阶段保留用来发现未定义的状态码.

else{

showError("出现未定义的状态码:"+ result.code);

    }

}

/**

* 根据 id 删除商品

*/

functiondeleteGoods(id){

    $.ajax({

type:"GET",

url:"/goods/delete",

dataType:"json",

success:function(result){

            handlerResult(result, deleteDone);

        }

    });

}

functiondeleteDone(data){

alert("删除成功");

}

showError 和 handlerResult 是公共方法, 分别用来显示错误和统一处理结果集.

然后将主要精力放在发送请求和处理正确结果的方法上即可, 如这里的 deleteDone 函数, 用来处理操作成功给用户的提示信息, 正所谓各司其职, 前端负责操作成功的消息提示更合理, 而错误信息只有后台知道, 所以需要后台来返回.

后端统一处理异常

说了这么多, 还没讲到后端不在业务层捕获任何异常的事, 既然所有业务层都没有捕获异常, 那么所有的异常都会抛出到 Controller 层, 我们只需要用 AOP 对 Controller 层的所有方法处理即可.

好在 Spring 为我们提供了一个注解, 用来统一处理异常:

@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("发生了未知异常", e);

// 发送邮件通知技术人员.

returnResultBean.error(-99,"系统出现错误, 请联系网站管理员!");

    }

}

在这里统一配置需要处理的异常, 同样, 对于未知的异常, 一定要及时发现, 并进行处理. 推荐出现未知异常后发送邮件, 提示技术人员.

总结

总结一下统一异常处理的方法:

不使用随意返回各种数据类型, 要统一返回值规范.

不在业务代码中捕获任何异常, 全部交由 @ControllerAdvice 来处理.

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,236评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,867评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,715评论 0 340
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,899评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,895评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,733评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,085评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,722评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,025评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,696评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,816评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,447评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,057评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,254评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,204评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,561评论 2 343

推荐阅读更多精彩内容