当前位置:首页 > 编程教程 > java技术文章 > 深入讲解基于JDK的动态代理机制

基于JDK的动态代理机制知识点总结

  • 发布时间:
  • 作者:码农之家
  • 点击:102

这篇文章主要知识点是关于jdk动态代理、jdk的动态代理、jdk动态代理原理、的内容,如果大家想对相关知识点有系统深入的学习,可以参阅以下电子书

阿里巴巴Java开发手册
  • 类型:Java开发大小:27.5 MB格式:PDF作者:杨冠宝
立即下载

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

前言

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

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

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

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

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

静态代理

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

JDK 动态代理机制

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

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

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

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

定义一个 Handler 处理类:

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

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

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

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

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

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

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

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

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

-Dsun.misc.ProxyGenerator.saveGeneratedFiles=true

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

一些缺陷与不足

单一的代理机制

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

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

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

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

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

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

不友好的返回值

深入讲解基于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)

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

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对码农之家的支持。

以上就是本次给大家分享的关于java的全部知识点内容总结,大家还可以在下方相关文章里找到相关文章进一步学习,感谢大家的阅读和支持。

您可能感兴趣的文章:

  • jdk自带线程池知识点总结
  • 深入分析JDK1.6集合框架bug 6260652
  • java8、jdk8日期与字符串转化
  • 详细介绍jdk自带定时器使用方法
  • JDK和Tomcat的安装与配置步骤详解
  • 相关电子书
    学习笔记
    网友NO.271744

    Linux中JDK安装配置教程

    JDK在官网下载,也可以从某个文件服务器进行下载。 1.在usr目录创建一个java文件夹: mkdir java 2.进入刚创建的java文件夹,下载jdk: cd /usr/java 例如: wget http://www.xxxx.com...jdk-8-linux-x64.tar.gz 3.解压jdk,在/usr/java目录下看到一个jdk1.8.0的目录: tar -zxvf jdk-8-linux-x64.tar.gz 4.删除原压缩包: rm -rf jdk-8-linux-x64.tar.gz 5.设置JDK的路径,设置环境变量: vi /etc/profile 点击 i 键编辑 在文件最后面加: #set java environmentJAVA_HOME=/user/java/jdk1.8.0CLASSPATH=.:$JAVA_HOME/lib/tools.jarPATH=$JAVA_HOME/bin:$PATHexport JAVA_HOME CLASSPATH PATH 点击 ESC键 + :wq + Enter键 退出并保存。 查看刚进行的编辑: cat /etc/profile 要使JDK在所有的用户中使用,可以这样: vi /etc/profile.d/java.sh 在新的java.sh中输入以下内容: #set java environmentJAVA_HOME=/user/java/jdk1.8.0CLASSPATH=.:$JAVA_HOME/lib/tools.jarPATH=$JAVA_HOME/bin:$PATHexport JAVA_HOME CLASSP……

    网友NO.721741

    CentOS 7下JDK8的详细安装步骤

    本文为大家分享了CentOS 7下安装JDK8的详细步骤,供大家参考,具体内容如下 一、下载JDK 至oracle官网下载,如图所示 二、安装JDK 把rpm文件拷贝至/usr/java,如果没有java文件夹, mkdir /usr/java/ 使用 npm -ivh jdk 文件名安装,如图所示 三、配置环境变量 使用命令 vi /etc/profile,插入图中红色部分: 注意=两边不要有空格 使用:wq保存退出,然后输入source /etc/profile和java -version,如图示表示安装成功,默认安装在/usr/java目录下 以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持码农之家。 ……

    网友NO.682695

    关于解决iReport4.1.1无法正常启动或者闪退或者JDK8不兼容的问题

    公司里面再用iReport的时候,我也安装使用了. 但是面临一个问题 我的iReport始终不能打开,困绕了老半天 但是是软件问题不方便问大佬自己研究了老大一会儿 加载之后就闪退了!!! 最后还是老大还是跟我说了需要JDK1.7才可以,我的电脑安装了两个jdk 一个是1.7的一个是1.8的 平时用的1.8 所以我开发的时候一直都是1.8 突然换成1.7的很麻烦 所以经过这两天的研究 我更改了配置文件,使得它单独强制的去使用jdk1.7来启动 1.我们打开\iReport-4.1.1\etc\ireport.conf配置文件 原来的是这样的,几乎所有的配置都是注释掉的,说明是默认的配置,在jdkhome这个配置上面说明他的是jdk的路径 2. 1. 我打开了这个配置,并且使用了我jdk1.7的路径 ,这样就可以正常的打开了.而且不会影响你是eclipse或者idea进行开发. 2. 成功的解决了 为了使用irport需要更换jdk版本的问题 总结 以上所述是小编给大……

    <
    1
    >

    Copyright 2018-2020 www.xz577.com 码农之家

    版权投诉 / 书籍推广 / 赞助:520161757@qq.com