代理模式实现方式介绍
什么是代理模式
日常生活中我们经常会碰到代理模式,例如我们找房产中介帮我们找房子,找婚姻中介帮我们介绍对象,找保洁帮我们打理房间等。在无形中运用到了代理模式。
为什么要使用代理?
运用代理可以使我们的生活更加便利,有了代理,我们不需要自己去找房子,不需要自己去找对象,不需要自己去打理房间。当然,你也可以选择一切都自己来干,但是存在前提条件,一是你是否都具备这样的资源和能力来做这些事情,二是你是否愿意花费这么多精力和时间来做这些事情。总之,代理模式使我们各专其事,我们可以将时间消耗在美好的事情上,而不用天天被一些琐事所羁绊。
代理模式有哪些实现?
Java中的代理有静态代理和动态代理,下面分别用一个简单的例子来介绍一下静态代理和动态代理代码实现。
静态代理
代理接口
1 | package main.java.com.study.designPatterns.proxy.staticProxy; |
目标对象
1 | package main.java.com.study.designPatterns.proxy.staticProxy; |
代理对象
1 | package main.java.com.study.designPatterns.proxy.staticProxy; |
测试调用
1 | package main.java.com.study.designPatterns.proxy.staticProxy; |
测试结果
1 | 开启事务控制... |
总结
静态代理实现简单也容易理解,但是静态代理不能使一个代理类反复作用于多个目标对象,代理对象直接持有目标对象的引用,这导致代理对象和目标对象类型紧密耦合了在一起。如果UserDao接口下还有另一个实现类也需要进行事务控制,那么就要重新写一个代理类,这样就会产生许多重复的模版代码,不能达到代码复用的目的。而动态代理就可以很好的解决这样的问题。
动态代理
代理接口
1 | package main.java.com.study.designPatterns.proxy.JdkProxy; |
目标对象
1 | package main.java.com.study.designPatterns.proxy.JdkProxy; |
代理对象
1 | package main.java.com.study.designPatterns.proxy.JdkProxy; |
测试类
1 | package main.java.com.study.designPatterns.proxy.JdkProxy; |
测试结果
1 | 开启事务控制... |
总结
之前发现静态代理会产生许多重复代码,不能很好的进行代码复用,而动态代理能够很好的解决这个问题,代理类TransactionHandler实现了InvocationHandler接口,并且它持有的目标对象类型是Object,因此事务控制代理类TransactionHandler能够代理任意的对象,为任意的对象添加事务控制的逻辑。因此动态代理才真正的将代码中横向切面的逻辑剥离了出来,起到代码复用的目的。
但是动态代理也有缺点,一是它的实现比静态代理更加复杂也不好理解;二是它存在一定的限制,例如它要求需要代理的对象必须实现了某个接口;三是它不够灵活,动态代理会为接口中的声明的所有方法添加上相同的代理逻辑。
JDK动态代理的底层实现Proxy分析
在动态代理的测试类TestJdkProxy
中使用了Proxy
类的静态方法newProxyInstance
方法去生成一个代理类,这个静态方法接收三个参数,分别是目标类的类加载器,目标类实现的接口集合,InvocationHandler
实例,最后返回一个Object
类型的代理类。先从该方法开始,看看代理类是怎样一步一步造出来的。
newProxyInstance方法
1 | /** |
newProxyInstance方法首先是对参数进行一些权限校验,之后通过调用getProxyClass0方法生成了代理类的类对象,然后获取参数类型是InvocationHandler.class的代理类构造器。检验构造器是否可以访问,最后传入InvocationHandler实例的引用去构造出一个代理类实例,InvocationHandler实例的引用其实是Proxy持有着,因为生成的代理类默认继承自Proxy,所以最后会调用Proxy的构造器将引用传入。
getProxyClass0方法
下面看下getProxyClass0这个方法,看看代理类的Class对象是怎样来的。
1 | /** |
getProxyClass0方法内部没有多少内容,首先是检查目标代理类实现的接口不能大于65535这个数,之后是通过类加载器和接口集合去缓存里面获取,如果能找到代理类就直接返回,否则就会调用ProxyClassFactory这个工厂去生成一个代理类。
ProxyClassFactory工厂类
1 | //代理类生成工厂 |
该工厂的apply方法会被调用用来生成代理类的Class对象,由于代码的注释比较详细,只挑关键点进行阐述。
在代码中可以看到JDK生成的代理类的类名是“$Proxy”+序号。
如果接口是public的,代理类默认是public final的,并且生成的代理类默认放到com.sun.proxy这个包下。
如果接口是非public的,那么代理类也是非public的,并且生成的代理类会放在对应接口所在的包下。
如果接口是非public的,并且这些接口不在同一个包下,那么就会报错。
WeakCache缓存的实现机制
Proxy内部用到了缓存机制,如果根据提供的类加载器和接口数组能在缓存中找到代理类就直接返回该代理类,否则会调用ProxyClassFactory工厂去生成代理类。这里用到的缓存是二级缓存,它的一级缓存key是根据类加载器生成的,二级缓存key是根据接口数组生成的。具体的内部机制直接上代码详细解释。
1 | //Reference引用队列 |
首先看一下WeakCache的成员变量和构造器,WeakCache缓存的内部实现是通过ConcurrentMap来完成的,成员变量map就是二级缓存的底层实现,reverseMap是为了实现缓存的过期机制,subKeyFactory是二级缓存key的生成工厂,通过构造器传入,这里传入的值是Proxy类的KeyFactory,valueFactory是二级缓存value的生成工厂,通过构造器传入,这里传入的是Proxy类的ProxyClassFactory。接下来看一下WeakCache的get方法。
1 | public V get(K key, P parameter) { |
WeakCache的get方法并没有用锁进行同步,那它是怎样实现线程安全的呢?
因为它的所有会进行修改的成员变量都使用了ConcurrentMap,这个类是线程安全的。因此它将自身的线程安全委托给了ConcurrentMap, get方法尽可能的将同步代码块缩小,这样可以有效提高WeakCache的性能。
ClassLoader作为了一级缓存的key,这样可以首先根据ClassLoader筛选一遍,因为不同ClassLoader加载的类是不同的。
然后它用接口数组来生成二级缓存的key,这里它进行了一些优化,因为大部分类都是实现了一个或两个接口,所以二级缓存key分为key0,key1,key2,keyX。key0到key2分别表示实现了0到2个接口,keyX表示实现了3个或以上的接口,事实上大部分都只会用到key1和key2。这些key的生成工厂是在Proxy类中,通过WeakCache的构造器将key工厂传入。这里的二级缓存的值是一个Factory实例,最终代理类的值是通过Factory这个工厂来获得的。
1 | private final class Factory implements Supplier<V> { |
再看看Factory这个内部工厂类,可以看到它的get方法是使用synchronized关键字进行了同步。进行get方法后首先会去验证subKey对应的suppiler是否是工厂本身,如果不是就返回null,而WeakCache的get方法会继续进行重试。如果确实是工厂本身,那么就会委托ProxyClassFactory生成代理类,ProxyClassFactory是在构造WeakCache的时候传入的。所以这里解释了为什么最后会调用到Proxy的ProxyClassFactory这个内部工厂来生成代理类。生成代理类后使用弱引用进行包装并放入reverseMap中,最后会返回原装的代理类。
ProxyGenerator生成代理类的字节码文件
通过前面的分析,知道了代理类是通过Proxy类的ProxyClassFactory工厂生成的,这个工厂类会去调用ProxyGenerator类的generateProxyClass()方法来生成代理类的字节码。ProxyGenerator这个类存放在sun.misc包下,该类的generateProxyClass()静态方法的核心内容就是去调用generateClassFile()实例方法来生成Class文件。接下来看看generateClassFile()这个方法内部做了些什么。
1 | private byte[] generateClassFile() { |
可以看到generateClassFile()方法是按照Class文件结构进行动态拼接的。那什么是Class文件呢?
我们平时编写的Java文件是以.java结尾的,在编写好了之后通过编译器进行编译会生成.class文件,这个.class文件就是Class文件。Java程序的执行只依赖于Class文件,和Java文件是没有关系的。这个Class文件描述了一个类的信息,当我们需要使用到一个类时,Java虚拟机就会提前去加载这个类的Class文件并进行初始化和相关的检验工作,Java虚拟机能够保证在你使用到这个类之前就会完成这些工作,我们只需要安心的去使用它就好了,而不必关心Java虚拟机是怎样加载它的。当然,Class文件并不一定非得通过编译Java文件而来,你甚至可以直接通过文本编辑器来编写Class文件。在这里,JDK动态代理就是通过程序来动态生成Class文件的。
生成Class文件主要分为三步:
第一步:收集所有要生成的代理方法,将其包装成ProxyMethod对象并注册到Map集合中。
第二步:收集所有要为Class文件生成的字段信息和方法信息。
第三步:完成了上面的工作后,开始组装Class文件。
一个类的核心部分就是它的字段和方法。重点聚焦第二步,看看它为代理类生成了哪些字段和方法。在第二步中,按顺序做了下面四件事。
为代理类生成一个带参构造器,传入InvocationHandler实例的引用并调用父类的带参构造器。
遍历代理方法Map集合,为每个代理方法生成对应的Method类型静态域,并将其添加到fields集合中。
遍历代理方法Map集合,为每个代理方法生成对应的MethodInfo对象,并将其添加到methods集合中。
为代理类生成静态初始化方法,该静态初始化方法主要是将每个代理方法的引用赋值给对应的静态字段。
通过以上分析,可以大致知道JDK动态代理最终会生成如下结构的代理类:
1 | public class Proxy0 extends Proxy implements UserDao { |
经过层层分析,深入探究JDK源码,还原动态生成的代理类的本来面目,明白了以下几点:
代理类默认继承Porxy类,因为Java中只支持单继承,所以JDK动态代理只能去实现接口。
代理方法都会去调用InvocationHandler的invoke()方法,因此需要重写InvocationHandler的invoke()方法。
调用invoke()方法时会传入代理实例本身,目标方法和目标方法参数。解释了invoke()方法的参数是怎样来的。