摘要:与静态代理对比,动态代理是在动态生成代理类,由代理类完成对具体方法的封装,实现的功能。本文将分析中两种动态代理的实现方式,和,比较它们的异同。那如何动态编译呢你可以使用,这是一个封装了的库,帮助你方便地实现动态编译源代码。
发现Java面试很喜欢问Spring AOP怎么实现的之类的问题,所以写一篇文章来整理一下。关于AOP和代理模式的概念这里并不做赘述,而是直奔主题,即AOP的实现方式:动态代理。与静态代理对比,动态代理是在runtime动态生成Java代理类,由代理类完成对具体方法的封装,实现AOP的功能。
本文将分析Java中两种动态代理的实现方式,jdk proxy和cglib,比较它们的异同。本文并不会过多地分析jdk和cglib的源码去探究底层的实现细节,而只关注最后生成的代理类应该是什么样的,如何实现代理。只是我个人的整理和思考,和真正的jdk,cglib的产生的结果可能不尽相同,但从原理上来讲是一致的。
文章的最后也会探讨如何自己实现一个简单的动态代理,并提供我自己实现的简单版本,当然仅供参考。
JDK Proxy这是Java反射包java.lang.reflect提供的动态代理的方式,这种代理方式是完全基于接口的。这里先给出一个简单的例子。
定义接口:
interface ifc { int add(int, int); }
然后是接口ifc的实现类Real:
class Real implements ifc { @Override public int add(int x, int y) { return x + y; }
Real就是我们需要代理的类,比如我们希望在调用add的前后打印一些log,这实际上就是AOP了。我们需要最终产生一个代理类,实现同样的接口ifc,执行Real.add的功能,但需要增加一行新的打印语句。这一切对用户是透明的,用户只需要关心接口的调用。为了能在Real.add的周围添加额外代码,动态代理都是通过一种类似方法拦截器的东西来实现的,在Java Proxy里这就是InvocationHandler.
class Handler implements InvocationHandler { private final Real real; public Handler(Real real) { this.real = real; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException { System.out.println("=== BEFORE ==="); Object re = method.invoke(real, args); System.out.println("=== AFTER ==="); return re; } }
这里最关键的就是invoke方法,实际上代理类的add方法,以及其它方法(如果接口还定义了其它方法),最终都只是调用这个Handler的invoke方法,由你来具体定义在invoke里需要做什么,通常就是调用真正实体类Real的方法,这里就是add,以及额外的AOP行为(打印 BEFORE 和 AFTER)。所以可想而知,代理类里必然是有一个InvocationHandler的实例的,所有的接口方法调用都会由这个handler实例来代理。
所以我们应该能大概刻画出这个代理类的模样:
public ProxyClass implements ifc { private static Method mAdd; private InvocationHandler handler; static { Class clazz = Class.forName("ifc"); mAdd = clazz.getMethod("add", int.class, int.class); } @Override public int add(int x, int y) { return (Integer)handler.invoke(this, mAdd, new Object[] {x, y}); } }
这个版本非常简单,但已足够实现我们的要求。我们来观察这个类,首先毋庸置疑它实现了ifc接口,这是代理模式的根本。它的add方法直接调用InvocationHandler实例的invoke方法,传入三个参数,第一个是代理类本身this指针,第二个是add方法的反射类,第三个是参数列表。所以在invoke方法里,用户就能自由定义它的行为实现AOP,所有这一切的桥梁就是InvocationHandler,它完成方法的拦截与代理。
代理模式一般要求代理类中有一个真正类(被代理类)的实例,在这里也就是Real的实例,这样代理类才能去调用Real中原本的add方法。那Real在哪里呢?答案也是在InvocationHandler里。这与标准的代理模式相比,似乎多了一层嵌套,不过这并没有关系,只要这个代理的链条能够搭建起来,它就符合代理模式的要求。
注意到这里add方法的反射实例mAdd的初始化方式,我们使用静态块static {...}来完成,只会被设置一次,并且不会有多线程问题。当然你也可以用懒加载等方式,不过就得考虑并发的安全性。
最后看一下JDK Proxy的具体使用:
Handler handler = new Handler(new Real()); ifc p = (ifc)Proxy.newProxyInstance(ifc.class.getClassLoader(), new Class[] {ifc}, handler); p.add(1, 2);
方法newProxyInstance就会动态产生代理类,并且返回给我们一个实例,实现了ifc接口。这个方法需要三个参数,第一个ClassLoader并不重要;第二个是接口列表,即这个代理类需要实现那些接口,因为JDK的Proxy是完全基于接口的,它封装的是接口的方法而不是实体类;第三个参数就是InvocationHandler的实例,它会被放置在最终的代理类中,作为方法拦截和代理的桥梁。注意到这里的handler包含了一个Real实例,这在上面已经说过是代理模式的必然要求。
总结一下JDK Proxy的原理,首先它是完全面向接口的,其实这才是符合代理模式的标准定义的。我们有两个类,被代理类Real和需要动态生成的代理类ProxyClass,都实现了接口ifc。类ProxyClass需要拦截接口ifc上所有方法的调用,并且最终转发到实体类Real上,这两者之间的桥梁就是方法拦截器InvocatioHandler的invoke方法。
上面的例子里我给出类ProxyClass的源代码,当然实际上JDK Proxy是不会去产生源代码的,而是直接生成类的原始数据,它具体是怎么实现我们暂时不讨论,我们目前只需要关心这个类是什么样的,以及它实现代理的原理。
cglib实现动态代理这是Spring使用的方式,与JDK Proxy不同之处在于它不是面向接口的,而是基于类的继承。这似乎是有点违背代理模式的标准格式,不过这没有关系,所谓的代理模式只是一种思想而不是严格的规范。我们直接看它是如何使用的。
现在没有接口,我们直接有实体类:
class Real { public int add(int x, int y) { return x + y; } }
类似于InvocationHandler,这里cglib直接使用一个叫MethodInterceptor的类,顾名思义。
public class Interceptor implements MethodInterceptor { @Override public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { System.out.println("=== BEFORE ==="); Object re = proxy.invokeSuper(obj, args); System.out.println("=== AFTER ==="); return re; } }
使用方法:
public static void main(String[] args) { Enhancer eh = new Enhancer(); eh.setSuperclass(Real.class); eh.setCallback(new Interceptor()); Real r = (Real)eh.create(); int result = r.add(1, 2); }
如果你仔细和JDK Proxy比较,会发现它们其实是类似的:
首先JDK Proxy提供interface列表,而cglib提供superclass供代理类继承,本质上都是一样的,就是提供这个代理类的签名,也就是对外表现为什么类型。
然后是一个方法拦截器,JDK Proxy里是InvocationHandler,而cglib里一般就是MethodInterceptor,所有被代理的方法的调用是通过它们的invoke和intercept方法进行转接的,AOP的逻辑也是在这一层实现。
它们不同之处上面已经说了,就在于cglib生成的动态代理类是直接继承原始类的,所以我们这里也可以大概刻画出这个代理类长什么样子:
public ProxyClass extends Real { private static Method mAdd; private static MethodProxy mAddProxy; private MethodInterceptor interceptor; static { Class clazz = Class.forName("ifc"); mAdd = clazz.getMethod("add", int.class, int.class); // Some logic to generate mAddProxy. // ... } @Override public int add(int x, int y) { return (Integer)interceptor.invoke( this, mAdd, new Object[] {x, y}, mAddProxy); } }
因为直接继承了Real,那自然就包含了Real的所有public方法,都通过interceptor.invoke进行拦截代理。这其实和上面JDK Proxy的原理是类似的,连invoke和intercept方法的签名都差不多,第一个参数是this指针代理类本身,第二个参数是方法的反射,第三个参数是方法调用的参数列表。唯一不同的是,这里多出一个MethodProxy,它是做什么用的?
如果你仔细看这里invoke方法内部的写法,当用户想调用原始类(这里是Real)定义的方法时,它必须使用:
Object re = proxy.invokeSuper(obj, args);
这里就用到了那个MethodProxy,那我们为什么不直接写:
Object re = method.invoke(obj, args);
答案当然是不可以,你不妨试一下,程序会进入一个无限递归调用。这里的原因恰恰就是因为代理类是继承了原始类的,obj指向的就是代理类对象的实例,所以如果你对它使用method.invoke,由于多态性,就会又去调用代理类的add方法,继而又进入invoke方法,进入一个无限递归:
obj.add() { interceptor.invoke() { obj.add() { interceptor.invoke() { ... } } } }
那我如何才能在interceptor.invoke()里去调用基类Real的add方法呢?当然通常做法是super.add(),然而这是在MethodInterceptor的方法里,而且这里的method调用必须通过反射完成,你并不能在语法层面上做到这一点。所以cglib封装了一个类叫MethodProxy帮助你,这也是为什么那个方法的名字叫invokeSuper,表明它调用的是原始基类的真正方法。它究竟是怎么办到的呢?你可以简单理解为,动态代理类里会生成这样一个方法:
int super_add(int x, int y) { return super.add(x, y); }
当然你并不知道有这么一个方法,但invokeSuper会最终找到这个方法并调用,这都是在生成代理类时通过一系列反射的机制实现的,这里就不细展开了。
小结以上我对比了JDK Proxy和cglib动态代理的使用方法和实现上的区别,它们本质上是类似的,都是提供两个最重要的东西:
接口列表或者基类,定义了代理类(当然也包括原始类)的签名。
一个方法拦截器,完成方法的拦截和代理,是所有调用链的桥梁。
需要说明的一点是,以上我给出的代理类ProxyClass的源代码,仅是参考性的最精简版本,只是为了说明原理,而不是JDK Proxy和cglib真正生成的代理类的样子,真正的代理类的逻辑要复杂的多,但是原理上基本是一致的。另外之前也说到过,事实上它们也不会生成源码,而是直接产生类的字节码,例如cglib是封装了ASM来直接生成Class数据的。
如何生成代理类接下来的部分纯粹是实验性质的。既然知道了代理类长什么样,可能还是有人会关心底层究竟如何在runtime动态生成这个类,这里我个人想了两种方案。
第一种方法是动态生成ProxyClass源码,然后动态编译,就能得到Class了。这里就需要利用反射,加上一系列字符串拼接,生成源码。如果你充分理解代理类应该长什么样,其实并不是很难做到。那如何动态编译呢?你可以使用JOOR,这是一个封装了javax.tools.JavaCompiler的库,帮助你方便地实现动态编译Java源代码。我试着写了一个Demo,纯粹是实验性质的。而且它有个重大问题,我不知道如何修改它编译使用的classpath,在默认情况下它无法引用到你自己定义的任何类,因为它们不在编译的classpath里,编译就不会通过,这实际上就使得这个代码生成器没有任何卵用。。。我强行通过修改System.setProperty的classpath来添加我的class路径绕开了这个问题,然而这显然不是个解决根本问题的方法。
第二种方法更直接,就是生成类的字节码。这也是cglib使用的方法,它封装了ASM,这是一个可以用来直接操纵Class数据的库,通过它你就可以任意生成或修改你想要的Class,当然这需要你对虚拟机的字节码比较了解,才能玩得通这种比较黑科技的套路。这里我也写了一个Demo,也纯粹是实验而已,感兴趣的童鞋也可以自己试一下。写字节码还是挺酸爽的,它类似汇编但其实比汇编容易的多。它不像汇编那样一会儿寄存器一会儿内存地址,一会儿堆一会儿栈,各种变量和地址绕来绕去。字节码的执行方式是很清晰的,变量都存储在本地变量表里,栈只是用来做函数调用,所以非常直观。
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/76511.html
摘要:要明白,动态代理类的存在意义是为了拦截方法并修改逻辑而动态代理的局限性之一就是只能拦截接口所声明的方法。因为动态代理类是继承自业务类,所以该类和方法不能声明成无法继承或重写。者最终都是生成了一个新的动态代理类对象。 动态代理 1、先谈静态代理 对于静态代理,我们已经很熟悉了。我们拥有一个抽象类,真实类继承自抽象类并重写其业务方法,代理类持有真实类的对象实例,在重写业务方法中通过调用真实...
摘要:动态代理又被称为代理或接口代理。静态代理在编译时产生字节码文件,可以直接使用,效率高。代理无需实现接口,通过生成类字节码实现代理,比反射稍快,不存在性能问题,但会继承目标对象,需要重写方法,所以目标对象不能为类。 一、代理模式介绍 代理模式是一种设计模式,提供了对目标对象额外的访问方式,即通过代理对象访问目标对象,这样可以在不修改原目标对象的前提下,提供额外的功能操作,扩展目标对象的功...
摘要:场景描述病从口入这句成语告诉我们注意饮食健康,小六同学想吃苹果,在吃苹果之前需要清洗一下苹果和洗一下手,吃完苹果后,需要洗一下手保持个人卫生十分钟后。。。动态代理小六委托管家来代理洗食物和洗手,小六属于委托对象,管家属于代理对象。 前言 为了更好的理解代理模式,首先根据生活中实际场景进行模拟,让我们在生活中去体验设计思想的美妙。 场景描述 病从口入这句成语告诉我们注意饮食健康,小六同学...
摘要:是一种特殊的增强切面切面由切点和增强通知组成,它既包括了横切逻辑的定义也包括了连接点的定义。实际上,一个的实现被拆分到多个类中在中声明切面我们知道注解很方便,但是,要想使用注解的方式使用就必须要有源码因为我们要 前言 只有光头才能变强 上一篇已经讲解了Spring IOC知识点一网打尽!,这篇主要是讲解Spring的AOP模块~ 之前我已经写过一篇关于AOP的文章了,那篇把比较重要的知...
摘要:代理模式的实现静态代理优缺点优点只对对需要的方法加代理逻辑。通过继承的方式进行代理,无论目标对象有没有实现接口都可以代理,但是无法处理的情况。 注意:本文所有的class使用的static修饰主要是为了能在一个类里面测试。实际项目中不应该这样做的,应该分包分class。文字描述不是很多,还是看代码比较好理解吧... 1. Java代理的理解 代理模式是一种设计模式,简单说即是在不改变源...
阅读 2450·2023-04-26 02:47
阅读 2965·2023-04-26 00:42
阅读 845·2021-10-12 10:12
阅读 1343·2021-09-29 09:35
阅读 1652·2021-09-26 09:55
阅读 425·2019-08-30 14:00
阅读 1510·2019-08-29 12:57
阅读 2332·2019-08-28 18:00