方法调用_问:JDK 动态代理调用接口方法时是怎么做到先调用 invoke 方法的?

本文探讨了JDK动态代理在调用接口方法时如何先调用InvocationHandler的invoke方法。通过案例重现问题和寻找真相,解析了动态代理的字节码生成过程,揭示了接口方法调用的内在逻辑。
摘要由CSDN通过智能技术生成

上一个推文我们解答了 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 方法的。

到此结题,扫描下面微信加个好友呗,朋友圈更精彩。

3b083469159484405796d2ca539301a7.png

小编 朋友圈技术更精彩>

f399b196bdcc3899d164c1f7305e19c1.gif▼往期精彩回顾▼问:为什么 JDK 的动态代理只能使用接口?Android 主线程崩溃与子线程崩溃有什么本质区别?你是怎么处理的?

48e11f3d984d1e4f931cf20b216ff977.png点击左下角阅读原文查看历史经典技术问题汇总,看完顺手一键三连呀~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值