当前位置: 首页 > 科技观察

手绘六图吃透动态代理

时间:2023-03-20 17:54:36 科技观察

本文转载自微信公众号《微笑的建筑师》,作者雷小帅。转载本文,请联系LoveSmile的架构师公众号。在讲解动态代理之前,先说说什么是静态代理。静态代理假设有一天领导突发奇想,给你发来一个请求:统计项目中所有类的执行时间。拿到需求的那一刻,脑子里第一个冒出来的想法是:在每个方法的首行和末行加上时间嵌入点,然后打印一行log就完了。拿起键盘准备开始,想了想,我又开始犹豫了:每个方法都加几行代码,这不是侵入式修改吗?听架构师说这样的场景可以使用代理模式,所以试试看。具体方法如下。静态代理的实现(1)为项目中的每个类编写一个代理类,使其和目标类实现相同的接口。图中红色标记的是代理类。(2)在代理类中维护一个目标实现类。在调用代理类的方法时,仍然会调用目标类的方法,只是前后添加了一些其他的逻辑代码。也就是说客户端不需要直接调用目标实现类,只需要调用代理类,从而间接调用相应的方法。用一个公式来概括:代理类=增强代码+目标实现类。下图中,计算的耗时逻辑是增强代码。(3)用新的代理类替换所有新的目标类,并将目标类作为构造函数参数传入;所有调用目标类的地方都替换为代理类调用。如果你看懂了上面的实现方法,恭喜你已经掌握了静态代理的核心思想。静态代理的缺点静态代理的思路很简单,就是为每一个目标实现类都写一个对应的代理实现类,但是如果一个项目有几千甚至几万个类,工作量可想而知.前面我们还隐藏了一个假设:每个类都会实现一个接口。如果一个类没有实现任何接口,代理类怎么实现呢?好了,总结一下静态代理的缺点:静态代理需要为每一个目标实现类都写一个对应的代理类。如果目标类的方法发生变化,代理类也需要随之移动,维护成本非常高。静态代理必须依赖接口。既然知道了静态代理的缺点,那么有没有什么方法可以少用或不用代理类来实现代理功能呢?答案是肯定的,动态代理。对象创建过程在正式介绍动态代理之前,我们先来回顾一下java中对象是如何创建的。我们可以在项目中用一行代码简单的创建一个对象,但是实际过程还是很复杂的。//创建对象Aa=newA();(1)Java源文件编译生成字节码文件(.class结尾);(2)类加载器将class文件加载到JVM内存中,也就是常说的Method区,生成Class对象;(3)执行new,申请一块内存区域,然后创建一个对象放到JVM对象中,准确的说是newgeneration;上面的过程中提到了Class对象,有两个概念初学者很容易混淆:Class对象和实例对象。Class对象只是Class类的一个实例,Class类描述了所有的类;实例对象是通过类对象创建的。从上面的分析我们可以看出,创建一个实例,最关键的是获取Class对象。可能有同学会有疑惑。当我编写代码时,我不使用Class对象来创建对象。那是因为底层Java语言为您封装了细节。Java语言为我们提供了new关键字,new就是这么好用,一行代码就可以创建一个对象。让我们回到上面提到的静态代理。静态代理最重要的是要提前写好代理类。使用代理类,您可以创建一个新的代理对象。但是每次都写一个代理类是不是太麻烦了?!稍微扩展一下思路,有没有办法不用写代理类就可以生成代理对象呢?是的,上面说的代理类Class对象可以生成Proxy对象,如何获取代理类Class对象呢?我们往下看。动态代理Class对象包含了一个类的所有信息,如:构造方法、成员方法、成员属性等,如果我们不写代理类,好像是得不到代理类Class对象,但稍微想一想:代理类和目标类实现了同一套接口,是否可以通过接口间接获取代理类Class对象?代理类和目标类实现了同一套接口,也就是说它们的总体结构是一致的,这样我们对代理对象的操作就可以转移到目标对象上,代理对象只需要专注于增强代码的实现。上面说了这么多其实是在介绍动态代理的概念。动态代理和静态代理最大的区别就是不需要提前写代理类。通常,代理类对象是在程序运行过程中动态生成的。JDK的动态代理实现JDK原生提供了动态代理的实现,主要是通过java.lang.reflect.Proxy和java.lang.reflect.InvocationHandler的使用。Proxy类有一个静态方法,通过传入一个类加载器和一组接口返回一个代理类对象。publicstaticClassgetProxyClass(ClassLoaderloader,Class...interfaces)这个方法的作用很简单,就是把你传入的一组接口类的结构信息“复制”到一个新的Class对象中,新的Class对象有一个可以创建对象的构造函数。一句话总结:静态方法Proxy.getProxyClass()的本质就是从一个Class创建一个Class。拿到Class对象后,就可以使用反射创建实例对象了://Proxy.getProxyClass默认会生成带参数的构造函数,这里指定参数获取构造函数Constructorconstructor=aClazz.getConstructor(InvocationHandler。班级);//使用反射创建代理对象Aa1=constructor.newInstance(newInvocationHandler(){});眼尖的同学已经看到了,在创建实例的时候,需要传入一个InvocationHandler对象,说明代理对象中必须有一个成员变量来接管。当调用代理对象的方法时,实际上会执行InvocationHandler对象的invoke方法。画图理解:可以在invoke方法中写增强代码,然后调用目标对象的work方法。总结一下流程:(1)通过Proxy.getProxyClass()方法获取代理类Class对象;(2)通过反射获取构造函数对象aClazz.getConstructor();(3)定义并实例化InvocationHandler类,当然也可以直接使用Anonymous内部类;(4)通过反射constructor.newInstance()创建代理类对象;(5)调用代理方法;看完上面的流程,是不是觉得比静态代理更繁琐,有没有更优雅的方法呢?当然可以!为了尽可能简化操作,JDKProxy类直接提供了一个静态方法:publicstaticObjectnewProxyInstance(ClassLoaderloader,Class[]interfaces,InvocationHandlerh)这个方法传入类加载器,一个集合接口和InvocationHandler对象的代理对象可以直接返回。有了代理对象,就可以调用代理方法了。有那么容易吗?!newProxyInstance方法本质上帮助我们省略了获取代理类对象和通过代理类对象创建代理类的过程。这些细节都隐藏起来了。所以直接在项目中使用newProxyInstance方法就好了。上面说的过程是为了方便大家了解整个过程。看到这里,相信大家应该对JDK原生动态代理有所了解了吧。动态代理实现的cglibJDK动态代理,一旦目标类有了明确的接口,就可以通过接口生成代理Class对象,通过代理Class对象创建代理对象。这里可以看出JDK动态代理有一个限制,就是目标类必须实现接口。如果一个目标类没有实现接口,那么动态代理就不能用了?cglib的出现就是为了实现这个目的,使用asm开源包加载代理对象类的class文件,通过修改其字节码生成子类进行处理。JDK动态代理和cglib动态代理的比较下面通过几个问题来简单比较一下JDK和cglib动态代理的区别。问题一:cglib和JDK动态代理有什么区别?JDK动态代理:利用InvocationHandler加反射机制生成代理接口的匿名类,在调用具体方法前调用InvokeHandler处理cglib动态代理。加载生成的class文件,修改其字节码生成代理子类。问题2:cglib比JDK快吗?cglib的底层是ASM字节码生成框架。在JDK1.6之前,字节码生成比反射更高效。在JDK1.6之后,JDK对动态代理也逐渐进行了优化。到了1.8,JDK的效率已经高于cglib。问题三:Spring框架什么时候使用cglib,什么时候使用JDK动态代理?目标对象生成接口,默认使用JDK动态代理。如果目标对象没有实现接口,则必须使用cglib。当然,如果目标对象使用了接口,也可以强制使用cglib。总结使用代理模式可以避免对原代码进行侵入式修改。代理分为:静态代理和动态代理。静态代理要求目标类必须实现接口。通过新建一个代理类,实现与目标类同一套接口,最终实现了通过代理类间接调用目标类的方法。关于代理类,可以用一个公式来概括:代理类=增强代码+目标实现类。静态代理必须提前写好,使用起来比较麻烦。这引入了动态代理。动态代理就是在程序运行过程中动态生成代理类。根据实现方式的不同,可以分为:JDK原生动态代理和CGLIB动态代理。JDK动态代理是通过反射+InvocationHandler机制动态生成代理类来实现的,目标类必须实现该接口。cglib不要求目标类实现接口,通过修改字节码生成目标类的子类,即代理类。动态代理不仅仅用在RPC框架中,在其他地方也有广泛的应用场景,比如:SpringAOP、测试框架mock、用户认证、日志、全局异常处理、事务处理等,你学会了吗还没完成?