JDK 动态代理从入门到掌握

快速入门

本文介绍 JDK 实现的动态代理及其原理,通过 ProxyGenerator 生成的动态代理类字节码文件

环境要求

要求原因
JDK 8 及以下在 JDK 9 之后无法使用直接调用 ProxyGenerator 中的方法,不便于将动态代理类对应的字节码文件输出
lombok为了使用 @SneakyThrows,避免异常处理代码对主体逻辑的干扰

基本概念

术语描述
目标类可以是任意一个现有的类
代理类对目标类进行功能的扩展,最简单的方式就是继承目标类,然后重写目标类的方法
动态代理类动态代理是实现代理的一种方式,不需要手动去继承目标类,不会写死,通用灵活
原始方法目标类中的方法
增强方法代理类中的方法,增强方法的逻辑处理中包含原始方法的处理逻辑,并且含有额外的扩展逻辑

案例准备

目标类(被代理类)和接口

JDKProxy 这种代理方式必须提供接口,从 Proxy.newProxyInstance() 方法要求的参数也可以看出来,实际上需要的是接口。这是 JDKProxy 的要求,不是动态代理的要求。

public interface IService {void show(String msg);
}

目标类是需要被增强的类,使用代理的目的是为了在不修改代码的情况下对目标类原有的功能进行增强扩展,使用动态代理的目的是为了减少手动创建的代理类

public class BaseServiceImpl implements IService {@Overridepublic void show(String msg) {System.out.println(msg);}
}

InvocationHandler(增强方法、核心)

通用的增强方法需要提供目标类对象,在 InvocationHandler 对象的 invoke 方法中调用目标类对象 target 的原始方法,是一种委托模式。

public class MyInvocationHandler implements InvocationHandler {private final Object target;public MyInvocationHandler(Object target) {this.target = target;}@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {System.out.println("===========method before==========");// 固定模板,调用原始方法,前后输出代表自定义的增强逻辑Object result = method.invoke(target, args);System.out.println("===========method after===========");return result;}
}

动态代理案例演示

理解 JDK 动态代理需要回答下面三个问题:

  • 为什么 MyInvocationHandler 类的设计中添加一个成员变量 target,并且在构造方法中强制要求使用者传入这个对象?
  • 通过 Proxy.newProxyInstance() 这个方法便得到了代理类对象,那一个 object 就有一个 Object 类,那么这个对象对应的类是怎么样的?
  • 代理类对象调用增强方法的执行逻辑是怎样的,它是如何和原始方法产生关联的?
public class Demo{public static void main(String[] args) {// 1. 创建InvocationHandler对象IService baseService = new BaseServiceImpl();InvocationHandler invocationHandler = new MyInvocationHandler(baseService);// 2. 通过JDKProxy生成代理类对象IService serviceProxy = (IService) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{IService.class}, invocationHandler);// 3. 调用代理类对象中的增强方法serviceProxy.show("Hello World");serviceProxy.toString(); //这是默认被增强的三个方法之一}
}
  • 首先,我们要理解 invoke() 的设计思路是什么,它对应的是一个通用的、增强后的方法

    既然是增强方法,那么就需要调用原始方法,因此需要一个目标类对象,所以在 MyInvocationHandler 这个类中有一个 target 用来接收该对象,并且是通过构造方法强制要求使用者来提供。这就是为什么要求在 InvocationHandler 中提供一个目标类对象。(回答第一个问题)

  • 其次,代理类对象对应的这个动态代理类,在 Proxy.newProxyInstance() 的底层逻辑中是通过 ProxyGenerator 来生成的。后面将使用 ProxyGenerator模拟该过程,并额外将动态代理类对应的字节码输出到文件中进行查看,在 JDK 9 之后我们不能够直接调用这个类,因此推荐使用 JDK 8。(未完全回答)

  • 最后,这个问题在回答上面两个问题之后通过流程图解释。(未回答)

JDKProxy 原理

ProxyGenerator 使用

在回答第二个问题之前,先了解 ProxyGenerator 如何使用。输出的字节码文件是 classpath 下的 ServiceJDKProxy.class。

public class ProxyGeneratorDemo{@SneakyThrowspublic static void main(){// 参数配置:生成的代理类的名称String classpath = ClassLoader.getSystemResource("").getFile().substring(1);String proxyClassName = "ServiceJDKProxy";File classFile = Paths.get(classpath, proxyClassName + ".class").toFile();// 1. 主体逻辑就一行代码,对目标类的所有接口进行代理,这里决定了JDKProxy是对接口的代理byte[] bytes = ProxyGenerator.generateProxyClass(proxyClassName, BaseServiceImpl.class.getInterfaces());// 2. 输出到指定文件中FileOutputStream fos = new FileOutputStream(classFile);fos.write(bytes);// 关闭流fos.flush();fos.close();}
}

跟踪 generateProxyClass 方法可以进入到 generateClassFile 方法中,看到下面的这一段逻辑,所以 JDK 动态代理会通过反射的方式,来获取传递的接口数组中的所有方法,并对这些方法进行增强。这能够回答两个问题:(a)JDK 动态代理模式中对哪些方法进行增强(接口的所有方法),(b)代理类对象是如何获取到目标类的方法对象的(反射遍历)

public class ProxyGenerator {private byte[] generateClassFile() {// 省略...for (Class<?> intf : interfaces) {for (Method m : intf.getMethods()) {// 从设计角度来看,如果需要过滤一些方法对象,按照责任链模式设计,需要额外保存一个列表,里面是一条一条的过滤规则addProxyMethod(m, intf);}}// 省略...}
}

动态代理类字节码文件

字节码反编译之后对应的 Java 源代码如下(经过适当调整),这回答了第二个问题。至于这个字节码是如何生成的,具体可以看源码的操作流程,本质上是按照JVM 字节码规范在对应的位置上填充数据,由于方法通过遍历已经获取到了,因此。

public final class ServiceJDKProxy // 父类是Proxyextends Proxy// 实现要求代理的所有接口implements IService {private static Method m0;private static Method m1;private static Method m2;private static Method m3;static {// 除了接口中的方法,默认会获取Object类中的三个方法:hashCode、equals、toString,因此调用代理对象的toString方法也会被增强m0 = Class.forName("java.lang.Object").getMethod("hashCode");m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));m2 = Class.forName("java.lang.Object").getMethod("toString");// 调用Proxy.newProxyInstance()时传入一个接口数组// 这里会获取接口数组中所有接口的所有方法,目前只传递一个接口,并且该接口中只有一个方法,因此只显示一个方法m3 = Class.forName("org.example.IService").getMethod("show", Class.forName("java.lang.String"));}public ServiceJDKProxy(InvocationHandler invocationHandler) {// 关键点1:调用父类构造器,即Proxy类的构造器super(invocationHandler);}public final int hashCode() {return (Integer)super.h.invoke(this, m0, (Object[])null);}public final boolean equals(Object args) {return (Boolean)super.h.invoke(this, m1, new Object[]{args});}public final String toString() {return (String)super.h.invoke(this, m2, (Object[])null);}// 关键点2:所有的方法参数构造成一个字符串,然后再将该特殊格式的字符串反解析成一个Object[],实现参数的传递//(类比JSON序列化和反序列化)public final void show(String args) {// 关键点3:super.hsuper.h.invoke(this, m3, new Object[]{args});}
}

动态代理原理图

从上面的字节码文件中,可以梳理出 JDK 动态代理的原理图如下:

  1. 动态代理类和目标类之间没有任何关系,只是共同实现了指定接口

  2. 动态代理类的父类是 Proxy,在父类构造方法中注入了 InvocationHandler 对象,所以后面通过 super.h.invoke() 实质上就是注入的 MyInvocationHandler 对象中的 invoke 方法,也就是自定义 MyInvocationHandler 的 invoke 方法

  3. 在动态代理类中所有的增强方法本质上都是去调用了 InvocationHandler 对象的 invoke 方法,只是传递的参数不同而已

    public class Proxy implements java.io.Serializable {protected InvocationHandler h;protected Proxy(InvocationHandler h) {Objects.requireNonNull(h);this.h = h;}
    }
    

请添加图片描述

图:JDK 动态代理原理图

下面内存结构需要额外注意的是 Method 对象是指目标类中的方法对象,在动态代理类中所有的增强方法本质上都是去调用了 InvocationHandler 对象的 invoke 方法,只是传递的参数不同而已。

InvocationHandler 对象是连接代理类对象和目标类对象的核心,Proxy 是作为父类。

在这里插入图片描述

图:动态代理类内存结构

增强方法的调用过程

现在我们可以来回答最后一个问题,增强方法是如何被调用的:

  1. 动态代理类中的所有增强方法本质上都是调用 InvocationHandler 对象的 invoke 方法(动态代理类的生成规则)
  2. 动态代理类将目标类中的方法对象 Method(当前调用方法的同名对象)传递给 InvocationHandler 对象
  3. InvocationHandler 对象中此时具有 Method 对象(原始方法)和 Traget 对象(目标类),此时便可以调用到原始方法

在这里插入图片描述

图:增强方法的调用过程

总结

JDK 动态代理的核心是对代理类的增强方法和目标类的原始方法对象的进行动态绑定(这部分是 JDK 源码做的事情);

而作为 JDK Proxy 的使用者,我们使用动态代理的核心就是正确地设计自定义的 InvocationHandler 类,也就是传入目标类对象

从调用过程中来看,JDK 完成前半部分的绑定工作,使用者完成后半部分 Target 对象的注入和方法调用工作。

Proxy.newProxyInstance() 的主要有两个作用:

  1. 拦截接口数组中的所有方法,创建代理类
  2. 为 Proxy 注入 InvocationHandler 对象,而 InvocationHandler 对象则是连接代理类对象和目标类对象的关键。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/234758.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

性能测试:系统架构性能优化

今天谈下业务系统性能问题分析诊断和性能优化方面的内容。这篇文章重点还是谈已经上线的业务系统后续出现性能问题后的问题诊断和优化重点。 系统性能问题分析流程 我们首先来分析下如果一个业务系统上线前没有性能问题&#xff0c;而在上线后出现了比较严重的性能问题&#x…

Android Studio Giraffe版本遇到的问题

背景 上周固态硬盘挂了&#xff0c;恢复数据之后&#xff0c;重新换了新的固态安装了Win11系统&#xff0c;之前安装的是Android Studio 4.x的版本&#xff0c;这次也是趁着新的系统安装新的Android开发工具。 版本如下&#xff1a; 但是打开以前的Android旧项目时&#xff…

vue3 keep-alive页面切换报错:parentComponent.ctx.deactivate is not a function

问题&#xff1a; <router-view v-slot"{ Component }"><keep-alive ><component :is"Component" v-if"$route.meta.keepAlive" /></keep-alive><component :is"Component" v-if"!$route.meta.keepA…

PLC:200smart(13-16章)

PLC&#xff1a;200smart 第十三章2、带参子程序3、将子程序设置成库文件 第十三章 项目ValueValue主程序MAIN一个项目只能有一个&#xff0c;循环扫描子程序SBR_0项目中最多有128个&#xff0c;只有在调用时 才执行&#xff08;子程序可以嵌套其他子程序&#xff0c;最多八层…

STM32 启动文件分析

STM32 启动文件分析 基于STM32F103VET6芯片的 startup_stm32f10x_hd.s 启动文件分析 设置栈&#xff0c;将栈的大小Stack_Size设置为0x00004900&#xff08;18688/102418KB&#xff09;&#xff0c;即局部变量不能大于18KB。&#xff08;EQU等值指令&#xff0c;将0x0000490…

fiddler弱网测试实践

准备工作 1、fiddler安装包 2、一部安卓手机 一、fiddler安装 安装fiddler到电脑上&#xff0c;傻瓜式安装即可 二、fiddler环境配置 三、手机端环境配置 1、获取电脑的IP地址&#xff1a;WindowsR&#xff0c;输入cmd弹出命令窗口&#xff0c;输入命令ipconfig 或者鼠标…

无mac电脑生成uniapp云打包私钥证书的攻略

uniapp顾名思义是一个跨平台的开发工具&#xff0c;大部分uniapp的开发者&#xff0c;其实并没有mac电脑来开发&#xff0c;但是生成ios的证书&#xff0c;官网的教程却是需要mac电脑的&#xff0c;那么有没有办法无需mac电脑即可生成uniapp云打包的私钥证书呢&#xff1f; 下…

虚幻学习笔记1—给UI添加动画

一、前言 本文所使用的虚幻版本为5.3.2&#xff0c;之前工作都是用unity&#xff0c;做这类效果用的最多的是一个DoTween的插件&#xff0c;在虚幻中都内置集成了这这种效果制作。 图1.1 UI动画 二、过程 1、首先&#xff0c;在诸如按钮、图像等可交互控件中选中&#xff0c;如…

vue项目通过HBuilder打包成apk,实现apk自动更新下载

vue 项目通过 HBuilder 打包成 apk&#xff0c;实现 apk 自动更新下载 1、vue 项目通过 HBuilder 打包成 apk vue 项目在终端执行 npm run build 打包成 dist 文件&#xff0c;生成的 dist 文件在 项目根目录下 在 HBuilder 中 新建一个项目 默认选择 5APP 的默认模板项目…

43.0BaseDao抽取dao公共父类

43.1. 回顾 1. 把数据库表中查询的结果封装到一个实体类中。 命名规则:类名和表名一致 类中属性和表的字段对应。 表中的一条记录对应实体的一个对象 多条记录→集合 43.2. 正文 目录 43.1. 回顾 43.2. 正文 43.3. 抽取dao公共父类。 43.4. 引入数据源 43.3. 抽取dao公共…

小米的算法部署岗对新手是真的友好

大家好啊&#xff0c;我是董董灿。 自从开始写一些AI行业的岗位介绍&#xff0c;就养成了一个习惯&#xff0c;在上下班的路上经常就会打开某聘瞧一瞧。 导致之前一年不看的某聘认为我要看机会换工作&#xff0c;疯狂给我推猎头&#xff0c;然后电话就进来了。 不堪骚扰的我…

EG20网口远程下载程序使用案例

EG20网口远程下载程序使用案例 前言&#xff1a;本文档主要说明了使用蓝蜂虚拟网络工具通过EG20网关的网口&#xff08;LAN口&#xff09;远程给PLC下载程序的步骤及其注意事项。使用蓝蜂虚拟网络工具&#xff0c;不仅支持程序的远程下载&#xff0c;同样支持程序的远程上传与…