我自己在刚开始写代码的时候,总是有着这样的疑惑,想着请求第三方接口,或者是重要的处理步骤万一出错了,是不是得把异常处理一下。但是在看代码时发现一个现象,项目中的异常主要处理形式如下所示,代码中基本没有try-catch的异常处理语句,大部分的异常直接向上层抛出。关于异常处理自己也查了一些资料和师兄讨论过,所以专门学习总结一下项目中关于异常处理思路。
public void methodB() throws 自定义Excetpion{
.......
.......
}
if(xxxx){
throw new 自定义Excetpion();
}
其实对于线上系统来说,最怕的事情有这两个,所以在日常开发中为了避免这两个问题,才形成了项目中常用的异常处理规约:
- 系统出现了异常不知道,下游用户投诉才知道出问题。
- 出了问题之后无法快速定位到出错位置及原因。
一、错误的异常处理示例
首先来看一下如果使用try-catch或者if语句来进行异常处理的话一般是如下这种方式:
private void methodName() {
JsonArray jsonArray = null;
try {
jsonArray = getMtlsById();
} catch (Exception e) {
logger.error("getMtlsById is error,message="+e.getMessage())
}
if(jsonArray != null){
logger.info("update jsonArray,jsonArray="+jsonArray);
updateMtls(jsonArray);
}
//dosomething
}
这段代码存在以下问题:
- 吞掉了异常。catch中就算打印了堆栈信息,面对着每天海量的日志信息,也不会有人每天盯着日志去看,除非用户告诉你出问题了,你才会去找日志!所以看着好像很严谨的代码,其实作用并不大。
- 异常处理再加上if语句中的空判断,当获取小资料出现异常的时候。本来需要更新jsonArray,由于没对为空的情况做处理,结果什么也没有做,什么错误也没有报,完美的避开了正确流程。
**其实这是新手最容易犯的错误,到处捕获异常,到处加空判断,自以为写出了“健壮”的代码,实际上完全相反。**导致的问题就是:
- 代码可读性很差,如果看到一半代码是try-catch和空判断,代码看过一遍之后,找不清楚业务逻辑,那简直要崩溃。
- 更加重要的掩盖了很多错误,就像上面的示例,日志是不会有人看的,我们的目的是尽早让错误抛出来。
**
**
所以代码要像下面这样干净整洁,重点一目了然,出异常了就甩锅给调用方处理吧。
private void methodName() throw YoursException {
JsonArray jsonArray = getMtlsById();
logger.info("update jsonArray,jsonArray="+jsonArray);
updateMtls(jsonArray);
}
二、异常处理思路
- 方法在设计时,不需要额外关心失败的情况,专注于业务逻辑的实现。失败的场景通过异常抛出即可。
- 方法在调用的过程中,即使我知道这个方法可能会失败,但不必立即处理这个异常,而是可以留到业务的边界集中进行处理。
- 因为异常定义时就带了层次结构,可以根据异常的层次结构进行分类批量处理。
- 如果是跨系统的调用,服务提供方要提供详细的异常信息,方便调用方排查问题。
比如说:
- 只有明显不需要关心的异常,如关闭资源的时候的io异常,可以捕获然后什么都不干,其他时候,不允许捕获异常,都抛出去,到controller处理,controller处理不了,就返回给调用方。
- 强调,空判断大部分时候不需要,但有些空判断是要的,如:参数是用户输入的情况下。为啥说大部分场景是不需要的,因为如参数是其它系统传过来,或者其他地方获取的传过来的,99.99%都不会为空,如果出现异常,就抛一个空指针到前台。
- 如果写了空判断,就必须测试为空和不为空二种场景,要么就不要写空判断。
三、checked和unchecked异常
在查资料的时候,大家的文章中频繁的提到checked和unchecked异常,刚开始不太明白,这里补充一下说明。Java的异常层次结构如下所示,简单来说将派生于Error或者RuntimeException的异常称为unchecked异常,所有其他的异常成为checked异常。
- Checked exception: 继承自 Exception 类。代码需要处理 API 抛出的 checked exception,要么用 catch 语句,要么直接用 throws 语句抛出去。可以理解为编译时异常, 编译器强制要求开发人员必须处理这些异常 。
- Unchecked exception: 也称 RuntimeException,它也是继承自 Exception。但所有 RuntimeException 的子类都有个特点,就是代码不需要处理它们的异常也能通过编译,所以它们称作 unchecked exception。 RuntimeException(运行时异常)不需要try…catch…或throws 机制去处理的异常,开发人员可以根据具体情况来选择是否处理这些异常 。
所以在日常开发中除了正确流程外的,所有预先可以判断的必须需要调用端处理的异常流程,都应该定义为checked异常,这样,可以显示的强制告诉接口调用方需要处理异常。举例,修改密码的方法如此定义: public boolean updatePwd(long userId) throws InvalidPwdException; 修改密码的过程中,可以预先判断,有可能原密码不正确,所以,通过定义一个checked异常:InvalidPwdException,显示要求调用方进行业务异常的处理;业务上无法预料的或者处理不了的异常,定义为unchecked业务异常。如在合成作战应用中,对处理不了的所有异常都统一包装为业务异常抛出。
四、异常码和异常类
可以根据是否是跨系统调用:
- 如果是长期对外提供服务的rpc接口(跨系统调用),可以选择Result + msg + errorcode方式更好;
- 如果是应用内部的相互调用,就可以选择异常类,比如login应用,用户修改密码,有可能出现原始密码不正确的异常,用户注册,有可能出现email重复的异常,这时,根据我的理解,使用2个checked异常。
维护异常码和异常之间的错误关系是一个非常繁杂的一件事,在异常种类较多情况下有一定优势,对用户友好,一般大型服务才会使用