likes
comments
collection
share

JAVA | 调皮的 InvocationTargetException 异常

作者站长头像
站长
· 阅读数 35

在业务开发中,一般都会定义全局通用的业务异常,然后通过拦截器对全局异常进行解析,返回给调用端对应的信息, 但是在某些场景(比如反射)下,抛出的业务异常会被吞掉,导致调用端收到懵逼的,统一的错误信息。

1 背景

今天有同事反馈,自定义的数据校验(会抛出全局自定义的业务异常),通过反射执行后,抛出的自定义业务异常没有被捕获到,一脸 o((⊙﹏⊙))o,这里用示例代码模拟下:

BusinessException 自定义的业务异常类

public class BusinessException extends Exception {
    private static final long serialVersionUID = 1L;

    private String code;
    private String message;

    public BusinessException(String code, String message) {
        this.code = code;
        this.message = message;
    }

    public String getMessage() {
        return message;
    }

    public String getCode() {
        return code;
    }
}

Form 验证表单

public class Form {

    public void validate() throws BusinessException {
        throw new BusinessException("10000", "缺少必要的参数");
    }
}

测试用例

public class TestCase {

    @Test
    public void error() throws BusinessException {
        try {
            Class<?> clazz = Class.forName("com.fairy.exercise.base.proxy.exec.Form");
            Method method = clazz.getMethod("validate");
            method.invoke(clazz.newInstance());
        } catch (ClassNotFoundException e) {
            System.out.println("异常:ClassNotFoundException 抛出");
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            System.out.println("异常:NoSuchMethodException 抛出");
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            System.out.println("异常:InvocationTargetException 抛出");
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            System.out.println("异常:IllegalAccessException 抛出");
            e.printStackTrace();
        } catch (InstantiationException e) {
            System.out.println("异常:InstantiationException 抛出");
            e.printStackTrace();
        }
    }
}

为了查看具体的异常,所以这里一个一个单独捕捉。

注意:如果直接 catch BusinessException 异常,不好意思,会直接报错,因为识别到的受控异常是 InvocationTargetException 异常。

运行结果如下:

JAVA | 调皮的 InvocationTargetException 异常

通过输出发现,业务中抛出的业务异常,被包装成了 InvocationTargetException 异常,也就是说,被 InvocationTargetException 给吞了,原来结在这里。

2 查查 InvocationTargetException 背景

查看源码快速锁定:

package java.lang.reflect;

/**
 * InvocationTargetException is a checked exception that wraps
 * an exception thrown by an invoked method or constructor.
 *
 * <p>As of release 1.4, this exception has been retrofitted to conform to
 * the general purpose exception-chaining mechanism.  The "target exception"
 * that is provided at construction time and accessed via the
 * {@link #getTargetException()} method is now known as the <i>cause</i>,
 * and may be accessed via the {@link Throwable#getCause()} method,
 * as well as the aforementioned "legacy method."
 *
 * @see Method
 * @see Constructor
 */
public class InvocationTargetException extends ReflectiveOperationException {
    ...
}

通过源码可以看到,位置:java.lang.reflect,其是一个受控异常,包装触发目标方法或者构造函数抛出的异常。哦,原来目标方法和构造函数抛出的异常都会包装成 InvocationTargetException 异常。到这里也就解答了同事的疑问。

但是问题又来了,为什么要包装成 InvocationTargetException 异常呢。

下面这几段摘自:www.zhihu.com/question/56…

因为 原来的异常 无法直接以一种统一而又明确的方式表达出来,所以使用 InvocationTargetException来将原来的异常包装起来,通过多加一层间接层的方式来提供统一的访问途径。

Java 方法可以静态声明它可能会抛出一组固定的异常类型。而反射 API 里,Method.invoke()Constructor.newInstance() 这些方法有 双重身份, 它们既代表要调用指定的目标方法,自身也是一个方法;目标方法可能会抛出异常,而它们自身在调用目标方法前也可能会抛出一些异常(例如IllegalArgumentException)。

它们要调用的目标方法可能抛出任意 Throwable 类的派生类的异常,但它们自身却不能根据要调用的目标而“动态”改变自己声明要抛出的异常类型,而只能静态声明一组可能抛出的异常类型。

声明抛出 ThrowableException 的话,这就太宽泛,难以准确反映异常的原因和意图;但不声明成这么宽泛的异常类型的话又无法完整覆盖所有可能由目标方法抛出的异常。

那怎么办?简单,新增一个 check exception 类型,InvocationTargetException,将原本由目标方法抛出的异常包装起来,这样就可以给 Method.invoke() / Constructor.newInstance() 的调用者一个统一的接口,既明确了 这个异常是由目标方法抛出的,不是由我自己抛出的 的意图,又能完整覆盖目标方法所能抛出的所有异常类型(InvocationTargetException.getTargetException() / getCause() 的类型是Throwable


3 怎么才能捕获自定义的异常

其实就是在捕捉到 InvocationTargetException 异常后,对此异常进行处理,然后再抛出对应的业务异常。

修改后的测试用例如下

public class TestCase {

    @Test
    public void error() throws BusinessException {
        try {
            Class<?> clazz = Class.forName("com.fairy.exercise.base.proxy.exec.Form");
            Method method = clazz.getMethod("validate");
            method.invoke(clazz.newInstance());
        } catch (ClassNotFoundException e) {
            System.out.println("异常:ClassNotFoundException 抛出");
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            System.out.println("异常:NoSuchMethodException 抛出");
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            // 🚀:重点关注代码块
            if (e.getTargetException() instanceof BusinessException) {
                System.out.println("异常:BusinessException 抛出");
                e.getCause().printStackTrace();
                // 抛出 BusinessException 类型的异常
                // throw (BusinessException) e.getTargetException();
            } else {
                System.out.println("异常:InvocationTargetException 抛出");
                e.printStackTrace();
            }
        } catch (IllegalAccessException e) {
            System.out.println("异常:IllegalAccessException 抛出");
            e.printStackTrace();
        } catch (InstantiationException e) {
            System.out.println("异常:InstantiationException 抛出");
            e.printStackTrace();
        }
    }
}

运行结果输出:

JAVA | 调皮的 InvocationTargetException 异常

查看输出结果,是符合预期的,但是这种处理方式就是对症下药,不知道有没有更加温和的方式。