动态代理模式下UndeclaredThrowableException的产生

时间:2022-11-12 10:57:02

API文档

我们先来看下这个异常类的api文档:

Thrown by a method invocation on a proxy instance if its invocation handler's invoke method throws a checked exception (a Throwable that is not assignable to RuntimeException or Error) that is not assignable to any of the exception types declared in the throws clause of the method that was invoked on the proxy instance and dispatched to the invocation handler.

这段描述中介绍了异常会被抛出的情况:调用代理实例的增强方法,如果调用处理程序(增强器)的invoke方法中抛出一个检查异常,但该异常不能被throws子句中声明的任何异常捕获(默认是RuntimeException和Error),那么UndeclaredThrowableException这个异常就会被代理实例抛出。

代码演示

由于是使用JDK的动态代理进行演示,那肯定少不了接口类:

public interface Animal {
// 奔跑
void run();
}
复制代码

被代理类:

public class Pig implements Animal {
@Override
public void run() {
System.out.println("猪突猛进");
}
}
复制代码

以及增强器InvocationHandler

public class AnimalInvocationHandler implements InvocationHandler {

private final Object target;

public AnimalInvocationHandler(Object target) {
this.target = target;
}

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("增强方法 -> className: " + target.getClass().getSimpleName() + " methodName:" + method.getName());
method.invoke(target, args);
throw new Exception("throw not catch exception");
}
}
复制代码

一切准备就绪,开始测试

public static void main(String[] args) throws Exception {

// 1、创建 InvocationHandler 实例并设置代理的目标类对象
Animal pig = new Pig();
InvocationHandler invocationHandler = new AnimalInvocationHandler(pig);

// 2、创建代理类型,获取一个带有InvocationHandler参数的构造器
Class<?> proxyClass = Proxy.getProxyClass(Animal.class.getClassLoader(), Animal.class);
Constructor<?> ProxyConstructor = proxyClass.getConstructor(InvocationHandler.class);

// 3、以构造器的方式创建代理实例,执行增强方法
Animal animalProxy = (Animal) ProxyConstructor.newInstance(invocationHandler);
try {
animalProxy.run();
} catch (Exception e) {
e.printStackTrace();
}
}

启动main方法后,控制台打印:

增强方法 -> className:Pig  methodName:run
猪突猛进
java.lang.reflect.UndeclaredThrowableException
at com.sun.proxy.$Proxy0.run(Unknown Source)
at com.learn.springtest.undeclaredthrowable.MainClass.main(MainClass.java:40)
Caused by: java.lang.Exception: throw not catch exception
at com.learn.springtest.undeclaredthrowable.AnimalInvocationHandler.invoke(AnimalInvocationHandler.java:22)
... 2 more

从上面的结果输出中,可以明显看到:代理类抛出的这个异常,而根本原因是AnimalInvocationHandler的invoke方法中抛出了Exception异常。

解释:由于Exception不在throws子句中声明的任何异常(Animal#run方法没有声明抛出异常,默认就是RuntimeException和Error)的范围内,异常无法被捕获。最终,代理类上抛了UndeclaredThrowableException异常,事实也确实如此!

两个影响因素

从上面的叙述中,可以得到一个结论,那就是影响代理类能否抛出UndeclaredThrowableException的因素有两个:

  1. 被代理类的方法上声明的抛出异常;
  2. 增强器InvocationHandler的invoke方法中抛出的异常类型;

接下来,将以实验的方式验证这两个影响因素,毕竟伟大的领袖*曾说过:实践是检验真理的唯一标准。

实验1:

其他代码不变,被代理类的方法上抛出Exception异常,改动如下:

void run() throws Exception;

执行main方法,控制台输出:

增强方法 -> className:Pig  methodName:run
猪突猛进
java.lang.Exception: throw not catch exception
at com.learn.springtest.undeclaredthrowable.AnimalInvocationHandler.invoke(AnimalInvocationHandler.java:22)
at com.sun.proxy.$Proxy0.run(Unknown Source)
at com.learn.springtest.undeclaredthrowable.MainClass.main(MainClass.java:40)

实验2:

其他代码不变,增强器AnimalInvocationHandler中抛出的异常改为RuntimeException,改动如下:

throw new RuntimeException("throw not catch exception");

执行main方法,控制台输出:

增强方法 -> className:Pig  methodName:run
猪突猛进
java.lang.RuntimeException: throw not catch exception
at com.learn.springtest.undeclaredthrowable.AnimalInvocationHandler.invoke(AnimalInvocationHandler.java:22)
at com.sun.proxy.$Proxy0.run(Unknown Source)
at com.learn.springtest.undeclaredthrowable.MainClass.main(MainClass.java:40)

通过上面实验中的针对性改动,两次的运行结果中都没有再出现UndeclaredThrowableException异常,那么这两个影响因素也就得到了证实。

字节码文件

生成代理实例的字节码文件

public static void main(String[] args) throws Exception {

// 测验代码,忽略
...

// 保存代理类
saveGeneratedJdkProxyFiles();
saveClass("Pig$Proxy0", proxyClass.getInterfaces(), "");
}

/**
* 开启设置:允许生成Java动态代理生成的类文件
*/
public static void saveGeneratedJdkProxyFiles() throws Exception {
Field field = System.class.getDeclaredField("props");
field.setAccessible(true);
Properties props = (Properties) field.get(null);
props.put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
}

/**
* 生成代理类 class 并保持到文件中
*
* @param className 生成的代理类名称
* @param interfaces 代理类的实现接口
* @param pathDir 代理类保存的目录路径,需要以目录分隔符"/"结尾
*/
public static void saveClass(String className, Class<?>[] interfaces, String pathDir) {
byte[] classFile = ProxyGenerator.generateProxyClass(className, interfaces);

// 如果目录不存在就新建所有子目录
Path path = Paths.get(pathDir);
if (!path.toFile().exists()) {
path.toFile().mkdirs();
}

String fullFilePath = pathDir + className + ".class";
try (FileOutputStream fos = new FileOutputStream(fullFilePath)) {
fos.write(classFile);
fos.flush();
System.out.println("代理类的class文件写入成功");
} catch (Exception e) {
e.printStackTrace();
}
}

执行main方法,在当前项目的根目录下可以找到一个名称为Pig$Proxy0.class的文件,它就是代理类的字节码文件。

贴一下代理类的字节码文件中run()增强方法:

public final void run() throws  {
try {
super.h.invoke(this, m3, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}

这样就可以很直观的看到,如果增强器InvocationHandler#invoke方法中抛出的异常,不能被RuntimeException或者Error捕获,最终就会抛出UndeclaredThrowableException异常。

其他代码不变,被代理类的方法上抛出IOException异常,执行main方法重新生成字节码文件

public final void run() throws IOException {
try {
super.h.invoke(this, m3, (Object[])null);
} catch (RuntimeException | IOException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}

增强方法的throws子句中多了一个IOException类型的异常。

Tips

1、saveClass的第三个参数中传入的是空字符串,这样生成的代理类字节码文件就会出现在项目的根目录下,直接使用编辑器(如:IDEA)打开即可。

如果生成在其他地方,就需要使用java字节码文件的反编译工具,推荐使用 Java Decompiler

下载后解压,打开程序,直接把.class文件丢进去就行了。

2、开启生成字节文件的配置方法中,丢进去一个key值为“sun.misc.ProxyGenerator.saveGeneratedFiles”的配置项

如果无法生成字节码文件,那就将配置项的key值改为你当前JDK版本中对应的key值即可

详见源码:ProxyGenerator类的属性域saveGeneratedFiles