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

为什么JDK动态代理一定要基于接口?

时间:2023-03-20 17:38:32 科技观察

大家好,我是九头蛇。不出意外,本文发表于2022年2月22日,正月二十二,星期二,22:22。毕竟是个有趣的约会,不发点东西有点浪费。毕竟我们大多数人大概率活不过2222年2月22日。小伙伴提出了一个问题,为什么JDK的动态代理一定要基于接口来实现呢?很好的安排,其实要理解这个问题,我们还是需要一些proxy和reflection的底层知识。我们今天将讨论这个问题,带你~一个简单的例子在分析原因之前,我们先完整的看一下实现jdk动态代理需要的步骤。首先我们需要定义一个接口:publicinterfaceWorker{voidwork();}然后基于这个接口写一个新的实现类:publicclassProgrammerimplementsWorker{@Overridepublicvoidwork(){System.out.println("编码...");}}自定义一个Handler,实现InvocationHandler接口,重写内部invoke方法实现逻辑增强。其实这个InvocationHandler可以用匿名内部类的形式定义,为了结构清晰,单独声明。公共类WorkHandler实现InvocationHandler{privateObjecttarget;WorkHandler(对象目标){this.target=target;}@OverridepublicObjectinvoke(Objectproxy,Methodmethod,Object[]args)throwsThrowable{if(method.getName().equals("work")){System.out.println("beforework...");对象结果=method.invoke(target,args);System.out.println("下班后...");返回结果;}returnmethod.invoke(target,args);}}在main方法中测试,使用Proxy类的静态方法newProxyInstance生成代理对象并调用该方法:publicstaticvoidmain(String[]args){Programmerprogrammer=newProgrammer();Workerworker=(Worker)Proxy.newProxyInstance(programmer.getClass().getClassLoader(),programmer.getClass().getInterfaces(),newWorkHandler(programmer));worker.work();}执行上面的代码,输出:beforework...coding...afterwork...可以看到方法逻辑增强了,这里是一个简单的动态代理过程它实现了。下面我们来分析Proxy的源码。由于源代码解析是一个代理过程,所以必须区分本机对象和代理对象。让我们看看如何在源代码中动态创建代理对象。上面的例子中,创建代理对象调使用的是代理类的静态方法newProxyInstance,查看一下下面的源码:@CallerSensitivepublicstaticObjectnewProxyInstance(ClassLoaderloader,Class[]interfaces,InvocationHandlerh)throwsIllegalArgumentException(h);finalClass[]intfs=interfaces.clone();最后的SecurityManagersm=System.getSecurityManager();if(sm!=null){checkProxyAccess(Reflection.getCallerClass(),loader,intfs);}/**查找或生成指定的代理类。*/Classcl=getProxyClass0(loader,intfs);/**使用指定的调用处理程序调用其构造函数。*/try{if(sm!=null){checkNewProxyPermission(Reflection.getCallerClass(),cl);}finalConstructorcons=cl.getConstructor(constructorParams);最终调用处理程序ih=h;if(!Modifier.isPublic(cl.getModifiers())){AccessController.doPprivileged(newPrivilegedAction(){publicVoidrun(){cons.setAccessible(true);returnnull;}});}返回cons.newInstance(newObject[]{h});}//省略catch}总结一下上面代码的关键部分:在checkProxyAccess方法中,进行参数校验。在getProxyClass0方法中,生成一个代理类Class或者找到生成的代理类的缓存。通过getConstructor方法获取生成的代理类的构造函数。newInstance方法生成一个实例对象,也就是最终的代理对象。在上面的过程中,构造方法的获取和对象的生成都是直接使用的反射。需要重点关注的是生成代理类getProxyClass0的方法privatestaticClassgetProxyClass0(ClassLoaderloader,Class...interfaces){if(interfaces.length>65535){thrownewIllegalArgumentException("超出接口限制");}//如果给定加载器定义的代理类实现//给定接口存在,这将简单地返回缓存的副本;//否则,它会通过ProxyClassFactory创建代理类returnproxyClassCache.get(loader,interfaces);}如果缓存中已经存在,则直接从缓存中获取。这里的proxyClassCache是??一个WeakCache类型。如果缓存中目标classLoader和接口数组对应的类已经存在,则返回缓存的副本。如果不是,则使用ProxyClassFactory生成Class对象。中间的调用过程可以省略,最后真正调用ProxyClassFactory的apply方法生成Class。在apply方法中,主要做了以下三件事。首先按照规则生成文件名:if(proxyPkg==null){//如果没有非公开代理接口,使用com.sun.proxy包proxyPkg=ReflectUtil.PROXY_PACKAGE+".";}/**为要生成的代理类选择一个名称。*/longnum=nextUniqueNumber.getAndIncrement();StringproxyName=proxyPkg+proxyClassNamePrefix+num;如果接口定义为public,com.sun.proxy会默认作为包名和类名是$Proxy加上一个自增整数值,初始为0,所以生成的文件名为$Proxy0。如果它是非公共接口,它将使用与代理类相同的包名。您可以编写一个私有接口的示例进行测试。packagecom.hydra.test.face;publicclassInnerTest{privateinterfaceInnerInterface{voidrun();}classInnerClazzimplementsInnerInterface{@Overridepublicvoidrun(){System.out.println("go");此时生成的代理类的包名为com.hydra.test.face,与代理类相同:然后,使用ProxyGenerator.generateProxyClass方法生成代理字节码数组:byte[]proxyClassFile=ProxyGenerator.generateProxyClass(proxyName,interfaces,accessFlags);在generateProxyClass方法中,一个重要的参数会发挥作用:privatestaticfinalbooleansaveGeneratedFiles=(Boolean)AccessController.doPrivileged(newGetBooleanAction("sun.misc.ProxyGenerator.saveGeneratedFiles"));如果该属性配置为true,则字节码将存储在硬盘上的class文件中,否则不会保存临时字节码文件。最后调用本地方法defineClass0生成Class对象:returndefineClass0(loader,proxyName,proxyClassFile,0,proxyClassFile.length);返回代理类的Class的过程之前已经介绍过,首先获取构造方法,然后使用构造函数反射的方式创建代理对象。神秘的代理对象创建代理对象的过程源码分析完毕。我们可以先用debug看看上面生成的代理对象是什么:和源码中看到的规则一样,是一个Class为$Proxy0的神秘对象。再看一下代理对象的类的详细信息:该类的全限定名是com.sun.proxy.$Proxy0,上面我们说了,这个类是在运行过程中动态生成的,程序运行后执行后,class文件会自动删除。如果想让这个临时文件不被删除,就需要修改我们上面提到的参数。有两种方法可以做到这一点。第一个是在启动VM参数中添加:-Dsun.misc.ProxyGenerator.saveGeneratedFiles=true第二个是在代码中添加如下一句,注意在生成动态代理对象之前添加:System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles","true");使用以上两种方法中的任何一种后,都可以保存临时字节码文件。您需要注意生成此文件的位置。它不在target目录下,而是在工程目录下的com\sun\proxy中。它只是对应默认生成的包名。得到字节码文件后,就可以使用反编译工具进行反编译了。这里可以使用jad在cmd中执行下一条命令:jad-sjava$Proxy0.class可以看到反编译后的$Proxy0.java文件内容,下面的代码中,我只保留了核心部分,省略了其中的定义不相关的equals、toString和hashCode方法。publicfinalclass$Proxy0extendsProxyimplementsWorker{public$Proxy0(InvocationHandlerinvocationhandler){super(invocationhandler);}publicfinalvoidwork(){try{super.h.invoke(this,m3,null);返回;}catch(Error_ex){}catch(Throwablethrowable){thrownewUndeclaredThrowableException(throwable);}}私有静态方法m3;static{try{m3=Class.forName("com.hydra.test.Worker").getMethod("work",newClass[0]);//省略其他Method}//省略catch}}这个临时生成的代理类$Proxy0主要做了以下几件事:在该类的静态代码块中,通过Reflection初始化多个静态方法方法变量。除了接口中的方法外,还有继承父类Proxy的三个方法equals、toString、hashCode。在实例化过程中,会调用父类的构造函数,构造函数中传入的invocationHandler对象实际上是我们自定义的WorkHandler实例,实现了自定义接口Worker,并重写了work方法。方法中调用了InvocationHandler的invoke方法,即真正调用了WorkHandler的invoke方法中省略的equals。toString和hashCode方法的实现是相同的。这里调用了super.h.invoke()方法,分析一下整体流程。我们可以用一张图来简单概括一下上面的过程。程:为什么会有界面?通过上面的分析,我们已经知道了代理对象是如何生成的,那么回到开头的问题,为什么jdk的动态代理一定要基于接口呢?其实如果不看上面的分析,我们也应该知道扩展一个类有两种常见的方式。无论是继承父类还是实现接口,都可以让我们增强方法的逻辑,但是现在重写方法不是我们的事,而是想办法让jvm调用InvocationHandler中的invoke方法,即也就是说,代理类需要关联两个东西:代理类InvocationHandler,jdk处理这个问题的方式是选择继承父类Proxy,将InvocationHandler存放在父类对象中:publicclassProxyimplementsjava.io.Serializable{protectedInvocationHandlerh;protectedProxy(InvocationHandlerh){Objects.requireNonNull(h);这个.h=h;}//...}通过父类Proxy构造方法保存了创建代理对象过程中传入的InvocationHandler实例,并使用protected修饰保证其在子类中可以被访问和使用。但同时,由于java是单继承的,在继承Proxy之后,只能通过实现目标接口来扩展方法,从而达到我们增强目标方法逻辑的目的。其实在阅读了源码,了解了生成代理对象的过程后,我们还可以使用另一种方法来实现动态代理:publicstaticvoidmain(String[]args)throwsException{ClassproxyClass=Proxy.getProxyClass(Test3.class.getClassLoader(),Worker.class);构造函数constructor=proxyClass.getConstructor(InvocationHandler.class);InvocationHandlerworkHandler=newWorkHandler(newProgrammer());Workerworker=(Worker)constructor.newInstance(workHandler);worker.work();}运行结果和之前一样。这种写法其实是把我们前面介绍的核心方法提了出来,省略了一些参数的验证过程。这种方式可以帮助大家熟悉jdk动态代理的原理,但是建议大家在使用过程中使用标准的方式,相对更加安全和规范。总结本文从源码和实验的角度分析了jdk动态代理生成代理对象的过程,通过代理类的实现原理分析了为什么jdk动态代理必须基于接口实现。总的来说,jdk动态代理的应用还是很广泛的。比如动态代理在Spring、Mybatis、Feign等很多框架中都有广泛的应用。很有帮助。