四问四答Java异常抛出和处理

2023-11-22 09:49:56 浏览数 (1)

抛什么类型的异常?什么时候抛自定义异常?如何自定义异常?什么时候抛出和处理异常?

为了符合阅读习惯,下文直接用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中的常见异常都是什么意思吗?点开这里来看看吧

0 人点赞