0%

JDK 动态代理关于异常的处理

最近在做接口重试的需求,技术选型是 guava-retrying,这个重试框架自定义程度高,可以自己定义重试的触发条件。业务上重试的都是 Dubbo 的接口,但这些接口会抛出哪些异常是不清楚的。异常的种类主要有 Error、RunTimeException、Exception 这三大类,其中前两种不需要显示的处理的,而后者需要显示的捕获或者抛出,业务方不希望感受到受检异常,因此得确保 Dubbo 接口不会抛出受检异常。Dubbo 生成代理的方式使用的是 Javassist,该框架以前也没有使用过,所以先来看看 JDK 生成的代理类是如果处理异常的。

保存字节码文件

1.准备测试代码

下面列出用来测试的代码,主要有三个类,接口、实现类以及入口类。

接口类:

1
2
3
4
5
public interface UserInterface {

String getUserName();

}

实现类:

1
2
3
4
5
6
7
public class UserImpl implements UserInterface {

@Override
public String getUserName() {
throw new IllegalArgumentException("参数异常");
}
}

入口类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class MyMain {

public static void main(String[] args) {
MyInvocationHandler myInvocationHandler = new MyInvocationHandler(new UserImpl());
UserInterface userInterface = (UserInterface) Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader()
, new Class[]{UserInterface.class}
, myInvocationHandler);
String userName = userInterface.getUserName();
System.out.println(userName);
while (true) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

2.启动 HSDB

HSDB 是一个 HotSpot VM 的调试器,在此之前保存 JDK 动态代理类的字节码都是通过写代码的方式存储的,不是很方便,后来通过查询才得知有这样一个 JVM 调试利器,下面会简要介绍一下如何使用该工具。

该工具主要在 sa-jdi.jar 这个 jar 包中,我的开发环境是 macOS,下面列出我的环境 jar 包所在的路径:

1
/Library/Java/JavaVirtualMachines/jdk1.8.0_221.jdk/Contents/Home/lib/sa-jdi.jar

打开控制台输入下面的命令就会看到启动的 GUI 界面:

1
sudo java -cp /Library/Java/JavaVirtualMachines/jdk1.8.0_221.jdk/Contents/Home/lib/sa-jdi.jar sun.jvm.hotspot.HSDB

上述命令注意要使用 sudo ,否则后面会 attach 不到 JVM 进程的 ID。至于 Windows 平台,网上说要使用 CMD ,不要使用 PowerShell,没有测试,如果出现问题可以自行尝试。此外也要注意一下当前控制台所在的路径,因为字节码的保存路径是在命令行启动的路径下面。

HSDB 启动后的界面:

在上述输入框中输入应用程序的进程 ID,然后就可以使用 Tools 中的 Class Browser 功能了:

搜索并保存代理类:

控制台所在的路径查看字节码文件:

使用 IDEA 查看字节码文件:

分析字节码文件

通过上述的操作,来看看 JDK 动态代理生成的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package com.sun.proxy;

import cn.itgrocery.proxy.exception.UserInterface;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;

public final class $Proxy0 extends Proxy implements UserInterface {
private static Method m1;
private static Method m2;
private static Method m3;
private static Method m0;

public $Proxy0(InvocationHandler var1) {
super(var1);
}

static {
try {
m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
m2 = Class.forName("java.lang.Object").getMethod("toString");
m3 = Class.forName("cn.itgrocery.proxy.exception.UserInterface").getMethod("getUserName");
m0 = Class.forName("java.lang.Object").getMethod("hashCode");
} catch (NoSuchMethodException var2) {
throw new NoSuchMethodError(var2.getMessage());
} catch (ClassNotFoundException var3) {
throw new NoClassDefFoundError(var3.getMessage());
}
}

public final boolean equals(Object var1) {
try {
return (Boolean)super.h.invoke(this, m1, new Object[]{var1});
} catch (RuntimeException | Error var3) {
throw var3;
} catch (Throwable var4) {
throw new UndeclaredThrowableException(var4);
}
}

public final String toString() {
try {
return (String)super.h.invoke(this, m2, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}

public final int hashCode() {
try {
return (Integer)super.h.invoke(this, m0, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}

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

可以看到 getUserName 当中处理的主要有三类异常,一类是 RuntimeException、Error,这一类异常是不需要显示处理,可以直接往外抛,除此之外的其它异常都会被包装成 UndeclaredThrowableException,而 UndeclaredThrowableException 是 RuntimeException 的子类的,所以直接往外抛也是没有问题的。当然上述的例子是我们要代理的方法没有声明要抛出的异常,所以对于受检异常 JDK 代理类只能进行包装,其它形式的接口可以按照上述方式查看源代码。

如果觉得我的文章对您有用,请查看广告(Google Ads)或扫码。您的支持将鼓励我继续创作!