Java中异常的装包与拆包

分类: Java

保留所有版权,请引用而不是转载本文(原文地址 https://yeecode.top/blog/75/ )。

在Java中,某些异常会被装到其他异常中之后才能抛出,我们将这个过程称为异常的装包。而当我们获取到某些异常时,需要拆解开来才能见到其内部包裹的真正异常,我们将这个过程称为异常的装包。

本文将从MyBatis源码中的ExceptionUtil类入手,详细了解Java中异常装包和拆包的原因和机制。相关内容参考《通用源码阅读指导书——MyBatis源码详解》一书。

ExceptionUtil是MyBatis的一个异常工具类,它提供一个拆包异常的工具方法unwrapThrowable。该方法是将InvocationTargetException和UndeclaredThrowableException这两类异常进行拆包,得到它们中包含的真正的异常。

unwrapThrowable方法带注释的源码如下所示。

/**
 * 拆解InvocationTargetException和UndeclaredThrowableException异常的包装,从而得到被包装的真正异常
 * @param wrapped 包装后的异常
 * @return 拆解出的被包装异常
 */
public static Throwable unwrapThrowable(Throwable wrapped) {
  // 该变量用以存放拆包得到的异常
  Throwable unwrapped = wrapped;
  while (true) {
    if (unwrapped instanceof InvocationTargetException) {
      // 拆包获得内部异常
      unwrapped = ((InvocationTargetException) unwrapped).getTargetException();
    } else if (unwrapped instanceof UndeclaredThrowableException) {
      // 拆包获得内部异常
      unwrapped = ((UndeclaredThrowableException) unwrapped).getUndeclaredThrowable();
    } else {
      // 该异常无需拆包
      return unwrapped;
    }
  }
}

unwrapThrowable方法的结构非常简单,但是我们需要思考它存在的意义:为什么需要给InvocationTargetException和UndeclaredThrowableException这两个类拆包?这两个类为什么要把其他异常包装起来?

很多时候读懂源码的实现并不难,但是一定要多思考源码为什么这么写。只有这样,才能在源码阅读的过程中有更多的收获。

接下来我们通过了解InvocationTargetException和UndeclaredThrowableException这两个类来解答上述疑问。

InvocationTargetException为必检异常,UndeclaredThrowableException为免检的运行时异常。它们都不属于MyBatis,而是都来自于java.lang.reflect包。

反射操作中,代理类通过反射调用目标类的方法时,目标类的方法可能抛出异常。反射可以调用各种目标方法,因此目标方法抛出的异常是多种多样无法确定的。这意味着反射操作可能抛出一个任意类型的异常。我们可以用Throwable去接收这个异常,但这无疑太过宽泛。

InvocationTargetException就是为解决这个问题而设计的,当反射操作的目标方法中出现异常时,都统一包装成为一个必检异常InvocationTargetException。InvocationTargetException内部的target属性则保存了原始的异常。这样一来,便使得反射操作中的异常更易管理。InvocationTargetException类带注释的源码如下所示。

public class InvocationTargetException extends ReflectiveOperationException {
    private static final long serialVersionUID = 4085088731926701167L;
    // 用来保存被包装的异常
    private Throwable target;

    /**
     * 构造方法
     * @param target 被包装的异常
     * @param s      异常的详细信息
     */
    public InvocationTargetException(Throwable target, String s) {
        super(s, null);  // Disallow initCause
        this.target = target;
    }

    // 省略了许多其他方法
}

说完了InvocationTargetException,我们再说说UndeclaredThrowableException。

根据Java的继承原则,我们知道:如果子类中要重写父类中的方法,那么子类方法中抛出的必检异常必须是父类方法中声明过的类型。

在建立目标类的代理类时,通常是建立了目标类接口的子类或者目标类的子类(《通用源码阅读指导书》的10.1.3 基于反射的动态代理章节和22.1.1 基于cglib的动态代理章节详细介绍了动态代理)。因此将Java的继承原则放在代理类和被代理类上可以演化为:

  • 如果代理类和被代理类实现了共同的接口,则代理类方法中抛出的必检异常必须是在共同接口中声明过的;
  • 如果代理类是被代理类的子类,则代理类方法中抛出的必检异常必须是在被代理类的方法中声明过的。

可是在代理类中难免会在执行某些方法时抛出一些共同接口或者父类方法中没有声明的必检异常,那这这个问题该怎么解决呢?如果不抛出,则它是必检异常,必须抛出;如果抛出,则父接口或父类中没有声明该必检异常,不能抛出。

答案就是这些必检异常会被包装为免检异常UndeclaredThrowableException后抛出。所以说UndeclaredThrowableException也是一个包装了其他异常的异常,UndeclaredThrowableException类带注释的源码如下所示,其包装的异常在undeclaredThrowable属性中。

public class UndeclaredThrowableException extends RuntimeException {
    static final long serialVersionUID = 330127114055056639L;
    // 被包装的必检异常
    private Throwable undeclaredThrowable;

    /**
     * 构造方法
     * @param   undeclaredThrowable 被包装的必检异常
     * @param   s                   异常的详细信息
     */
    public UndeclaredThrowableException(Throwable undeclaredThrowable, String s)
    {
        super(s, null); 
        this.undeclaredThrowable = undeclaredThrowable;
    }

    // 省略了许多其他方法
}

有一个简单的例子可以恰好同时涉及InvocationTargetException和UndeclaredThrowableException这两个异常。就是代理类在进行反射操作时发生异常,于是异常被包装成InvocationTargetException。InvocationTargetException显然没有在共同接口或者父类方法中声明过,于是又被包装成了UndeclaredThrowableException。这样,真正的异常就被包装了两层。这也是为什么在ExceptionUtil的unwrapThrowable方法中存在一个“while (true)”死循环,用来持续拆包。

总之,InvocationTargetException和UndeclaredThrowableException这两个类都是异常包装类,需要拆包后才能得到真正的异常类。而ExceptionUtil的unwrapThrowable方法就可以完成该拆包工作。

以上内容均参考《通用源码阅读指导书——MyBatis源码详解》一书。

通用源码阅读指导书-京东自营

《通用源码阅读指导书》

这是一本以MyBatis的源码为实例讲述源码阅读方法的书籍,并且附带有示例项目源码,MyBatis的全中文注释。书籍还总结了大量的编程知识和架构经验,对提升编程和架构能力十分有用。推荐给大家。

可以访问个人知乎阅读更多文章:易哥(https://www.zhihu.com/people/yeecode),欢迎关注。

作者书籍推荐

作者书籍推荐 作者书籍推荐