异常的分层处理
- 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保留标识资源不足,或者其他使程序无法进行的错误.最好不要实现它的子类
- RuntimeException:用来表明编程错误.比如校验入参
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封装错误码和错误信息
- 如果使用异常,一旦客户端没有处理,就会产生运行时错误,导致程序中断
- 应用内部推荐直接抛出异常