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

前言

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

举个最简单的例子,比如我们想要「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)

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

总结

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

(0)

相关推荐

  • 深入理解java动态代理的两种实现方式(JDK/Cglib)

    什么是代理模式? 代理模式:在调用处不直接调用目标类进行操作,而是调用代理类,然后通过代理类来调用目标类进行操作.在代理类调用目标类的前后可以添加一些预处理和后处理操作来完成一些不属于目标类的功能. 为什么要使用代理模式? 通过代理模式可以实现对目标类调用的控制.在目标类调用前/后进行一些不属于目标类的操作,如:数据验证.预处理.后处理.异常处理等 什么是静态代理什么是动态代理? 静态代理:代理类只能实现对"特定接口的实现类"进行代理 动态代理:代理类可以实现对多种类的代理 jdk代理

  • Spring AOP注解失效的坑及JDK动态代理

    @Transactional @Async等注解不起作用 之前很多人在使用Spring中的@Transactional, @Async等注解时,都多少碰到过注解不起作用的情况. 为什么会出现这些情况呢?因为这些注解的功能实际上都是Spring AOP实现的,而其实现原理是通过代理实现的. JDK动态代理 以一个简单的例子理解一下JDK动态代理的基本原理: //目标类接口 public interface JDKProxyTestService { void run(); } //目标类 publ

  • 浅谈Java代理(jdk静态代理、动态代理和cglib动态代理)

    一.代理是Java常用的设计模式,代理类通过调用被代理类的相关方法,并对相关方法进行增强.加入一些非业务性代码,比如事务.日志.报警发邮件等操作. 二.jdk静态代理 1.业务接口 /** * 业务接口 * @author pc * */ public interface UserService { // 增加一个用户 public void addUser(); // 编辑账户 public void editUser(); } 2.业务实现类 /** * 业务实现类 * @author pc

  • 深度剖析java中JDK动态代理机制

    摘要 相比于静态代理,动态代理避免了开发人员编写各个繁锁的静态代理类,只需简单地指定一组接口及目标类对象就能动态的获得代理对象. 代理模式 使用代理模式必须要让代理类和目标类实现相同的接口,客户端通过代理类来调用目标方法,代理类会将所有的方法调用分派到目标对象上反射执行,还可以在分派过程中添加"前置通知"和后置处理(如在调用目标方法前校验权限,在调用完目标方法后打印日志等)等功能. 使用动态代理的五大步骤 1.通过实现InvocationHandler接口来自定义自己的Invocati

  • Java JDK动态代理的基本原理详细介绍

    JDK动态代理详解 本文主要介绍JDK动态代理的基本原理,让大家更深刻的理解JDK Proxy,知其然知其所以然.明白JDK动态代理真正的原理及其生成的过程,我们以后写JDK Proxy可以不用去查demo,就可以徒手写个完美的Proxy.下面首先来个简单的Demo,后续的分析过程都依赖这个Demo去介绍,例子采用JDK1.8运行. JDK Proxy HelloWorld package com.yao.proxy; /** * Created by robin */ public inter

  • Java JDK动态代理(AOP)的实现原理与使用详析

    本文主要给大家介绍了关于Java JDK动态代理(AOP)实现原理与使用的相关内容,分享出来供大家参考学习,下面来一起看看详细的介绍: 一.什么是代理? 代理是一种常用的设计模式,其目的就是为其他对象提供一个代理以控制对某个对象的访问.代理类负责为委托类预处理消息,过滤消息并转发消息,以及进行消息被委托类执行后的后续处理. 代理模式UML图: 简单结构示意图: 为了保持行为的一致性,代理类和委托类通常会实现相同的接口,所以在访问者看来两者没有丝毫的区别.通过代理类这中间一层,能有效控制对委托类对

  • java动态代理(jdk与cglib)详细解析

    JAVA的动态代理 代理模式 代理模式是常用的java设计模式,他的特征是代理类与委托类有同样的接口,代理类主要负责为委托类预处理消息.过滤消息.把消息转发给委托类,以及事后处理消息等.代理类与委托类之间通常会存在关联关系,一个代理类的对象与一个委托类的对象关联,代理类的对象本身并不真正实现服务,而是通过调用委托类的对象的相关方法,来提供特定的服务. 按照代理的创建时期,代理类可以分为两种. 静态代理:由程序员创建或特定工具自动生成源代码,再对其编译.在程序运行前,代理类的.class文件就已经

  • java jdk动态代理详解

    jdk动态代理要对一个类进行代理,被代理的类必须实现至少一个接口,并且只有接口中的方法才能被代理. jdk实现动态代理一般分为三步: 1. 编写接口和实现类. 2. 写一个处理器,该处理器实现InvocationHandler接口,该接口只有一个方法,其签名为public Object invoke(Object proxy, Method method, Object[] args)throws Throwable;可在该处理器的实现方法中,在方法调用前和调用后加入自己的代码,从而进行动态拦截

  • java代理 jdk动态代理应用案列

    java代理有jdk动态代理.cglib代理,这里只说下jdk动态代理,jdk动态代理主要使用的是java反射机制(既java.lang.reflect包) 原理是(歌手.经纪人做例子): 建立一个公共的接口,比如:歌手public interface Singer: 用具体的类实现接口,比如:周杰伦,他是歌手所以实现Singer这个类,class MySinger implements Singer 建立代理类,这里也就是经纪人,他需要实现InvocationHandler类,并重写invok

  • Java JDK 动态代理的使用方法示例

    本文主要和大家分享介绍了关于Java JDK 动态代理使用的相关内容,分享出来供大家参考学习,下面来一起看看详细的介绍: 前言 代理是一种常用的设计模式,其目的就是为其他对象提供一个代理以控制对某个对象的访问.代理类负责为委托类预处理消息,过滤消息并转发消息,以及进行消息被委托类执行后的后续处理. Spring AOP的实现对于接口来说就是使用的JDK的动态代理来实现的,而对于类的代理使用CGLIB来实现. JDK的动态代理,就是在程序运行的过程中,根据被代理的接口来动态生成代理类的class文

随机推荐