深入讲解基于JDK的 * 机制
作者:Single_Yam 发布时间:2022-07-19 07:13:32
前言
『 * 』其实源于设计模式中的代理模式,而代理模式就是使用代理对象完成用户请求,屏蔽用户对真实对象的访问。
举个最简单的例子,比如我们想要「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)
大家也可以选择通过本地下载。
来源:https://www.cnblogs.com/yangming1996/p/9254412.html
猜你喜欢
- 简单介绍快速排序(Quicksort) 是对 冒泡排序的一种改进。基本思想快速排序算法通过多次比较和交换来实现排序,其排序流程如下:(1)首
- ArrrayList是Java中经常被用到的集合,弄清楚它的底层实现,有利于我们更好地使用它。下图是ArrayList的UML图从图中我们可
- 一、创建字符串创建字符串的方式有很多种,当是常见的一般就三种1. 直接赋值(常用)String str = "hello worl
- 前言Java的StringUtil.isEmpty(str)和"".equals(str)都是用来判断字符串是否为空的方
- 高并发访问时,缓存、限流、降级往往是系统的利剑,在互联网蓬勃发展的时期,经常会面临因用户暴涨导致的请求不可用的情况,甚至引发连锁反映导致整个
- 1.springBoot的依赖确定项目中包含可以注解的依赖<dependency> <group
- Java 异步实现的几种方式1. jdk1.8之前的Futurejdk并发包里的Future代表了未来的某个结果,当我们向线程池中提交任务的
- 1.引入依赖,版本3.0.0只引入一个即可<dependency> &n
- spring boot诞生的背景在spring boot出现以前,使用spring框架的程序员是这样配置web应用环境的,需要大量的xml配
- 假如是在同一台机器上开发,前后端分离的工程中出现跨域问题的原因是,前端工程和后端工程运行在不同的端口上。只要协议、域名、端口有一个不同就会产
- ES是一个基于Lucene的分布式全文搜索服务器,和SQL Server的全文索引(Fulltext Index)有点类似,都是基于分词和分
- 本文实例讲述了C++实现的O(n)复杂度内查找第K大数算法。分享给大家供大家参考,具体如下:题目:是在一组数组(数组元素为整数,可正可负可为
- 前言现在很多web应用,做过web项目的童鞋都知道,web结果由html+js+css组成,html结构都有一定的规范,数据动态交互可以通过
- 前言本节将介绍如何设置和使用MongoDB 驱动程序,通过 java实现与MongoDB服务端的通信功能,用户可以在此基础上进行各种Java
- Spring Boot1.为什么要使用 Spring Boot因为Spring, SpringMVC 需要使用的大量的配置文件 (xml文件
- 图片解析:1.生成字节码文件的过程可能产生编译时异常(checked),由字节码文件到在内存中加载、运行类此过程可能产生运行时异常(unch
- 可以用于简单的过期订单取消支付、7天自动收货场景中1、Spring Boot整合redis 参考https://www.jb51.net/a
- 一、实体类转换成XML将实体类转换成XML需要使用XmlSerializer类的Serialize方法,将实体类序列化public stat
- 要求:用DateTimeFormatter实现: 用扫描器获取输入的时间(年月日时分),这个时间的格式是常用的格式,然后格式化这个时间,把格
- 关于java图片验证码的文章最近更新了不少,帮助大家掌握java验证码的生成技术,下文为大家分享了java生成图片验证码最简单的方法,供大家