Loading

Java异常处理

异常的分层处理

  • adaptor
    • 统一拦截异常,返回ServiceData
  • app
    • 不做异常处理
  • infrastructure
    • 不做处理

Result vs Exception

Application层只返回DTO,可以直接抛异常,不用统一处理。所有调用到的服务也都可以直接抛异常,除非需要特殊处理,否则不需要刻意捕捉异常。

异常的好处是能明确的知道错误的来源,堆栈等,在Interface层统一捕捉异常是为了避免异常堆栈信息泄漏到API之外,但是在Application层,异常机制仍然是信息量最大,代码结构最清晰的方法,避免了Result的一些常见且繁杂的Result.isSuccess判断。所以在Application层、Domain层,以及Infrastructure层,遇到错误直接抛异常是最合理的方法。

异常使用原则

1.只针对异常情况才使用异常,不应该用于控制流的逻辑判断

如果类有"状态相关"的方法,应该提供一个"状态检测"方法,而不是强迫客户端为了正常的控制流而使用异常.

  • 提供专门的状态检测方法

比如,Iterator接口的hasNext方法就是"状态检测"方法,它的next方法就是"状态相关方法".

如果缺少hasNext方法

try{
    Iterator<Foo> i = collection.iterator();
    while(true) {
        Foo foo = i.next();
        ...
    }
} catch(NoSuchElementException) {
}

状态检测之后的做法:

Iterator<Foo> i = collection.iterator();
while(i.hasNext()) {
    Foo foo = i.next();
        ...
}

就无需关注异常的处理,代码可读性强.

但是这种方式需要注意并发的问题,因为状态检测和执行方法不是原子的操作,在"状态检测"和"状态相关"方法之间,可能存在检测完状态后,状态被并发修改的情况.这时可以使用可识别的返回值来做状态检测.

  • 返回可识别的返回值

如果"状态相关"方法返回一个标识值,比如null,表示处于不正确的状态中,那么就当作异常处理

while(true) {
    String status = i.next();
    if(ERROE_STATUS.equlas(value) {
        // 执行其他操作
    }
}

相应地,这种方式的代码可读性差一点,如果忘记做状态校验,就会有bug.

2.对于可恢复的异常用受检(checked)异常,对编程错误使用运行时异常(RuntimeException)

  • throwable可抛出结构
    • checked exception 受检异常
    • unchecked exception 非受检异常
      • runtime exception 运行时异常
      • error 错误

说明一下这几种异常:

  • 受检异常
    • 如果期望调用这能够适当地恢复,应该使用受检异常.强迫调用者处理.
    • 因为受检异常往往指明了可恢复的条件,所以在设计API的时候,最好针对这个异常情况,提供一些辅助方法,调用者可以方便地获取有助于恢复的信息.
    • 使用受检异常的原则:
      • 正确地使用API不能阻止这种异常条件的产生
      • 一旦产生异常,客户端程序员可以立即采取有效措施
    • 不要过分使用受检异常,会给客户端增添负担-->可以用上文提到的"状态检测"方法,把受检异常变为非受检异常.但要注意缺少同步时的并发问题
  • 非受检异常
    • RuntimeException:用来表明编程错误.比如校验入参
      • 可预测的异常:如边界越界,空指针,这种异常不应该产生或抛出.要在代码里做好边界检查,空指针校验.
      • 需要捕捉的异常:比如RPC调用超时,这类异常客户端必须显示处理,不能因为服务端的异常导致客户端不可用,一般处理方式是重试或者降级处理
      • 可透出异常:比如框架的异常,程序无需关心
    • error:往往被JVM保留标识资源不足,或者其他使程序无法进行的错误.最好不要实现它的子类

3.优先使用标准的异常

原因很简单,因为大多数程序员都认识这些异常.

4.抛出与抽象相对应的异常

底层的异常被抛到高层,需要根据高层的的抽象含义做异常转译,能够被高层的使用者了解含义.

推荐的做法是根据当前场景定义具有业务含义的异常.

比如:对于i.next就是NoSuchElementException,而对于get方法就是IndexOutOfBoundsException

public E get(int index) {
   ListIterator<E> i = listIterator(index);
   try {
   		return i.next();   
   } catch(NoSuchElementException e){
       throw new IndexOutOfBoundsException("Index:"+index);
   }
}

5.每个方法抛出的异常都要有文档

Javadoc说明什么情况下抛出异常

6.在细节消息中包含能捕获失败的信息

7.努力使失败保持原子性

失败的方法调用应该使对象保持再被调用之前的状态.调用方可能会执行一些回滚操作.

8.不要忽略异常

永远不要什么都不做地catch异常

try {
    doSomehing();
} catch(Exception e) {
    // do nothing
}

9.try-catch-finally

加锁的情况,下面的代码,在try块内进行加锁,如果加锁失败,lock.unlock()就会报错.所以要再try块之前调用loc()方法,避免由于加索失败导致finally调用unlock()抛出异常.

Lock lock = new XxxLock();
preDo();
try{
    // 无论加锁是否成功,unlock都会执行
    lock.lock();
    doSomething();
} finally {
    lock.unlock();
}

10.错误码or抛出受检异常?

  • 对外提供的开放接口用错误码
  • 公司内服务之间用统一的Result封装错误码和错误信息
    • 如果使用异常,一旦客户端没有处理,就会产生运行时错误,导致程序中断
  • 应用内部推荐直接抛出异常
posted @ 2021-06-23 16:29  非凡岁月  阅读(80)  评论(0编辑  收藏  举报