当前位置: 首页 > 后端技术 > Java

动态代理:JDK和Cglib

时间:2023-04-01 21:17:52 Java

动态代理的出现率非常高,无论是应用在框架中还是面试中,都经常出现。所以,了解动态代理的来龙去脉是了解框架的基础,也是进阶道路上不可绕过的敲门砖。1、静态代理先说静态代理,也就是代理模式的出现解决了什么问题?现实生活中,保姆是家庭事务的代理人,经纪人是明星的代理人,代理人是为客户服务的,一般都是在某一类事情上比较专业的人。代码中,模拟雇佣清洁工打扫房子的场景,CleanProxyPerson是清洁工,Person代表业主,CleanThing代表约定的清洁范围,如下图,运行后会增强cleanHouse()方法,不在接口cleanSafeBox()中不能被委托。接口的一个功能是将职责定义为协议。比如把分娩之类的东西放在界面里,显然是不合适的。公共类MainOfUndynamicProxy{publicstaticvoidmain(String[]args){CleanThingperson=newPerson();CleanThingproxyPerson=newCleanProxyPerson(person);proxyPerson.cleanHouse();}}//ownerpublicclassPersonimplementsCleanThing{@OverridepublicvoidcleanHouse(){System.out.println("打扫房间核心区域");}publicvoidcleanSafeBox(){System.out.println("清理保险箱");}publicPersongetChild(){returnnewPerson();}}publicclassCleanProxyPersonimplementsCleanThing{privateCleanThingcleanThing;publicCleanProxyPerson(CleanThingcleanThing){this.cleanThing=cleanThing;}@OverridepublicvoidcleanHouse(){System.out.println("-----整体清扫下(专业人士)-----");cleanThing.cleanHouse();}}publicinterfaceCleanThing{voidcleanHouse();}代理的应用,以接口为纽带,与目标类解耦,同时达到增强目标类的目的。在实际的业务场景中,有各种各样的接口和实现。如果有增强的需求,比如调用统计,耗时统计,这样一一写显然是不现实的。2.动态代理动态代理解决了工作量的问题。一般来说,为了省去写代码的工作,需要在编译时或者运行时使用一些hack。先看对象实例化的过程。如下图,实例化一个Person对象,首先将Person.java编译成Person.class文件,然后通过ClassLoader加载到JVM中,生成一个Class,放在方法区,然后根据把Class实例变成person对象放到堆中。Class是java.lang中的类,描述了类的原始信息,比如类定义了哪些成员变量、方法、字段等。因此,要实例化一个对象,需要得到它的Class<>,通常是从.class文件中加载。它能凭空产生吗?答案是肯定的,因为目标类和代理类的信息基本一致,直接从接口的Class<>复制一份即可。JDK动态代理这是JDK动态代理的核心思想。其中,完成这个过程的核心类是Proxy和InvocationHandler。Proxy中的getProxyClass()用于获取Class<>。InvocationHandler是一个钩子。代理对象生成后,InvocationHandler的invoke()方法执行时会被回调。来看下具体的写法:publicclassMainOfJDKProxy{publicstaticvoidmain(String[]args){IOrderorder=newOrder();//目标类LogInvocationHandlerhandler=newLogInvocationHandler(order);//返回调函数ClassproxyClass=Proxy.getProxyClass(order.getClass().getClassLoader(),order.getClass().getInterfaces());//这里就是copyClass<>Constructorconstructor=proxyClass.getConstructor(InvocationHandler.class);IOrderproxyOrder=(IOrder)constructor.newInstance(handler);proxyOrder.run();}}publicinterfaceIOrder{voidrun();}publicclassOrderimplementsIOrder{@Overridepublicvoidrun(){System.out.println("Orderrun");}}公共类LogInvocationHandler实现InvocationHandler{privateObjecttargetObject;publicLogInvocationHandler(ObjecttargetObject){this.targetObject=targetObject;}@OverridepublicObjectinvoke(Objectproxy,Methodmethod,Object[]args)throwsThrowable{System.out.println("-----LogInvocationHandlerbegin-----");方法.invoke(targetObject,args);System.out.println("-----LogInvocationHandler结束-----");返回空值;}}其实还有更简单的方法,使用Proxy.newProxyInstance()直接返回代理对象,底层原理类似publicclassMainOfJDKProxy{publicstaticvoidmain(String[]args){IOrderorder=newOrder();//目标类LogInvocationHandlerhandler=newLogInvocationHandler(order);//回调函数IOrderproxyOrder=(IOrder)Proxy.newProxyInstance(order.getClass().getClassLoader(),order.getClass().getInterfaces(),handler);proxyOrder.run();}}所以,代理模式是为了增强业务代码。JDK使用反射机制复制类的元信息来实例化代理类,简化了手工编写的问题。但是目标类需要实现接口的限制在使用上还是有很大的局限性。还有其他解决办法吗?cglib:CodeGenerationLibrarycglib通过继承目标类给出了自己的答案。核心思想是将目标类的方法增强为子类,而不是通过实现接口。其中,核心类是Enhancer和MethodInterceptor,相当于Proxy和InvocationHandler。如下图,Enhancer通过设置父类和回调函数来创建一个代理对象。增强器enhancer=newEnhancer();enhancer.setSuperclass(Item.class);//设置父类enhancer.setCallback(newLogMethodInterceptor(newItem()));//回调函数ItemproxyItem=(Item)enhancer.create();proxyItem.run();回调函数的实现如下:publicclassLogMethodInterceptorimplementsMethodInterceptor{privateObjecttargetObject;publicLogMethodInterceptor(ObjecttargetObject){this.targetObject=targetObject;}@OverridepublicObjectintercept(ObjectproxyObject,Methodmethod,Object[]args,MethodProxymethodProxy)throwsThrowable{System.out.println("-----LogMethodInterceptorbegin-----");对象结果=method.invoke(targetObject,args);System.out.println("-----LogMethodInterceptor结束-----");返回结果;}}这种写法和JDK类似。创建时,将目标对象存储为成员变量。回调时,通过Reflect调用对应的方法。intercept()有4个入参,proxyObject是代理,method是目标类的方法,args是方法入参,methodProxy是代理方法。其他一切都很容易理解。和JDK的回调方法基本一样,只是多了一个methodProxy。为什么我们有它?如果这样写,其实是调用代理类的代理方法,而不是直接调用目标类。invokeSuper()和invoke()的区别在于是否继续拦截(),所以invoke()会造成死循环,相当递归调用。publicclassLogSuperMethodInterceptorimplementsMethodInterceptor{@OverridepublicObjectintercept(ObjectproxyObject,Methodmethod,Object[]args,MethodProxymethodProxy)throwsThrowable{System.out.println("-----LogMethodInterceptor开始-----");Objectresult=methodProxy.invokeSuper(proxyObject,args);//Objectresult=methodProxy.invoke(obj,args);//无限循环System.out.println("-----LogMethodInterceptorend-----");返回结果;}}如图,先调用代理类的run()方法,再回调拦截器的intercept()方法。这一步由cglib自动实现。所以,如果继续调用intercept()中的代理方法,就会走到图中③,然后自动走到②,死循环。③如果不允许使用,④和⑤有什么区别?好像都是调用目标类的方法。本质上的区别是通过子类调用父类方法和直接调用父类方法的区别,体现在关??键字this上。如下图,如果run()调用runElse()是子类调用的,这里的this指的是子类,不是父类,有违直觉。publicclassItem{publicvoidrun(){System.out.println("itemrun");这个.runElse();}publicvoidrunElse(){System.out.println("itemrunelse");}}所以最后体现出来的是在嵌套调用的时候,会不会去代理方法,然后去回调方法,这样整个调用环节上的方法都会增强。明白了这一点,我们继续看底层实现。在第一行添加下面这行代码,就可以看到编译好的代理类文件了。System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY,"target/temp");可以看到代理类继承了目标类Order,在成员变量中可以看到传入的拦截器。在图1中,带有FastClass的两个文件是做什么用的?FastClass机制是用另一种思路来实现反射调用的效果。当通过反射调用一个对象的具体方法时,一般这样写:invokeByName(person,"cleanHouse");invokeByName(人,“cleanSafeBox”);订单order=newOrder();invokeByName(命令,“运行”);}publicstaticvoidinvokeByName(Objectobject,StringmethodName)throwsThrowable{ClassaClass=Class.forName(object.getClass().getName());方法method=aClass.getMethod(methodName,newClass[0]);方法调用(对象,空);}}但是反射操作比较繁重,一般需要鉴权、native等。FastClass利用空间换时间的思想,将要调用的方法保存下来,放在文件中。生成的文件记录了一个类的方法列表,可以想象成数据库记录,方法名就是一个索引。这样当要运行指定的方法名时,就会根据方法名调用对应的方法。publicstaticvoidinvokeByName(Personobject,StringmethodName){if("cleanHouse".equals(methodName)){object.cleanHouse();}elseif("cleanSafeBox".equals(methodName)){object.cleanSafeBox();这里并没有直接根据方法名来判断。方法名可以重复,所以根据方法签名做一层映射,用映射的Id来表示。一个类中的方法数量是有限的。提前记录和索引,避免使用过多的反射机制,浪费空间换时间。综上所述,cglib通过继承目标类,成为目标类的子类来扩展功能。在实际调用过程中,还使用了FastClass来优化性能。3.JDKvscglib接下来比较JDK方式和cglib方式。最直观的是,在对目标类的限制上,JDK方法要求必须实现接口,没有接口就没有代理。然而,cglib采取了不同的方法,使用继承来实现代理。还有一定的限制,比如final修饰的类不能被代理。其次,在实现机制上,JDK使用反射机制运行目标方法,而cglib通过FastClass优化调用流程。在性能方面,cglib理论上更快。一般业务场景下,类数量有限的时候,一般不会有太大区别。在使用上,JDK方式原生,编写简单,无需引入其他依赖即可平滑升级,而cglib是三方包,维护成本较高。在具体选型过程中,应更多地考虑可维护性、可靠性、性能和工作量。4.应用场景理解了实现原理之后,再看看平时用到的东西,比如调用RPC时的接口,比如写mybatis。为什么只写接口而不写实现?还有spring的AOP机制,是基于动态代理实现的。在此基础上,更高层的玩法就是代理链,比如mybatis中的拦截器。在多个的情况下,就是一个代理,一层代理,就像套娃一样。五、总结从代理模式到动态代理,在增强代码的同时,解决了代码侵入和工作繁琐的问题。JDK和cglib从不同的角度给出解决方案,也有不同的优势和局限性。其他文章中的示例代码都在github上,欢迎来玩:https://github.com/JayeGuo/Ja...