从设计上理解JDK动态代理

作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO

联系qq:184480602,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬

照理说,动态代理经过前面3篇介绍,该讲的都已经讲完了,再深入下去的意义不是特别大。但看到群里有小伙伴说对InvocationHandler#invoke()方法的参数有些困惑,所以又补了一篇。

关于这三个参数,其实一句话就能讲完:

  • Object proxy:很遗憾,是代理对象本身,而不是目标对象(不要调用,会无限递归,一般不会使用)
  • Method method:方法执行器,用来执行方法(有点不好解释,Method只是一个执行器,传入目标对象就执行目标对象的方法)
  • Obeject[] args:方法参数

上一篇也是这么介绍的,但大家的接受度似乎不是很好,甚至对参数怎么传进来的感到困惑,所以本篇打算站在Proxy类设计的角度分析三个参数的由来。

当然啦,我还远不敢说能写出JDK级别的代码。本文虽然也尝试编写MyProxy类,但它是用来解释参数由来的,意义并不在于完美复刻JDK Proxy。

山寨Proxy类概览

先看一下我编写的山寨MyProxy和MyInvocationHandler吧:

是不是和上面原版的JDK Proxy几乎一模一样呢?激动吗?一起来看看我是怎么设计的(不要细想代码逻辑,这根本是伪代码,跑不起来的)。

/**
 * 山寨Proxy类
 */
public static class MyProxy implements java.io.Serializable {

    protected MyInvocationHandler h;

    private MyProxy() {
    }

    protected MyProxy(MyInvocationHandler h) {
        Objects.requireNonNull(h);
        this.h = h;
    }

    public static Object newProxyInstance(ClassLoader classLoader,
                                          Class<?>[] interfaces,
                                          MyInvocationHandler h) throws Exception {
        // 拷贝一份接口Class(接口可能有多个,所以拷贝的Class也有多个)
        final Class<?>[] interfaceCls = interfaces.clone();
        // 这里简化处理,只取第一个
        Class<?> copyClazzOfInterface = interfaceCls[0];
        // 获取Proxy带InvocationHandler参数的那个有参构造器
        Constructor<?> constructor = copyClazzOfInterface.getConstructor(MyInvocationHandler.class);
        // 创建一个Proxy代理对象,并把InvocationHandler塞到代理对象内部,返回代理对象
        return constructor.newInstance(h);
    }

}

/**
 * 山寨InvocationHandler接口
 */
interface MyInvocationHandler {
    Object invoke(Object proxy, Method method, Object[] args) throws Throwable;
}

也就是说,上面的设计思路就是之前分析的这张图:

目前上面的MyProxy有两个问题没解决:

  • 返回的代理对象只是Proxy类型的,没法强转为目标接口类型
  • 返回的代理对象即使能调用接口的同名方法,如何最终调用到它内部的InvocationHandler#invoke()呢

底层原理

上面遗留的两个问题,其实换种说法就是:

  • 怎么让MyProxy的实例对象变成代理类的对象呢(比如Calculator)?
  • InvocationHandler#invoke()怎么调用到目标对象同名方法?

首先我们要明确,MyProxy(JDK Proxy同理)是一个已经写好的类,一开始就没有实现Calculator接口,那么它的实例对象肯定是无法强转为Calculator的。那么,Java是如何解决这个问题的呢?方式很简单粗暴,因为JVM确确实实在运行时动态构造了代理类,并让代理类实现了接口,也就是我们经常看到的$Proxy0。

也就是说,我们通常理解的代理对象,并不是JDK Proxy的直接实例对象,而是JDK Proxy的子类$Proxy0的实例对象,而$Proxy0 extends Proxy implements Calculator

由于$Proxy0是运行时的产物,一旦程序停止便会消失,我们需要借助阿里开源的Arthas工具来观察并验证。

假设需要代理的接口是:

/**
 * 需要代理的接口
 */
interface Calculator {
    int add(int a, int b);
}

当我们期望使用Proxy创建代理对象时,JDK会先动态生成一个代理类$Proxy0:

final class $Proxy0 extends Proxy implements InvocationHandlerTest.Calculator {
    private static Method m1;
    private static Method m2;
    private static Method m3;
    private static Method m0;

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

    static {
        try {
            m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
            m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]);
            m3 = Class.forName("com.bravo.demo.InvocationHandlerTest$Calculator").getMethod("add", Integer.TYPE, Integer.TYPE);
            m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]);
            return;
        }
        catch (NoSuchMethodException noSuchMethodException) {
            throw new NoSuchMethodError(noSuchMethodException.getMessage());
        }
        catch (ClassNotFoundException classNotFoundException) {
            throw new NoClassDefFoundError(classNotFoundException.getMessage());
        }
    }

    public final int add(int n, int n2) {
        try {
            return (Integer)this.h.invoke(this, m3, new Object[]{n, n2});
        }
        catch (Error | RuntimeException throwable) {
            throw throwable;
        }
        catch (Throwable throwable) {
            throw new UndeclaredThrowableException(throwable);
        }
    }

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

    public final String toString() {
        try {
            return (String)this.h.invoke(this, m2, null);
        }
        catch (Error | RuntimeException throwable) {
            throw throwable;
        }
        catch (Throwable throwable) {
            throw new UndeclaredThrowableException(throwable);
        }
    }

    public final int hashCode() {
        try {
            return (Integer)this.h.invoke(this, m0, null);
        }
        catch (Error | RuntimeException throwable) {
            throw throwable;
        }
        catch (Throwable throwable) {
            throw new UndeclaredThrowableException(throwable);
        }
    }
}

简化无关代码:

// 1.自动实现目标接口,所以代理对象可以转成Calculator
final class $Proxy0 extends Proxy implements InvocationHandlerTest.Calculator {
    private static Method m3;

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

    static {
        // 2.获取目标方法Method
        m3 = Class.forName("com.bravo.demo.InvocationHandlerTest$Calculator").getMethod("add", Integer.TYPE, Integer.TYPE);
    }

    public final int add(int n, int n2) {
        // 3.通过InvocationHandler执行方法,现在你能理解invoke()三个参数的含义了吗?
        //   this:就是$Proxy0的实例,所以是代理对象,不是目标对象
        return (Integer)this.h.invoke(this, m3, new Object[]{n, n2});
    }

}

最后一个问题是,代理对象$proxy调用add()时,是如何最终调用到目标对象的add()方法的呢?观察上面的代码可以发现,代理对象的方法调用都是通过this.h.invoke()桥接过去的,而这个h就是InvocationHandler,在$Proxy的父类Proxy中已经存在,而且会被赋值。

我编写的MyProxy基本上就是简化版的JDK Proxy,没有本质的区别。只不过JVM只认识JDK Proxy,只会给它生成动态代理类,所以我的MyProxy即使模仿到99.99%,也注定少了最关键的那一步,最终沦为一段玩具代码。

希望这篇文章能帮大家更了解JDK动态代理。至于Java是如何自动生成$Proxy代理类的,交给大家另外研究。很多读者让我讲讲CGLib,其实没啥好讲的,就是API使用而已,底层也是自动生成代理类/代理对象,和JDK动态代理很相似,只不过CGLib底层用的是ASM,感兴趣可以去百度一下。

作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO

进群,大家一起学习,一起进步,一起对抗互联网寒冬

  • 23
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值