基于JDK的动态代理机制知识点总结
- 更新时间:2020-06-04 08:53:38
- 编辑:沃德海
前言
『动态代理』其实源于设计模式中的代理模式,而代理模式就是使用代理对象完成用户请求,屏蔽用户对真实对象的访问。
举个最简单的例子,比如我们想要「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)
大家也可以选择通过本地下载。
总结
以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对码农之家的支持。
相关教程
-
jdk自带线程池知识点总结
在最近做的一个项目中,需要大量的使用到多线程和线程池,下面就java自带的线程池和大家一起分享
发布时间:2019-12-30
-
深入分析JDK1.6集合框架bug 6260652
这篇文章主要为大家解析了JDK1.6集合框架bug:c.toArray might (incorrectly) not return Object[] (see 6260652),具有一定的参考价值,感兴趣的小伙伴们可以参考一下
发布时间:2020-02-10
-
java8、jdk8日期与字符串转化
在本篇文章中小编给大家整理了关于java8、jdk8日期转化成字符串的相关知识点和代码,需要的朋友们学习下。
发布时间:2020-01-27
-
详细介绍jdk自带定时器使用方法
这篇文章主要为大家详细介绍了jdk自带定时器的使用方法,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
发布时间:2020-01-28
-
JDK和Tomcat的安装与配置步骤详解
这篇文章主要介绍了JDK和Tomcat的安装与配置方法,本文给大家介绍的非常详细,具有一定的参考借鉴价值,需要的朋友参考下吧
发布时间:2020-02-19
-
JavaScript之美
阅读文章这书好像是坐着来与一些JavaScript大师共进午餐,听她们探讨她们现场不断涌现出去的新念头。JavaScript能够说成全世界*异议和别人误会*多的程序语言。许多人企图用别的语言替代它的
大小:94 MBJavaScript电子书
-
深度解析Java游戏服务器开发
以便协助想掌握新手入门服务器开发设计的从业者或式从业者快速把握Java服务器开发设计的技术性,这书从手机游戏的行业现状、Java技术性、手机游戏逻辑性、数据库系统、网络理论、服务器
大小:314.4 MBJava开发电子书
-
Java多线程编程实战指南:核心篇
Java多线程编程实战指南以基本概念、原理与方法为主线,辅以丰富的实战案例和生活化实例,从Java虚拟机、操作系统和硬件多个层次与角度出发,循序渐进介绍Java平台下的多线程编程核心技术及相关工具
大小:172.6 MBJava编程电子书
-
Java函数式编程
这是一本关于函数式编程的书,由浅入深地介绍了函数式编程的思维方式,非常适合对Java有所了解的程序员,欢迎下载
大小:205.8 MBJava编程电子书
-
Effective Java中文版(第3版)
Java之父James Gosling鼎力推荐、Jolt获奖作品全新升级,针对Java 7、8、9全面更新,Java程序员必备参考书。包含大量完整的示例代码和透彻的技术分析,通过90条经验法则,探索新的设计模式和语言
大小:197.6 MBjava电子书
-
Java编程实战宝典
这是一本百科全书式的Java编程秘笈,以J2SE为平台,以新的JDK1.7技术规范为切入点,全面、系统地介绍了Java的基础编程技术和常用开发方法,实例丰富,特别适合想全面自学Java开发技术的人员阅读
大小:154.4 MBJava编程电子书
-
图解Java多线程设计模式
本书通过具体的Java 程序,以浅显易懂的语言逐一说明了多线程和并发处理中常用的12 种设计模式。内容涉及线程的基础知识、线程的启动与终止、线程间的互斥处理与协作、线程的有效应用、线程的数量管理以及性能优化的注意事项等。
大小:86.3 MBJava多线程电子书
-
Java机器学习
利用Java机器学习常见库设计、构建、部署你自己的机器学习应用,包含机器学习基本概念、原理,Weka、Mahout、Spark等常见机器学习库的用法
大小:80.7 MBJava电子书
-
Java开发实例大全:基础卷
本书超级详尽的实例大全,源码分析的案头手册,提高效率的绝好帮手,45个方向,1201个实例案例,java编程类四库全书,分门别类常用编程实例,《java开发实战1200例》之全新升级
大小:176 MBJava实例电子书
-
JavaScript忍者秘籍
这是由jQuery库创始人编写的一本深入剖析JavaScript语言的书,从不同层次讲述了逐步成为JavaScript高手所需的知识,适合具备一定JavaScript基础知识的读者阅读
大小:38.6 MBJavaScript电子书