上一个推文我们解答了 JDK 动态代理深度问题之一的《问:为什么 JDK 的动态代理只能使用接口?》。这一篇我们继续解答关于 JDK 动态代理的另一个深度问题:JDK 动态代理调用自己接口方法时是怎么做到先调用 invoke 方法的?
案例重现问题
我们平时使用动态代理最典型的样例如下:
// 接口定义
public interface Foo {
void add();
}
InvocationHandler handler = new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args)throws Throwable {
//动态代理增强实现
}
}
Foo f = (Foo) Proxy.newProxyInstance(Foo.class.getClassLoader(), new Class>[] { Foo.class }, handler);
//调用接口方法
f.add();
你有没有想过,通过上面代码,明明最后我调用的是接口 Foo 的 add 方法f.add();
,为什么就自动触发了 InvocationHandler 接口的invoke
方法呢?
要回答这个问题,我们还需要对上篇推文问题进行一个深挖。下面会从源码分析和实例验证(别走开,这是一个冷门骚技能操作)两个角度给出解释。
寻找真相
JDK 动态代理的代理类是运行时通过字节码生成的,我们通过Proxy.newProxyInstance
方法获取的接口实现类就是这个字节码生成的代理类,所以要想搞明白问题本质就得从这个生成的类下手,即怎么获得这个运行时生成类的字节码文件。
通过源码可以发现,这个类是通过ProxyGenerator.generateProxyClass
获取 byte 字节流然后被加载的,所以从这个角度看我们似乎是没法在磁盘拿到这个代理类的字节码。但是我们可以模拟,然后存盘分析,通过之前源码分析可以通过如下模拟将代理类字节码存盘:
interface Foo {
void add();
}
class FooImpl implements Foo {
@Override
public void add() {
System.out.println("FooImpl#add");
}
}
public class JavaDynamicProxyTest {
// 模拟 ProxyGenerator.generateProxyClass 逻辑生成代理类字节码并存盘
public static void main(String[] args) throws Exception {
FileOutputStream out = null;
try {
byte[] classFile = ProxyGenerator.generateProxyClass("$Proxy0",
FooImpl.class.getInterfaces());
out = new FileOutputStream("$Proxy0.class");
out.write(classFile);
} finally {
if (out != null) {
out.flush();
out.close();
}
}
}
}
运行上面代码得到$Proxy0.class
动态代理字节码,内容如下:
// (powered by Fernflower decompiler)
import cn.yan.test.controller.cn.yan.test.Foo;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;
// JDK 动态代理时生成的动态代理类字节码内容
// 很明显继承了 Proxy 类,同时实现了我们传递的 Foo 接口(多个的话这里也就是多个)
public final class $Proxy0 extends Proxy implements Foo {
private static Method m1;
private static Method m2;
private static Method m3;
private static Method m0;
// 构造方法传递 InvocationHandler 接口实例,本质在 Proxy 中接收
public $Proxy0(InvocationHandler var1) throws {
super(var1);
}
// super.h 就是基类 Proxy 中的 InvocationHandler 成员,也就是这里构造方法赋值的实例
public final boolean equals(Object var1) throws {
try {
// 直接调用 InvocationHandler 接口的 invoke 方法
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() throws {
//忽略,雷同,直接调用 InvocationHandler 接口的 invoke 方法
......
}
public final int hashCode() throws {
//忽略,雷同,直接调用 InvocationHandler 接口的 invoke 方法
......
}
// 重点!!!!!
// 我们接口 Foo 的方法被它通过字节码生成了,描述完全一致,只是实现变了
public final void add() throws {
try {
// 直接调用 InvocationHandler 接口的 invoke 方法
super.h.invoke(this, m3, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
// 初始化方法列表
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.yan.test.controller.cn.yan.test.Foo").getMethod("add");
m0 = Class.forName("java.lang.Object").getMethod("hashCode");
} catch (NoSuchMethodException var2) {
throw new NoSuchMethodError(var2.getMessage());
} catch (ClassNotFoundException var3) {
throw new NoClassDefFoundError(var3.getMessage());
}
}
}
渐渐接近真相了,JDK 生成的动态代理类字节码中默认实现了 toString、equals、hashCode 三个方法的 InvocationHandler 接口转调,同时新增加了自己代理接口的方法,也同样通过 InvocationHandler 接口进行 invoke 转调实现。
还记不记得源码中动态代理这个方法的实现,如下:
public static Object newProxyInstance(ClassLoader loader,
Class>[] interfaces,
InvocationHandler h)throws IllegalArgumentException{
......
// 获取通过 ProxyGenerator.generateProxyClass 生成的字节码类 class 对象
// 上例子中也就是 $Proxy0 类
Class> cl = getProxyClass0(loader, intfs);
try {
......
final Constructor> cons = cl.getConstructor(constructorParams);
final InvocationHandler ih = h;
......
// 实例化 $Proxy0 类,同时参数传递 InvocationHandler 接口
// 此时完美的让 $Proxy0 类持有了 InvocationHandler 接口实现
return cons.newInstance(new Object[]{h});
} catch (IllegalAccessException|InstantiationException e) {
......
}
}
因此绕一圈就回到了问题本质,上面就完美的解释了 JDK 动态代理调用自己接口方法时是怎么做到先调用 invoke 方法的。
到此结题,扫描下面微信加个好友呗,朋友圈更精彩。小编 朋友圈技术更精彩>
▼往期精彩回顾▼问:为什么 JDK 的动态代理只能使用接口?Android 主线程崩溃与子线程崩溃有什么本质区别?你是怎么处理的?点击左下角阅读原文查看历史经典技术问题汇总,看完顺手一键三连呀~