从生成的代理类看动态代理机制

引言

上一篇文章我们分析了InvocationHandler的注释,并从注释中总结出了一些问题。到现在为止,我们只是知道使用动态代理可能出现哪些问题,对于动态代理的底层实现机制并不清楚,看完这篇文章,相信你会对动态代理有个更加深刻的理解,我们之前发现的那些问题和疑惑,也能得到正确的解答。

最终结果

按照逻辑,这个时候我们应该分析Proxy类的源码,但是源码分析总是枯燥乏味的,如果没有一个可以直接展示动态代理机制的生成物,我们肯定要去分析源码。幸运的是,动态代理中,运行时为我们偷偷生成了一个动态代理类,通过分析这个类的特性,我们就能轻松掌握动态代理的精髓。

我在这里先把这个动态代理类展示出来。

生成的代理类

在main方法中加入这行代码:

arduino 复制代码
System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles","true");

就能在当前工程的com.sun.proxy目录下生成一个名为$Proxy0.class文件,这是我的工程目录:

直接使用IDEA打开这个文件或者使用JD-GUI进行反编译,可以看到类的内容如下(为了之后的详细描述,我把整个类的代码都展示了出来):

java 复制代码
public final class $Proxy0 extends Proxy implements IHello {
    private static Method m1;
    private static Method m3;
    private static Method m5;
    private static Method m4;
    private static Method m2;
    private static Method m0;

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

    public final boolean equals(Object var1) throws  {
        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 void sayHello() throws  {
        try {
            super.h.invoke(this, m3, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

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

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

    public final String toString() throws  {
        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() throws  {
        try {
            return (Integer)super.h.invoke(this, m0, (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"));
            m3 = Class.forName("person.andy.concurrency.proxy.IHello").getMethod("sayHello");
            m5 = Class.forName("person.andy.concurrency.proxy.IHello").getMethod("returnAString");
            m4 = Class.forName("person.andy.concurrency.proxy.IHello").getMethod("returnAInt");
            m2 = Class.forName("java.lang.Object").getMethod("toString");
            m0 = Class.forName("java.lang.Object").getMethod("hashCode");
        } catch (NoSuchMethodException var2) {
            throw new NoSuchMethodError(var2.getMessage());
        } catch (ClassNotFoundException var3) {
            throw new NoClassDefFoundError(var3.getMessage());
        }
    }
}

这个类就是之前注释里出现了很多次的代理类。当我们使用动态代理,或者更具体的说,调用Proxy.newProxyInstance方法的时候,虚拟机就会自动为我们生成一个这样的类,接下来我们就分析一下这个类。

代理类的特性

我们看到这个代理类的第一印象,应该就是继承了Proxy类和实现了IHello接口。

那我们瞬间就明白了,为什么jdk的动态代理要求被增强的类必须实现某个接口,因为代理类本身已经继承了Proxy类,要想对类方法进行增强,只能去实现被增强的类实现的接口,也就是这里的IHello。

再看构造方法,是一个参数为InvocationHandler的构造方法,这个方法也是父类Proxy的构造方法,通过这个构造方法,就把动态代理中两个最重要的类,Proxy和InvocationHandler联系起来了。

既然实现了IHello接口,那么必然就要对接口方法进行实现,那么代理类是怎么实现的呢?我们看到这个类中声明了几个Method方法并在静态块中进行了初始化,而这几个方法里面就有IHello中需要实现的几个方法。除了接口中的方法,还有Object的几个方法。

ini 复制代码
static {
        try {
            m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
            m3 = Class.forName("person.andy.concurrency.proxy.IHello").getMethod("sayHello");
            m5 = Class.forName("person.andy.concurrency.proxy.IHello").getMethod("returnAString");
            m4 = Class.forName("person.andy.concurrency.proxy.IHello").getMethod("returnAInt");
            m2 = Class.forName("java.lang.Object").getMethod("toString");
            m0 = Class.forName("java.lang.Object").getMethod("hashCode");
        } catch (NoSuchMethodException var2) {
            throw new NoSuchMethodError(var2.getMessage());
        } catch (ClassNotFoundException var3) {
            throw new NoClassDefFoundError(var3.getMessage());
        }
    }

那这几个方法是怎么实现的呢?

还记得InvocationHandler注释中写的吗,proxy实例的方法调用都会被转发到与它相关的InvocationHandler上面来,由调用invoke方法来实现。我们拿带有返回值的returnAString()方法来举例:

typescript 复制代码
public final String returnAString() throws  {
        try {
            return (String)super.h.invoke(this, m5, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

它果然调用了自己的InvocationHandler的invoke方法,并且第一个参数代理实例传入的是this,第二个方法实例传入的就是在静态块里初始化的IHello的returnAString方法,由于方法执行没有参数,所以第三个参数为null;

看到这里,我们就能理解之前类型兼容的问题:

invoke方法和接口中声明的方法的返回值类型兼容问题。因为代理类本质上实现了接口,需要实现接口方法,而实现接口方法的方式就是调用InvocationHandler的invoke方法,所以必然需要invoke方法的返回值类型与接口方法的返回值类型一致,并且由于代理类的每个需要实现的接口方法(在这里是sayHello和returnAString)都需要调用invoke方法,所以一般的写法就是

arduino 复制代码
return method.invoke(originalObj, args);

来让这两个返回值保持一致,所以invoke方法适合对方法做增强之后将原本的方法调用返回值返回。

那我们应该就能描述出动态代理是怎么实现的了:

(1)我们想要对某个类(Hello)的方法进行增强,这个类已经实现了某个接口(IHello)。

(2)为了实现增强的目的,我们定义了InvocationHandler接口,这个接口的invoke方法可以完成需要增强的逻辑,这里就是:

csharp 复制代码
System.out.println("welcome");

增强之后还能接着执行被增强的类的方法,也就是

arduino 复制代码
method.invoke(originalObj, args);

我们唯一需要做的就是将被增强的类与InvocationHandler绑定起来。所以说,我们的InvocationHandler决定了对哪个类增强以及如何增强。

(3)解决了怎么增强方法的问题,就需要解决怎么进行方法调用转发的问题。解决的方法就是创建代理类$Proxy0。我们让这个类实现被增强的类(Hello)的所有接口,这样就需要实现所有的接口方法了,然后代理类中有InvocationHandler作为成员变量,所有的接口方法实现都调用这个InvocationHandler的invoke方法,这样就完成了方法调用的转发。

(4)在整个实现中,InvocationHandler决定增强哪个类以及怎样增强这个类,proxy负责方法调用的转发,动态 代理的动态性就在这里,如果我想代理其他的类,我们只需要在bind方法中传入需要代理的类就行了,Proxy.newProxyInstance会自动为我们生成正确的代理类。如果你不能理解的话,请看下面的例子:

我新创建了一个ISell接口和一个实现类Sell:

csharp 复制代码
public interface ISell {
    void sell();
}
typescript 复制代码
public class Sell implements ISell {
    @Override
    public void sell() {
        System.out.println("i am a seller");
    }
}

怎么对这个Sell进行代理呢,很简单:

scss 复制代码
public static void main(String[] args) {
        System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles","true");
        IHello iHello = (IHello) new DynamicProxy().bind(new Hello());
        iHello.sayHello();
        ISell iSell = (ISell) new DynamicProxy().bind(new Sell());
        iSell.sell();
    }

你不需要修改DynamicProxy的实现,只需要新创建DynamicProxy,然后绑定我们的Sell就可以了。

几个细节

invoke方法中使用动态代理实例的问题

因为invoke的第一个参数我们传入的是this,也就是当前代理类的实例,所以如果我们将invoke方法从:

typescript 复制代码
@Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("welcome");
        return method.invoke(originalObj, args);
    }

改成

typescript 复制代码
@Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("welcome");
        return method.invoke(proxy, args);
    }

注意变化,method.invoke的第一个参数从我们增强的Hello对象变成了代理类对象,就会发生很严重的问题,以sayHello方法来说,代理对象的sayHello方法是通过调用invoke方法实现的,而invoke方法由调用了代理对象的sayHello方法,这样就会无限循环调用下去,最后的结果就是报错。

toString方法的问题

我们看到,在代理类里面,不仅接口的方法通过invoke方法来实现,object类的几个方法,toString、equals和hashCode方法也是通过invoke方法来实现的,我们重点看toString方法:

typescript 复制代码
public final String toString() throws  {
        try {
            return (String)super.h.invoke(this, m2, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }
m2 = Class.forName("java.lang.Object").getMethod("toString");

这个m2是Object类的方法,也就是说传给invoke方法的method参数是Object的toString方法。

我们在invoke方法中增加一条输出,输出第一个参数:

kotlin 复制代码
@Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println(proxy);
        System.out.println("welcome");
        return method.invoke(originalObj, args);
    }

想一下过程,我们使用System.out.println方法输出某个对象时,会调用对象的toString方法,而proxy的toString方法就是上面代理类中的调用invoke的那个toString方法,也就是会调用invoke方法,然后又输出proxy,继续调用toString方法,一直循环调用下去,直到栈溢出。

运行的结果也是这样:

php 复制代码
Exception in thread "main" java.lang.StackOverflowError
	at person.andy.concurrency.proxy.DynamicProxy.invoke(DynamicProxy.java:16)
	at com.sun.proxy.$Proxy0.toString(Unknown Source)
	at java.lang.String.valueOf(String.java:2994)
	at java.io.PrintStream.println(PrintStream.java:821)
	at person.andy.concurrency.proxy.DynamicProxy.invoke(DynamicProxy.java:16)
	at com.sun.proxy.$Proxy0.toString(Unknown Source)
	at java.lang.String.valueOf(String.java:2994)
	at java.io.PrintStream.println(PrintStream.java:821)
	at person.andy.concurrency.proxy.DynamicProxy.invoke(DynamicProxy.java:16)
	at com.sun.proxy.$Proxy0.toString(Unknown Source)

这可以看成是jdk动态代理的一个小问题。

小结

这篇文章没有分析Proxy的源码,而是直接通过动态生成的代理类讲解了使用动态代理的时候运行时为我们做了哪些工作。相信读到这里,动态代理的实现机制已经相当明了了。

如果不去深挖动态代理类到底是怎么创建的,jdk动态代理这个主题完全可以到此为止。不过,了解运行时怎么为我们生成这个动态代理类,对于理解字节码生成技术很有帮助。从下一篇文章开始,我们就开始介绍动态代理技术中代理类的创建过程。

相关推荐
m0_5719575843 分钟前
Java | Leetcode Java题解之第543题二叉树的直径
java·leetcode·题解
魔道不误砍柴功3 小时前
Java 中如何巧妙应用 Function 让方法复用性更强
java·开发语言·python
NiNg_1_2343 小时前
SpringBoot整合SpringSecurity实现密码加密解密、登录认证退出功能
java·spring boot·后端
闲晨3 小时前
C++ 继承:代码传承的魔法棒,开启奇幻编程之旅
java·c语言·开发语言·c++·经验分享
测开小菜鸟4 小时前
使用python向钉钉群聊发送消息
java·python·钉钉
P.H. Infinity5 小时前
【RabbitMQ】04-发送者可靠性
java·rabbitmq·java-rabbitmq
生命几十年3万天5 小时前
java的threadlocal为何内存泄漏
java
caridle6 小时前
教程:使用 InterBase Express 访问数据库(五):TIBTransaction
java·数据库·express
^velpro^6 小时前
数据库连接池的创建
java·开发语言·数据库
苹果醋36 小时前
Java8->Java19的初步探索
java·运维·spring boot·mysql·nginx