深入理解动态代理
代理模式
在某些情况下,一个客户不想或者不能直接引用一个对象,此时可以通过一个称之为”代理”的第三者来实现间接引用。代理对象可以在客户端和目标对象之间起到中介的作用,并且可以通过代理对象去掉客户不看到的内容和服务或者添加客户需要的额外服务。
通过引入一个新的对象(如小图片和远程代理对象)来实现对真实对象的操作或者将新的对象作为真实对象的一个替身,这种实现机制即为代理模式,通过引入代理对象来间接访问一个对象,这就是代理模式的模式动机。
代理模式(Proxy Pattern) :给某一个对象提供一个代理,并由代理对象控制对原对象的引用。代理模式的英文叫做Proxy或Surrogate,它是一种对象结构型模式。
静态代理
- 程序运行前就已经存在代理类的字节码文件,代理类和委托类的关系在运行前就已经确定了;
- 优点
- 业务类只需要关注业务逻辑本身,保证了业务类的重用性;
- 缺点
- 代理对象的一个接口只服务于一种类型的对象,如果要代理的方法很多,势必要为每一种方法都进行代理,静态代理在程序规模稍大时就无法胜任了;
- 如果接口增加一个方法,除了所有实现类需要实现这个方法外,所有代理类也需要实现此方法。增加了代码维护的复杂度;
动态代理
动态代理类的源码是在程序运行期间由JVM根据反射等机制动态的生成,所以不存在代理类的字节码文件。代理类和委托类的关系是在程序运行时确定;
Java动态代理
类加载机制
动态代理加载机制
JDK
关键代码
- [创建调用处理类] 实现 java.lang.reflect.InvocationHandler接口,实现它的 java.lang.reflect.InvocationHandler#invoke(Object proxy,Method method, Object[] args) throws Throwable接口;
- [创建动态代理类实例] 调用
java.lang.reflect.Proxy#newProxyInstance(ClassLoaderloader,Class<?>[] interfaces,InvocationHandler h) throws IllegalArgumentException 方法,传递上面创建的InvocationHandler实例, 创建动态代理实例; - jdk动态代理类: public final class $Proxy0 extends Proxy implements TargetInterface
jdk动态代理的机制特点
- 包:如果所代理的接口都是 public的,那么它将被定义在顶层包(即包路径为空),如果所代理的接口中有非public 的接口(因为接口不能被定义为 protect 或 private,所以除 public 之外就是默认的 package 访问级别),那么它将被定义在该接口所在包(假设代理了com.ibm.developerworks 包中的某非 public 接口A,那么新生成的代理类所在的包就是com.ibm.developerworks),这样设计的目的是为了最大程度的保证动态代理类不会因为包管理的问题而无法被成功定义并访问;
- 类修饰符:该代理类具有 final 和 public修饰符,意味着它可以被所有的类访问,但是不能被再度继承;
- 类名:格式是”\$ProxyN”,其中 N 是一个逐一递增的阿拉伯数字,代表Proxy 类第 N 次生成的动态代理类,值得注意的一点是,并不是每次调用Proxy 的静态方法创建动态代理类都会使得 N值增加,原因是如果对同一组接口(包括接口排列的顺序相同)试图重复创建动态代理类,它会很聪明地返回先前已经创建好的代理类的类对象,而不会再尝试去创建一个全新的代理类,这样可以节省不必要的代码重复生成,提高了代理类的创建效率;
代理类实例的特点
- 每个实例都会关联一个调用处理器对象,可以通过 Proxy 提供的静态方法getInvocationHandler 去获得代理类实例的调用处理器对象;
- 在代理类实例上调用其代理的接口中所声明的方法时,这些方法最终都会由调用处理器的invoke 方法执行,此外,值得注意的是,委托类的根类 java.lang.Object中有三个方法也同样会被分派到调用处理器的 invoke 方法执行,它们是hashCode,equals 和 toString,可能的原因有:一是因为这些方法为public 且非 final类型,能够被代理类覆盖;二是因为这些方法往往呈现出一个类的某种特征属性,具有一定的区分度,所以为了保证代理类与委托类对外的一致性,这三个方法也应该被分派到委托类执行;
- 当代理的一组接口有重复声明的方法且该方法被调用时,代理类总是从排在最前面的接口中获取方法对象并分派给调用处理器,而无论代理类实例是否正在以该接口(或继承于该接口的某子接口)的形式被外部引用,因为在代理类内部无法区分其当前的被引用类型;
被代理的一组接口的特点
- 首先,要注意不能有重复的接口,以避免动态代理类代码生成时的编译错误;
- 其次,这些接口对于类装载器必须可见,否则类装载器将无法链接它们,将会导致类定义失败;
- 再次,需被代理的所有非 public 的接口必须在同一个包中,否则代理类生成也会失败;
- 最后,接口的数目不能超过 65535,这是 JVM 设定的限制;
异常处理方面的特点
- 从调用处理器接口声明的方法中可以看到理论上它能够抛出任何类型的异常,因为所有的异常都继承于Throwable接口,但事实是否如此呢?答案是否定的,原因是我们必须遵守一个继承原则:即子类覆盖父类或实现父接口的方法时,抛出的异常必须在原方法支持的异常列表之内。所以虽然调用处理器理论上讲能够,但实际上往往受限制,除非父接口中的方法支持抛Throwable 异常。那么如果在 invoke方法中的确产生了接口方法声明中不支持的异常,那将如何呢?放心,Java动态代理类已经为我们设计好了解决方法:它将会抛出UndeclaredThrowableException 异常。这个异常是一个 RuntimeException类型,所以不会引起编译错误。通过该异常的 getCause方法,还可以获得原来那个不受支持的异常对象,以便于错误诊断。
动态代理的优点
- 动态代理与静态代理相比较,最大的好处是接口中声明的所有方法都被转移到调用处理器一个集中的方法中处理(InvocationHandler.invoke)。这样,在接口方法数量比较多的时候,我们可以进行灵活处理,而不需要像静态代理那样每一个方法进行中转;
动态代理的不足
- 诚然,Proxy已经设计得非常优美,但是还是有一点点小小的遗憾之处,那就是它始终无法摆脱仅支持interface代理的桎梏,因为它的设计注定了这个遗憾。回想一下那些动态生成的代理类的继承关系图,它们已经注定有一个共同的父类叫Proxy。Java 的继承机制注定了这些动态代理类们无法实现对 class的动态代理,原因是多继承在 Java 中本质上就行不通。
Cglib
cglib是针对类来实现代理的,基于ASM的包装,他的原理是对指定的目标类生成一个子类,并覆盖其中方法实现增强,但因为采用的是继承,所以不能对final修饰的类进行代理。
cglib 创建某个类A的动态代理类的模式是:
查找A上的所有非final 的public类型的方法定义;
将这些方法的定义转换成字节码;
将组成的字节码转换成相应的代理的class对象;
实现 MethodInterceptor接口,用来处理
对代理类上所有方法的请求(这个接口和JDK动态代理InvocationHandler的功能和角色是一样的)
差异
- JDK动态代理
- 其代理的对象必须是某个接口的实现,它是通过在运行期间创建一个接口的实现类来完成对目标对象的代理。
- 通过反射来实现对目标方法的调用,效率低一些;
- CGLIG动态代理
- 无法通知(advice)final方法,因为它不能被覆写;
- 真实对象的引用,可以直接调用,效率更高;
Javassist
javassist是jboss的一个子项目,其主要的优点,在于简单,而且快速。直接使用java编码的形式,而不需要了解虚拟机指令,就能动态改变类的结构,或者动态生成类。
使用Javassist有两种方式来实现动态代理,一种是使用代理工厂创建,和普通的JDK动态代理和CGLIB类似,另一种则可以使用字节码技术创建。
ASM
ASM 是一个 Java字节码操控框架。它能够以二进制形式修改已有类或者动态生成类。ASM可以直接产生二进制 class 文件,也可以在类被加载入 Java虚拟机之前动态改变类行为。ASM从类文件中读入信息后,能够改变类行为,分析类信息,甚至能够根据用户要求生成新类。
不过ASM在创建class字节码的过程中,操纵的级别是底层JVM的汇编指令级别,这要求ASM使用者要对class组织结构和JVM汇编指令有一定的了解。
性能比较
ASM > Javassist Bytecode > Cglib > JDK > Javassist ProxyFactory
- 差异原因
- 各方案生成的字节码不一样, 像JDK和CGLIB都考虑了很多因素,以及继承或包装了自己的一些类,所以生成的字节码非常大,而我们很多时候用不上这些, 而手工生成的字节码非常小,所以速度快。
参考文档
附录
公用类
|
|
|
|
静态代理 demo
|
|
JDK demo
|
|
JDK动态代理字节码源码
|
|
Cglib demo
|
|
Cglib动态代理字节码源码
|
|
|
|
|
|
Javassist demo
|
|
|
|
Javassist动态代理字节码源码
|
|