抛什么类型的异常?什么时候抛自定义异常?如何自定义异常?什么时候抛出和处理异常?
为了符合阅读习惯,下文直接用xx Exception代替称呼各种异常。
基本概念
先来张经典异常族谱图:
- Throwable:当对象为此类或其子类时,才能通过Java的throw语句抛出。
- Error:系统错误,由JVM处理,开发者无需处理。例如:OutOfMemoryError、StackOverflowError。
- Exception:异常,开发者需要关注。又分为Checked Exception和UnChecked Exception。
- Checked Exception/Compile-time Exception:此类异常需要在编译时处理。若方法声明抛出此类异常,开发者需要在程序捕获。例如:GeneralSecurityException、IllegalClassFormatException。
- UnChecked Exception/Runtime Exception:此类异常在编译时无需处理,运行时抛出。抛出后,当前运行的线程将中断。例如:NullPointerException、IllegalArgumentException。
抛什么类型的异常
需要处理且使用者有能力处理的场景,抛Checked Exception。
以stripe-java的request方法为例:
代码语言:javascript复制 /**
* Sends the given request to Stripe's API, and returns a buffered response.
*
* @param request the request
* @return the response
* @throws ApiConnectionException if an error occurs when sending or receiving
*/
@Override
public StripeResponse request(StripeRequest request) throws ApiConnectionException {
final StripeResponseStream responseStream = requestStream(request);
try {
return responseStream.unstream();
} catch (IOException e) {
throw new ApiConnectionException(
String.format(
"IOException during API request to Stripe (%s): %s "
"Please check your internet connection and try again. If this problem persists,"
"you should check Stripe's service status at https://twitter.com/stripestatus,"
" or let us know at support@stripe.com.",
Stripe.getApiBase(), e.getMessage()),
e);
}
}
该方法向stripe API发送请求,它抛出了一个ApiConnectionException,并附上供使用者参考的处理信息。
另外,还有一些可能导致程序崩溃的场景,特别是可能被坏人利用、篡改信息导致使用者程序不断崩溃的情况,酌情抛出Checked Excpetion让使用者处理。
总之,从使用者角度出发,抛出的异常要便于使用者处理,并尽量保证程序健壮。
什么时候抛自定义异常
有可使用的已定义异常,优先使用;没有时才考虑自定义异常。
抛出的异常种类越多,使用者的理解、处理成本越高。因此,尽量使用已有异常,当需要抛业务相关的异常,才考虑自定义。
自定义异常注意事项
语义清晰、带有业务意义、保留原异常数据。 一般自定义异常都为Checked Exception。如果抛出自定义的Unchecked Exception,使用者没有关注到,当程序上线了,突然出现一个不认识的Unchecked Exception,他的心情一定很复杂。
以GeneralSecurityException为例:
代码语言:javascript复制package java.security;
public class GeneralSecurityException extends Exception {
private static final long serialVersionUID = 894798122053539237L;
public GeneralSecurityException() {
super();
}
public GeneralSecurityException(String msg) {
super(msg);
}
public GeneralSecurityException(String message, Throwable cause) {
super(message, cause);
}
public GeneralSecurityException(Throwable cause) {
super(cause);
}
}
可以看到定义异常有以下要点:
- serialVersionUID
在序列化和反序列化中作为唯一标识。因为Throwable实现了Serializable接口,支持序列化和反序列化,所以所有异常都定义要它。
- 自定义message 异常描述信息。便于使用者定位、排查问题。最好提供提供处理指引,像上面提到的stripe-java的request方法的错误描述。
- 原始Throwable类 引起该异常的源头。保留完整信息,便于使用者定位、排查问题。
对于具体业务,如果有需要,我们还可以抽象合适的异常层级或者添加业务信息。例如stripe-java的StripeException。它是Stripe自定义异常的基类,封装了业务相关的requestId、Code、statusCode等。
什么时候抛出和处理异常
尽量早地抛出异常,尽量晚地捕获异常。
类比打工人的早C晚A,对异常来说,就是早T(throw)晚C(catch)。
尽量早地抛出异常,才能更好地定位、解决问题。
在Exceptions in Java中举了一个例子:多线程执行时,一个线程遇到ConcurrentModificationException异常没有及时抛出,为定位和解决问题带来巨大代价。
在最合适的时候才捕获异常,才能更好地解决问题。
10 Best Practices to Handle Java Exceptions中有一条原则:只在可以处理的时候捕获并处理,不能处理的异常就抛出去。
在上面的stripe-java的request方法中,可以选择在request内调用内部方法遇到异常时就捕获并处理,例如主动进行1次重试,遇到特定httpcode作特定操作。
但这不是最好的处理方式。抛出异常让使用者处理,具有拓展性,用户体验最优。使用者能够根据自身需要,选择什么场景重试,什么场景不重试,什么场景重试几次等等。
小结
在业务程序中抛出自定义异常时,我曾经想过只定义一个xxExcpetion,然后用错误码来代表不同的异常类型。听起来有点像Java和C 的城中结合风。
这样做不是不行,只是异常含义不明确,还需要使用者二次转义。如果你的异常体系已经很庞大,其中一种类型异常本身包括二次转义(例如,http请求不同返回码含义不同),这也是一个可取的方式。
还有,在获取不到数据时,是返回null,还是抛出NoSuchElementException,还是抛出自定义异常,也是一个在不同场景下有不同答案的问题。
遇到问题时,可以看最佳实践,或者看看别人怎么做的。看多了就会发现,更不懂了(指我)。
欢迎和我分享你的实践经验和观点,拯救我于水深火热的菜狗队列之中。
小彩蛋:你知道Java中的常见异常都是什么意思吗?点开这里来看看吧