诚然,java并不是一门十分死板的语言,反射的功能给开发者们带来了不少的便利(虽然反过来也对代码安全性本身有影响),最近在网上逛的时候意外发现了java除了能进行一般的反射调用外,也支持对接口对象方法的扩展,这种技术官方就叫“动态代理”。虽然这东西已经是很久以前的技术了,但我居然到最近才了解到,惭愧。
所谓的动态代理,说白了一点,就是通过自定义的一个代理类,对原对象的接口方法进行扩展,以达到在运行时执行方法的过程中对定义好的流程进行一点干预。以下是一个简单的例子。
定义一个接口1
2
3public interface Flyable {
void fly();
}
定义一个实现该接口的类1
2
3
4
5
6
7public class Bird implements Flyable {
public void fly() {
System.out.println("I am Flying");
}
}
再实现一个代理类
1 | public class FlyProxy implements InvocationHandler { |
这个类需要实现InvocationHandler接口,还需要把需要代理的类实例以参数的形式传进来,以在触发接口方法的时候不仅能调用到干预的代码,也能执行原对象的代码。
最后是一个简单的测试类
1 | public class ProxyTest { |
最后执行的结果就是
1 | I am Flying |
容易看到,同样是执行fly方法,但不仅把bird实例的fly执行了,还执行了代理类中的两行输出。
开始的时候我只是认为只是一个简单对bird对象再进行了一次封装,然后在实际调用的时候通过调用另一个代理对象来实现扩展。但是,Proxy.newProxyInstance创建的对象同样是Flyable类型的,jvm创建的这个这个类的时候怎么知道需要创建的代理类的类型呢,接口又不能直接创建实例。整个过程中比较关键的地方有两点,一个就是需要有一个实现了InvocationHandler接口的类,这个理解没难度;另一个就是Proxy.newProxyInstance是怎么把代理类给创建出来的。
对于java而言,创建一个实例,最重要的是找到其对应的class,在但这里很明显是没有这个类的(有也只是代理类Proxy,一个通用的类),先来看一下Proxy.newProxyInstance的实现(以下代码刨去了安全检查相关的):
1 | public static Object newProxyInstance(ClassLoader loader, |
参数interfaces就是从bird.getClass().getInterfaces()中来的,指的是bird这个类实现的接口类,loader即为bird类的类加载器。显然,此处是通过getProxyClass0来获取,再深看
1 | private static Class<?> getProxyClass0(ClassLoader loader, |
getProxyClass0就是通过proxyClassCache这个对象来获取对应的类的。proxyClassCache的定义是
1 | private static final WeakCache<ClassLoader, Class<?>[], Class<?>> |
从名字上可以推断,它是一个带自动生成键/值工厂类的键值对。KeyFactory和ProxyClassFactory都是实现接口BiFunction的类.
来到WeakCache的get方法内部
1 | public V get(K key, P parameter) { |
WeakCache的结构简述如下表,嵌套关系为从左往右,简单来说WeakCache就是一个带有自动生成任意对象为键值对的缓存(虚引用)
整个过程简单来说就是通过在valuesMap中查找到合适的supplier,然后不断地在supplier中获取Class类型的值(此处为泛型V)。此处重点讲在没有缓存情况下的加载过程。
第一次加载接口类,使用keySub获取到的supplier中是肯定找不到对于的代理类缓存的,这时候factory也为空,于是就需要创建一个新的Factory类,用于作为下一次查找的supplier。Factory同样也是一个Supplier类型的,着重看一下它的get方法实现
1 | private final class Factory implements Supplier<V> { |
前面都是一些检查之类的,关键就在于//create new value的地方,假如说现在要加载的代理类没有缓存,那么就需要通过valueFactory.apply(key,parameter)来创建,其中valueFactory就是proxyClassCache时创建的ProxyClassFactory类对象。查看ProxyClassFactory对象的apply实现,如下
1 | public Class<?> apply(ClassLoader loader, Class<?>[] interfaces) { |
流程图如下:
假如接口类是私有的受保护的,那么如果直接在生成的类中使用别的包里的接口的话会失败的,所以才会有这么一个区分。
部分解释在上述注释上体现,检查之类的不细说,对于一个代理类来说,它的生成规则如下(假定原接口公开):
1 | 1\. 包名为ReflectUtil.PROXY_PACKAGE = "com.sun.proxy" |
如上述例子中,本次运行生成的全限定名为com.sun.proxy.$Proxy0,然后再根据这个代理类的类名去加载,加载的过程是在ProxyGenerator中执行的,也是这次动态代理中最重要的一部分,ProxyGenreator对象通过方法生成了一串byte,先看源码
1 | private byte[] generateClassFile() { |
以上就是生成一个class文件的二进制数据,具体是什么东西呢,我中断了它把byte[]的内容输出到了一个文件,实际上这就是一个类编译后的二进制文件。接着使用jd-gui打开(此处有一个技巧,需要把文件把到去项目工程下的bin的目录下,否则容易打不开),显示以下内容
1 | package com.sun.proxy; |
可以看到,这个$Proxy0.class其实就是一个继承Proxy,实现Flyable接口的类。然后再调用native方法去把这个类加载起来。
1 | defineClass0(loader, proxyName,proxyClassFile, 0, proxyClassFile.length); |
在加载类的时候,有四个Method对象需要被赋值,。1
2
3
4m0 -> Object.hashCode
m1 -> Object.equals
m2 -> Object.toString
m3 -> Flyable.fly
如此,把类加载起来后,就可以回到Proxy的newProxyInstance中继续了。余下的操作就跟普通反射类似了,获取到构造方法,创建$Proxy0实例,最终返回给我们应用的就是一个创建好的$Proxy0实例,而且它是实现了Flyable接口的。这样我们才可以直接进行类型转换,并调用其实的方法的。
在$Proxy0的fly方法中,调用的是
1 | this.h.invoke(this, m3, null); |
此处的h就是指Proxy中的h成员变量
1 | /** |
它是在创建 $Proxy0实例时从构造方法中传入的,事实上就是我们在newProxyInstance时传入的FlyProxy,于是,调用fly方法的时候,FlyProxy的invoke方法就被触发了,剩下的也不需要我说了。
==============================================================
纵观整个过程,其实动态代理最重要的一点技术就是类的动态生成与加载,这一特性的出现并非有心故意弄出来的,而是基于Java本身语言的特性设立的。对于jvm而言,它认为这是不是一个类,并不是看你是不是真的创建好一个实在的文件等它去读才可以,它认识的只是编译好的二进制流,在java开发中,找类这一过程实际上是开放的,无论是从jar中,还是android的dex中,实际上都是寻找class的二进制数据,喂给它规范的二进制数据,它才不管你从哪里来。通过临时创建一个实际上并不存在的类,用来桥接我们的调用,这就是代理的作用,说明白了,其实也就那么点事。