当前位置:主页 > 软件编程 > JAVA代码 >

深入讲解基于JDK的动态代理机制

时间:2020-10-07 14:26:36 | 栏目:JAVA代码 | 点击:

前言

『动态代理』其实源于设计模式中的代理模式,而代理模式就是使用代理对象完成用户请求,屏蔽用户对真实对象的访问。

举个最简单的例子,比如我们想要「FQ」访问国外网站,因为我们并没有墙掉所有国外的 IP,所以你可以将你的请求数据报发送到那些没有被屏蔽的国外主机上,然后你通过配置国外主机将请求转发到目的地并在得到响应报文后转发回我们国内主机上。

这个例子中,国外主机就是一个代理对象,而那些被墙掉的主机就是真实对象,我们不能直接访问到真实对象,但可以通过一个代理间接的访问到。

代理模式的一个好处就是,所有的外部请求都经过代理对象,而代理对象有权利控制是否允许你真正的访问到真实对象,如果不合法的请求,代理对象完全可以拒绝你而不用实际麻烦到真实对象。

代理模式的一个最典型的应用就是 Spring 框架,Spring 的 AOP 以面向切面式编程将实际的业务逻辑和相关日志异常等信息隔离开,而你每次对业务逻辑的请求都对应的是一个代理对象,这个代理对象中除了进行必要的权限检查,日志打印,就是真实的业务逻辑处理块。

静态代理

代理模式的实现者主要有两种,『静态代理』和『动态代理』,这两者的本质区别就在于,前者的代理类是需要程序员手动编码的,而后者的代理类是自动生成的。所以,这也是你几乎没有听过『静态代理』这个概念的原因,当然,了解一下静态代理自然更容易去理解『动态代理』。

有一点大家需要清楚,代理对象代理了真实对象所有的方法,也就是代理对象需要向外提供至少和真实对象一样的方法名供调用,所以一个代理对象就需要定义出真实对象拥有的所有方法,包括父类中的方法。

我们看一个简单的静态代理示例:

为了说明问题,我们定义了一个 IService 接口,并让我们的真实类继承并实现该接口,这样我们的真实类中就有两个方法了。

那么代理类该怎样定义才能完成对真实对象的代理呢?

一般来说,代理类的本质就是,定义出真实类中所有的方法并在方法内部添加一些其他操作,最后再调用真实类的该方法。

代理类要代理真实类中所有的方法,也就是说需要定义和真实类中那些方法签名一模一样的方法,而这些方法的内部还是会间接调用真实类的该方法。

所以一般来说,代理类会选择直接继承真实类所有的接口和父类以便拿到真实类所有的父级方法签名,也就是先代理所有的父级方法。

接着,代理真实类中非父级方法,以这里的例子来说,doService 方法就是真实类自己的方法,我们的代理类也要定义一个一模一样方法签名的方法对其进行代理。

这样,我们的代理类就算是完成了,以后对于真实类中所有方法的调用都可以通过代理类进行代理。像这样:

public static void main(String[] args){
 realClass realClass = new realClass();
 ProxyClass proxyClass = new ProxyClass(realClass);
 proxyClass.sayHello();
 proxyClass.doService();
}

proxyClass 作为一个代理类对象,可以代理真实类中所有的方法,并在这些方法执行之前,打印了一些「无关紧要」的信息。

代理模式的一个基本实现思路基本是这样,但是动态代理不同于这种静态代理的一点在于,动态代理不用我们一个一个方法的定义,虚拟机会自动为你生成这些方法。

JDK 动态代理机制

动态代理区别于静态代理的一点是,动态代理的代理类由虚拟机在运行时动态创建并于虚拟机卸载时清除。

我们复用上述静态代理中使用的类,看看 JDK 的动态代理具体是如何做到代理出某个类实例的所有方法的。

定义一个 Handler 处理类:

Main 函数中调用 JDK 的动态代理 API 生成代理类实例:

涉及的代码还是比较多的,我们一点点来分析。首先,realClass 作为我们的被代理类实现了接口 IService 并在内部定义了一个自己的方法 doService。

接着,我们定义了一个处理类,它继承了接口 InvocationHandler 并实现了其唯一申明的 invoke 方法。除此之外,我们还得声明一个成员字段用于存储真实对象,也就是被代理对象,因为我们代理的任何方法基本上都是基于真实对象的相关方法的。

关于这个 invoke 方法的作用以及各个形式参数的意义,待会我们反射代理类源码的时候再做详细的分析。

最后,定义好我们的处理类,基本上就可以进行基于 JDK 的动态代理了。核心的方法是 Proxy 类的 newProxyInstance 方法,该方法有三个参数,其一是一个类加载器,其二是被代理类实现的所有接口集合,其三是我们自定义的处理器类。

虚拟机会在运行时使用你提供的类加载器,将所有指定的接口类加载进方法区,然后反射读取这些接口中的方法并结合处理器类生成一个代理类型。

最后一句话可能有点抽象,如何「结合处理器类生成一个代理类型」?这一点我们通过指定虚拟机启动参数,让它保存下来生成的代理类的 Class 文件。

-Dsun.misc.ProxyGenerator.saveGeneratedFiles=true

我们通过第三方工具反编译这个 Class 文件,内容比较多,我们拆分了分析:

首先,这个代理类的名字是很随意的,一个程序中如果有多个代理类要生成,「$Proxy + 数字」就是它们的类名。

接着,你会注意到这个代理类继承 Proxy 类和我们指定的接口 IService(之前如果指定多个接口,这里就会继承多个接口)。

然后你会发现,这个构造器需要一个 InvocationHandler 类型的参数,并且构造器的主体就是将这个 InvocationHandler 实例传递到父类 Proxy 的对应字段进行保存,这也是为什么所有的代理类都必须使用 Proxy 作为父类的一个原因,就是为了公用父类中的 InvocationHandler 字段。后面我们会知道,这一个小小的设计将导致基于 JDK 的动态代理存在一个致命性的缺点,待会介绍。

这一块内容也算是代理类中较为重要的部分了,它将于虚拟机静态初始化这个代理类的时候执行。这一大段代码就是完成反射出所有接口中方法的功能,所有被反射出来的方法都对应一个 Method 类型的字段进行存储。

除此之外,虚拟机还反射了 Object 中的三个常用方法,也就是说,代理类还会代理真实对象从 Object 那继承来的这三个方法。

最后一部分我们看到的就是,虚拟机根据静态初始化代码块反射出来所有待代理的方法,为它们生成代理的方法。

这些方法看起来好多代码,其实就一行代码,从父类 Proxy 中取出构造实例化时存入的处理器类,并调用它的 invoke 方法。

方法的参数基本一样,第一个参数是当前代理类实例(事实证明这个参数传过去并没什么用),第二个参数是 Method 方法实例,第三个参数是方法的形式参数集合,如果没有就是 null。

而这会儿我们再来看看当初自定的处理器类:

所有的代理类方法内部都会调用处理器类的 invoke 方法并传入被代理类的当前方法,而这个 invoke 方法可以选择去让 method 正常被调用,也可以跳过 method 的调用,甚至可以在 method 真正被调用前后做一些额外的事情。

这,就是 JDK 动态代理的核心思想,我们稍微总结一下整个调用流程。

首先,一个处理器类的定义是必不可少的,它的内部必须得关联一个真实对象,即被代理类实例。

接着,我们从外部调用代理类的任一方法,从反编译的源码我们知道,代理类方法会转而去调用处理器的 invoke 方法并传入方法签名和方法的形式参数集合。

最后,方法能否得到正常的调用取决于处理器 invoke 方法体是否实实在在去调用了 method 方法。

其实,基于 JDK 实现的的动态代理是有缺陷的,并且这些缺陷是不易修复的,所以才有了 CGLIB 的流行。

一些缺陷与不足

单一的代理机制

不知道大家注意到我们上述的例子没有,虚拟机生成的代理类为了公用 Proxy 类中的 InvocationHandler 字段来存储自己的处理器类实例而继承了 Proxy 类,那说明了什么?

Java 的单根继承告诉你,代理类不能再继承任何别的类了,那么被代理类父类中的方法自然就无从获取,即代理类无法代理真实类中父类的任何方法。

除此之外的是另一个小细节,不知道大家有没有注意到,我特意这样写的。

这里的 sayHello 方法是实现的接口 IService,而 doService 方法则是独属于 realClass 自己的方法。但是我们从代理类中并没有看到这个方法,也就是说这个方法没有被代理。

所以说,JDK 的动态代理机制是单一的,它只能代理被代理类的接口集合中的方法。

不友好的返回值

大家注意一下,newProxyInstance 返回的是代理类 「$Proxy0」 的一个实例,但是它是以 Object 类型进行返回的,而你又不能强转这个 Object 实例到 「$Proxy0」 类型。

虽然我们知道这个 Object 实例其实就是 「$Proxy0」 类型,但编译期是不存在这个 「$Proxy0」 类型的,编译器自然不会允许你强转为一个不存在的类型了。所以一般只会强转为该代理类实现的接口之一。

realClass rc = new realClass();
MyHanlder hanlder = new MyHanlder(rc);
IService obj = (IService)Proxy.newProxyInstance(
  rc.getClass().getClassLoader(),
  new Class[]{IService.class},
  hanlder);
obj.sayHello();

程序运行输出:

proxy begainning.....
hello world.....
proxy ending.....

那么问题又来了,假如我们的被代理类实现了多个接口,请问你该强转为那个接口类型,现在假设被代理类实现了接口 A 和 B,那么最后的实例如果强转为 A ,自然被代理类所实现的接口 B 中所有的方法你都不能调用,反之亦然。

这样就直接导致一个结果,你得清楚哪个方法是哪个接口中的,调用某个方法之前强转为对应的接口,相当不友好的设计。

以上是我们认为基于 JDK 的动态代理机制所不太优雅的设计之处,当然了,它的优点肯定是大于这些缺点的,下一篇我们将介绍一个广为各类框架使用的 CGLIB 动态代理库,它的底层基于字节码操作框架 ASM,不再依赖继承来实现,完美的解决了 JDK 的单一代理的不足。

文章中的所有代码、图片、文件都云存储在我的 GitHub 上:

(https://github.com/SingleYam/overview_java)

大家也可以选择通过本地下载

总结

您可能感兴趣的文章:

相关文章