优雅处理你的Java异常

作者:叶知泉

来源:https://my.oschina.net/c5ms/blog/1827907

本文介绍

本文仅按照业务系统开发角度描述异常的一些处理看法,不涉及java的异常基础知识,可以自行查阅 《Java核心技术 卷I》 和 《java编程思想》 可以得到更多的基础信息。

写在前面的话

笔者文笔功力尚浅,言语多有不妥,请慷慨指正,必定感激不尽. 本文提出了几个概念: 处理反馈;业务异常;代码错误。

请认真思考一下各中区别。

在开发业务系统中,我们目前绝大多数采用MVC模式,但是往往有人把service跟controller紧紧的耦合在一起,甚至直接使用Threadlocal来隐式传值,并且复杂的逻辑几乎只能使用service中存储的全局对象来传递处理结果,包括异常。

这样一来首先有违MVC模式,二来逻辑十分不清晰,难以维护。本文结合工作经验,给出一些异常使用建议,使用spring来实战异常为我们带来的好处。

常常,我们读罢了各种java的书,异常的各种机制,特性都很清楚,但是始终还是不知道如何使用,甚至背下了概念,却不知道如何致用。

我们开发的业务系统,或者是产品,常常面临着这样的问题:

系统运行出错,但是完全不知道错误发生的位置

我们找到了错误的位置,但是完全不知道是因为什么

系统明明出了错误,但是就是看不到错误堆栈信息

什么情况需要自定义异常

经常看到一些项目,在全局定义一个 AppException,然后所有地方都只抛出这个异常,并且把捕获的异常case到这个AppException中.会有如下问题:

浪费log日志存储空间,并且栈顶并不是最接近发生异常的代码位置

只有一种异常类,无法精准区分开异常类型

异常类后期难以修改以增加其携带的信息

什么情况需要手动处理异常

我不会把书上的东西直接复制下来,这里说一下容易记住的,并且适合业务开发的。

你有能力处理异常,并且你知道如何处理

你有责任处理异常

自定义业务异常

考虑如下场景: 系统提供一个API,用于修改用户信息,服务器端采用json数据交互.首先我们定义ServiceException,用来表示业务逻辑受理失败,它仅表示我们处理业务的时候发现无法继续执行下去。

/**

* 业务受理失败异常

*/

public class ServiceException extends RuntimeException {

   //接收reason参数用来描述业务失败原因.

 public ServiceException(String reason) { super(reason); }

}

接下来看下Controller层

// UserController.java

/**

  * 修改用户信息

  * @param userID 用户ID

  * @param user 修改用户信息表单数据

  */

 @PutMapping("{userID}")

 public JSONResult updateUser(@PathVariable("userID") Integer userID, @RequestBody UpdateUserForm userForm) {

   User user = new User(); //准备业务逻辑层使用的领域模型

   BeanUtils.copyProperties(userForm, user); //拷贝要修改的值

   user.setUserId(userID); //设置主键到用户数据中

   userService.updateUser(user); //调用更新业务逻辑

   JSONResult json = new JSONResult(); //准备要响应的数据

   json.put("user", user); //把修改后的用户数据还给页面

   return json; // --  

 }

关于上述Controller写法乍一看会有一些冗余,如果无法理解,请仔细研读MVC设计模式. 先不管service,我们来考虑下. 一个业务系统不可能不对用户提交的数据进行验证,验证包括两方面 : 有效性和合法性。

有效性: 比如用户所在岗位,是否属于数据库有记录的岗位ID,如果不存在,无效。

合法性: 比如用户名只允许输入最多12个字符,用户提交了20个字符,不合法。

有效性检查,可以交给java的校验框架执行,比如JSR303. 假设用户提交的数据经过验证都合法,还是有一些情况是不能调用修改逻辑的。

要修改的用户ID不存在

用户被锁定,不允许修改

乐观锁机制发现用户已经被被人修改过

由于某种原因,我们的程序无法保存到数据库

一些程序员错误的开发了代码,导致保存过程中出现异常,比如NPE

对于前3种,我们认为是有效性检查失败,第4种属与我们无法处理的异常,第5种就是程序员bug。

现在的问题是,前三种情况我们如何通知用户呢?

在ccontroller 调用userService的checkUserExist()方法

在controller直接书写业务逻辑

在service响应一个状态码机制,比如1 2 3表示错误信息,0 表示没有任何错误

显然前2种方法都不可取 ,因为MVC不设计模式告诉我们,controller是用来接收页面参数,并且调用逻辑处理,最后组织页面响应的地方。我们不可以在controller进行逻辑处理,controller只应该负责用户API入口和响应的处理(如若不然,思考一下如果有一天service的代码打包成jar放到另一个平台,没有controller了,该怎么办?)

状态码机制是个不错的选择,可是如此一来,用户保存逻辑变了,比如增加一个情况,不允许修改已经离职的用户,那么我们还需要修改controller的代码,代码量增加,维护成本增高,并且还耦合了service,不符合MVC设计模式。

那么怎么办呢?

现在我们来看下service代码如何编写

 /**

  * 修改用户信息

  * @param user 要修改的用户数据

  */

 public void updateUser(User user) {

   User userOrig = userDao.getUserById(user.getUserID());

   if (null == userOrig) {

     throw new ServiceException("用户不存在");

   }

   if (userOrig.isLocked()) {

     throw new ServiceException("用户被锁定,不允许修改");

   }

   if (!user.getVersion().equals(userOrig.getVersion())) {

     throw new ServiceException("用户已经被别人修改过,请刷新重试");

   }

   // TODO 保存用户数据  ...

 }

这样一来只要我们检查到不允许保存的项目,我们就可以直接throw 一个新的异常,异常机制会帮助我们中断代码执行。

接下来有2种选择:

在controller 使用try-catch进行处理

直接把异常抛给上层框架统一处理

第1种方式是不可取的 ,注意我们抛出的ServiceException,它仅仅逻辑处理异常,并且我们的方法前面没有声明throws ServiceException,这表示他是一个非受查异常.controller也没有关心会发生什么异常。

为什么不定义成受查异常呢? 

如果是一个受查异常,那么意味着controller必须要处理你的异常.并且如果有一天你的业务逻辑变了,可能多一种检查项,就需要增加一个异常,反之需要删除一个异常,那么你的方法签名也需要改变,controller也随之要改变,这又变成了紧耦合,这和用状态码123表示处理结果没有什么不同。

我们可以为每一种检查项定义一个异常吗? 

可以,但是那样显得太多余了.因为业务逻辑处理失败的时候,根据我们需求,我们只需要通知用户失败的原因(通常应该是一段字符串),以及服务器受理失败的一个状态码(有时可能不需要状态码,这要看你的设计了),这样这需要一个包含原因属性的异常即可满足我们需求。

最后我们决定这个异常继承自RuntimeException,并且包含一个接受一个错误原因的构造器,这样controller层也不需要知道异常,只要全局捕获到ServiceException做统一的处理即可,这无论是在struct1,2时代,还是springMVC中,甚至servlet年代,都是极为容易的!

异常不提供无参构造器 ,因为绝对不允许你抛出一个逻辑处理异常,但是不指明原因,想想看,你是必须要告诉用户为什么受理失败的!

如此一来,我们只需要全局统一处理下 ServiceException 就可以了,很好,spring为我们提供了ControllerAdvice机制,有关ControllerAdvice,可以查阅springMVC使用文档,下面是一个简单的示例:

@ControllerAdvice(basePackages = { "com.xxx.xxx.bussiness.xxx" })

public class ModuleControllerAdvice {

 private static final Logger LOGGER = LoggerFactory.getLogger(ModuleControllerAdvice.class);

 private static final Logger SERVICE_LOGGER = LoggerFactory.getLogger(ServiceException.class);

 /**

  * 业务受理失败

  */

 @ResponseBody

 @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)

 @ExceptionHandler(ServiceException.class)

 private JSONResult handleServiceException(ServiceException exception) {

   String message = "业务受理失败,原因:" + exception.getLocalizedMessage();

   SERVICE_LOGGER.info(message);

   JSONResult json = new JSONResult();

   json.serCode(500001); // 500000表示系统异常,500001表示业务逻辑异常

   json.setMessage(message);

   return json;

 }

}

在这个时候,我们就可以很轻松的处理各种情况了。

注意一点,在这个类中,我们定义了2个log对象,分别指向 ServiceException.class 和 ModuleControllerAdvice.class . 并且处理 ServiceException的时候使用了info级别的日志输出,这是很有用的。

首先,ServiceException一定要和其他的代码错误分离,不应该混为一谈

其次,ServiceException并不一定要记录日志,我们应该提供独立的log对象,方便开关

接下来你可以在修改用户的时候想客户端响应这样的JSON

{

   code: 200001,

   message: "业务受理失败,原因:用户名称不存在!"

}

如此一来没有任何地方需要关心异常,或者业务逻辑校验失败的情况.用户也可以得到很友好的错误提示。

如何对异常进行分类

如果你只需要一句概括,那么直接定义一个简单的异常,用于中断处理,并且与用户保持友好交互即可。

如果不可能一句话描述清楚,并且包含附加信息,比如需要在日志或者数据库记录消息ID,此时可能专门针对这种重要/复杂业务创建独立异常。

上述两种情况因为web系统,是用户发起请求之后需要等待程序给予响应结果的。

如果是后台作业,或者复杂业务需要追溯性.这种通常用流程判断语句控制,要用异常处理.我们认为这些流程判断一定在一个原子性处理中.并且检查到(不是遇到)的问题(不是异常)需要记录到用户可友好查看的日志.这种情况属于处理反馈,并不叫异常。

综上,笔者通常分为如下几类:

逻辑异常,这类异常用于描述业务无法按照预期的情况处理下去,属于用户制造的意外

代码错误,这类异常用于描述开发的代码错误,例如NPE,ILLARG,都属于程序员制造的BUG

专有异常,多用于特定业务场景,用于描述指定作业出现意外情况无法预先处理

各类异常必须要有单独的日志记录,或者分级,分类可管理.有的时候仅仅想给三方运维看到逻辑异常。

写在后面的注意

异常设计的初衷是解决程序运行中的各种意外情况,且异常的处理效率比条件判断方式要低很多。

上面这句话出自,但是我们思考如下几点:

业务逻辑检查,也是意外情况

UnknownHostException,表示找不到这样的主机,这个异常和NoUserException有什么区别么?换言之,没有这样的主机是异常,没有这样的用户不是异常了么? 所以一定要弄明白什么是用异常来控制逻辑,什么是定义程序异常。

异常处理效率很低

书中所示的例子,是在循环中大量使用try-catch进行检查,但是业务系统,用户发起请求的次数与该场景天壤地别.淘宝的11`11是个很好的反例.但是请你的系统上到这个级别再考虑这种问题。

系统有千万并发,不可能还去考虑这些中规中矩的按部就班的方式,别忘了MVC本来就浪费很多资源,代码量增加很多

业务系统也存在很多巨量任务处理的情况.但是那些任务都是原子性的,现在MVC中的controller和service可不是原子性的,不然为什么要区分这么多层呢

如果那么在乎效率,考虑下重写Throwable的fillStackTrace方法.你要知道异常的开销大到底大在什么地方,fillStackTrace是一个native方法,会填充异常类内部的运行轨迹

不要用异常进行业务逻辑处理

我们先来看一个例子:

   //这是一个非常典型的反例,也是一个误区.

 /**

  * 处理业务消息

  * @param message 要处理的消息

  */

 public void processMessage(Message message) {

   try{

       // 处理消息验证

       // 处理消息解析

       // 处理消息入库

   }catch(ValidateException e ){

       // 验证失败

   }catch(ParseException e ){

       // 解析失败

   }catch(PersistException e ){

       // 入库失败

   }

 }

上述代码就是典型的使用异常来处理业务逻辑.这种方式需要严重的禁止!上述代码最大的问题在于,我们如何利用异常来自动处理事务呢?

然而这和我们的异常中断service没有什么冲突.也并不是一回事。

我们提倡在 业务处理 的时候,如果发现无法处理直接抛出异常即可

而并不是在 逻辑处理 的时候,用异常来判断逻辑进行的状况

改正后的逻辑

 /**

  * 处理业务消息

  * @param message 要处理的消息

  */

 public void processMessage(Message message) {

   // 处理消息验证

   if(!message.isValud()){

       MessageLogService.log("消息校验失败"+message.errors())

       return ;

   }

   // 处理消息解析

   if(!message.parse()){

       MessageLogService.log("消息解析失败"+message.errors())

       return ;

   }

 // TODO ....

 }

最后俏皮一句:微服务横行的今天,我们在action里面直接写业务处理,也无可厚非。

- END -

以上是来自程序员DD的微信公众号,我个人感觉这篇文章非常好,首先他没有很多的代码篇幅,我们会异常,看代码也只是告诉我们如何写的,根本不知道理论知识,以及如何改进.其次,这是以个人开发的项目经验去谈异常,对自己以后在项目中设计异常很有帮助,对于我们新手来说更不知道如何去设计异常.最后表达一下自己的感慨,这篇文章说明了我们程序员不应该是拿着代码去copy,paste,我们应该称为技术的先驱者!

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

推荐阅读更多精彩内容