什么是UndeclaredThrowableException异常
学习java的老铁们应该都知道,在java中子类方法不能抛出被父类更广泛的异常,意思就是说,如果在父类或者接口的方法签名中声明了,该方法可以抛出A异常,那么子类或者实现类在重写这个方法的时候,只能抛出异常A或者A的子类才可以,而且这个规则的验证,已经放到了java编译器中了,如果在代码编写的过程中违反了这个规则,会出现编译错误。
不知道各位老铁有没有想过,java为什么会有这样的要求?这是因为在面向对象编程范式中,要求我们面向抽象而非具体编程。所在在编写方法调用的客户端程序时,调用的方法是定义在父类或者接口中,那么此时对异常的处理也是面向父类方法中抛出的异常来的,如果子类在实现方法时,抛出了比父类更广泛异常的话,那么在运行期,客户端程序的异常处理逻辑就失效了,这显然是不符合我们预期的。
了解了java中的这个异常处理规则后,我么再来看看UndeclaredThrowableException 这个异常,看起来是不是很陌生,不过从名字中可以大概看出他的含义:未声明的Throwable异常,就是抛出了未声明的异常。这个异常描述的就是子类方法抛出了比父类方法声明异常更广泛的异常。但是上面我们说到,在代码编写的时候编译器会帮助我们进行检查,来避免这类情况的出现,所以一般情况下,我们基本上见不到这个异常类,这也是我们对这个异常很陌生的原因。那么到底什么情况下抛出这个异常呢?
为什么会抛出这个异常
上面我们说到了抛出这个异常的原因,也说了因为编译器的帮助,这种异常也很少会产生,那么这种异常产生场景是什么呢?
我们知道,编译器的检查是在编译期,如果在运行期出现了违反上面所说的规则的话,如动态代理生成的代理对象,对于这种情况编译器就无能为力了,那么在使用动态代理场景下,是如何触发这个异常的呢?下面我们复现一下。
异常复现
以jdk动态代理为例,jdk中动态代理的实现方式是生成一个和被代理对象实现相同接口的一个实现类,然后在这个实现类中对被代理对象进行增强。具体细节看下面代码:
interface Flyable {
void fly() throws IOException;
}
static class Bired implements Flyable {
@Override
public void fly() throws IOException {
}
}
static class Handler implements InvocationHandler {
private Object target;
public Handler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println(String.format("before invoke %s", method.getName()));
throw new Exception("throw checked exception");
}
}
public static void main(String[] args) {
// 将生成的代理类的class文件,写入磁盘
System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
Class birdClazz = Bired.class;
Bired bired = new Bired();
Flyable flyable = (Flyable) Proxy.newProxyInstance(birdClazz.getClassLoader(), birdClazz.getInterfaces(),
new Handler(bired));
try {
flyable.fly();
} catch (IOException e) {
e.printStackTrace();
}
}
上面的代码是使用jdk动态代理的经典范式,接口Flyable的fly方法声明了可能抛出IOException,所以在main方法调用的时候,就对IOException进行了捕获处理,但是生成的代理对象的fly方法会抛出Exception异常,也就是比IOException更广泛的异常。那么此时执行main方法就会抛出 UndeclaredThrowableException。
执行结果如下图:
看到这里你可能会很奇怪,代理类的方法抛出的异常明明是Exception,那么为什么我们应用程序中,收到的却是UndeclaredThrowableException呢?有问题看"源码",这里的源码并不是我们写的这些java代码,而是jvm执行的字节码,看看生成的代理类是如何抛出 UndeclaredThrowableException的。
细心的小伙伴已经发现了,我在main方中配置的一个property:
System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
这段代码就是将生成的代理类的字节码写到磁盘,代理类的名称 $Proxy0.class,使用反编译工具查看一下具体内容:
public final void fly() throws IOException {
try {
super.h.invoke(this, m3, (Object[])null);
} catch (RuntimeException | IOException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
其中这段代码是抛出 UndeclaredThrowableException的关键地方:这里只对RuntimeException,Error,IOException三种异常进行了处理,如果抛出了这三种异常之外的异常,那么异常就会被包装成 UndeclaredThrowableException 抛出去。看到这里应该就知道 UndeclaredThrowableException是怎么抛出来的了吧。
总结来说就是:如果抛出RuntimeExcepton,Error和声明的异常以及其子类外的其他异常,都会被统一转换成 UndeclaredThrowableException 进行抛出,这也是实现“子类不能抛出比父类更广泛异常"的规则。
更详细的内容可以参考 :https://docs.oracle.com/javase/7/docs/api/java/lang/reflect/UndeclaredThrowableException.html
解决方案
通过抛出异常的堆栈可以看出,当转换成了UndeclaredThrowableException后,十分不利于异常问题的排查。那么对于这个问题该怎么解决呢? 知道了问题发生的原因,其实问题也就比较容易解决了,常用的方法有以下几种:
1.将可能抛出声明异常外的其他异常的地方,进行异常捕获处理,避免此类异常的抛出。
2.在InvocationHandler的 invoke方法中进行异常捕获,然后统一抛出运行时异常。