从设计上理解JDK动态代理

作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO

联系qq:184480602,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬

照理说,动态代理经过前面3篇介绍,该讲的都已经讲完了,再深入下去的意义不是特别大。但看到群里有小伙伴说对InvocationHandler#invoke()方法的参数有些困惑,所以又补了一篇。

关于这三个参数,其实一句话就能讲完:

  • Object proxy:很遗憾,是代理对象本身,而不是目标对象(不要调用,会无限递归,一般不会使用)
  • Method method:方法执行器,用来执行方法(有点不好解释,Method只是一个执行器,传入目标对象就执行目标对象的方法)
  • Obeject[] args:方法参数

上一篇也是这么介绍的,但大家的接受度似乎不是很好,甚至对参数怎么传进来的感到困惑,所以本篇打算站在Proxy类设计的角度分析三个参数的由来。

当然啦,我还远不敢说能写出JDK级别的代码。本文虽然也尝试编写MyProxy类,但它是用来解释参数由来的,意义并不在于完美复刻JDK Proxy。

山寨Proxy类概览

先看一下我编写的山寨MyProxy和MyInvocationHandler吧:

是不是和上面原版的JDK Proxy几乎一模一样呢?激动吗?一起来看看我是怎么设计的(不要细想代码逻辑,这根本是伪代码,跑不起来的)。

/*** 山寨Proxy类*/
public static class MyProxy implements java.io.Serializable {protected MyInvocationHandler h;private MyProxy() {}protected MyProxy(MyInvocationHandler h) {Objects.requireNonNull(h);this.h = h;}public static Object newProxyInstance(ClassLoader classLoader,Class<?>[] interfaces,MyInvocationHandler h) throws Exception {// 拷贝一份接口Class(接口可能有多个,所以拷贝的Class也有多个)final Class<?>[] interfaceCls = interfaces.clone();// 这里简化处理,只取第一个Class<?> copyClazzOfInterface = interfaceCls[0];// 获取Proxy带InvocationHandler参数的那个有参构造器Constructor<?> constructor = copyClazzOfInterface.getConstructor(MyInvocationHandler.class);// 创建一个Proxy代理对象,并把InvocationHandler塞到代理对象内部,返回代理对象return constructor.newInstance(h);}}/*** 山寨InvocationHandler接口*/
interface MyInvocationHandler {Object invoke(Object proxy, Method method, Object[] args) throws Throwable;
}

也就是说,上面的设计思路就是之前分析的这张图:

目前上面的MyProxy有两个问题没解决:

  • 返回的代理对象只是Proxy类型的,没法强转为目标接口类型
  • 返回的代理对象即使能调用接口的同名方法,如何最终调用到它内部的InvocationHandler#invoke()呢

底层原理

上面遗留的两个问题,其实换种说法就是:

  • 怎么让MyProxy的实例对象变成代理类的对象呢(比如Calculator)?
  • InvocationHandler#invoke()怎么调用到目标对象同名方法?

首先我们要明确,MyProxy(JDK Proxy同理)是一个已经写好的类,一开始就没有实现Calculator接口,那么它的实例对象肯定是无法强转为Calculator的。那么,Java是如何解决这个问题的呢?方式很简单粗暴,因为JVM确确实实在运行时动态构造了代理类,并让代理类实现了接口,也就是我们经常看到的$Proxy0。

也就是说,我们通常理解的代理对象,并不是JDK Proxy的直接实例对象,而是JDK Proxy的子类$Proxy0的实例对象,而$Proxy0 extends Proxy implements Calculator

由于$Proxy0是运行时的产物,一旦程序停止便会消失,我们需要借助阿里开源的Arthas工具来观察并验证。

假设需要代理的接口是:

/*** 需要代理的接口*/
interface Calculator {int add(int a, int b);
}

当我们期望使用Proxy创建代理对象时,JDK会先动态生成一个代理类$Proxy0:

final class $Proxy0 extends Proxy implements InvocationHandlerTest.Calculator {private static Method m1;private static Method m2;private static Method m3;private static Method m0;public $Proxy0(InvocationHandler invocationHandler) {super(invocationHandler);}static {try {m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]);m3 = Class.forName("com.bravo.demo.InvocationHandlerTest$Calculator").getMethod("add", Integer.TYPE, Integer.TYPE);m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]);return;}catch (NoSuchMethodException noSuchMethodException) {throw new NoSuchMethodError(noSuchMethodException.getMessage());}catch (ClassNotFoundException classNotFoundException) {throw new NoClassDefFoundError(classNotFoundException.getMessage());}}public final int add(int n, int n2) {try {return (Integer)this.h.invoke(this, m3, new Object[]{n, n2});}catch (Error | RuntimeException throwable) {throw throwable;}catch (Throwable throwable) {throw new UndeclaredThrowableException(throwable);}}public final boolean equals(Object object) {try {return (Boolean)this.h.invoke(this, m1, new Object[]{object});}catch (Error | RuntimeException throwable) {throw throwable;}catch (Throwable throwable) {throw new UndeclaredThrowableException(throwable);}}public final String toString() {try {return (String)this.h.invoke(this, m2, null);}catch (Error | RuntimeException throwable) {throw throwable;}catch (Throwable throwable) {throw new UndeclaredThrowableException(throwable);}}public final int hashCode() {try {return (Integer)this.h.invoke(this, m0, null);}catch (Error | RuntimeException throwable) {throw throwable;}catch (Throwable throwable) {throw new UndeclaredThrowableException(throwable);}}
}

简化无关代码:

// 1.自动实现目标接口,所以代理对象可以转成Calculator
final class $Proxy0 extends Proxy implements InvocationHandlerTest.Calculator {private static Method m3;public $Proxy0(InvocationHandler invocationHandler) {super(invocationHandler);}static {// 2.获取目标方法Methodm3 = Class.forName("com.bravo.demo.InvocationHandlerTest$Calculator").getMethod("add", Integer.TYPE, Integer.TYPE);}public final int add(int n, int n2) {// 3.通过InvocationHandler执行方法,现在你能理解invoke()三个参数的含义了吗?//   this:就是$Proxy0的实例,所以是代理对象,不是目标对象return (Integer)this.h.invoke(this, m3, new Object[]{n, n2});}}

最后一个问题是,代理对象$proxy调用add()时,是如何最终调用到目标对象的add()方法的呢?观察上面的代码可以发现,代理对象的方法调用都是通过this.h.invoke()桥接过去的,而这个h就是InvocationHandler,在$Proxy的父类Proxy中已经存在,而且会被赋值。

我编写的MyProxy基本上就是简化版的JDK Proxy,没有本质的区别。只不过JVM只认识JDK Proxy,只会给它生成动态代理类,所以我的MyProxy即使模仿到99.99%,也注定少了最关键的那一步,最终沦为一段玩具代码。

希望这篇文章能帮大家更了解JDK动态代理。至于Java是如何自动生成$Proxy代理类的,交给大家另外研究。很多读者让我讲讲CGLib,其实没啥好讲的,就是API使用而已,底层也是自动生成代理类/代理对象,和JDK动态代理很相似,只不过CGLib底层用的是ASM,感兴趣可以去百度一下。

作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO

进群,大家一起学习,一起进步,一起对抗互联网寒冬

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

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

相关文章

小程序可拖拽按钮

你有没有遇到过在页面中有一个固定在某个位置的按钮&#xff0c;永远的挡住了你想要看的区域&#xff1f; 在小程序的列表页面中&#xff0c;常常会有一个提报的入口固定在右下角&#xff0c;如果这个按钮不可拖动的话&#xff0c;可能会挡住下面的事件&#xff0c;让用户操作起…

GPU - cuda 安装

GPU - cuda 安装 环境搭建安装 0.确认你的电脑上有英伟达显卡 通过winR输入:control /name Microsoft.DeviceManager打开显示适配器&#xff0c;能看到显卡即可。我的版本是 3060 驱动版本 31.0.15.4617 1.查看主机显卡驱动版本. 主机GPU驱动版本决定你的主机最高能支持到什…

[架构之路-249]:目标系统 - 设计方法 - 软件工程 - 需求工程- 需求开发:如何用图形表达需求,结构化方法的需求分析

目录 一、概述 二、数据模型&#xff1a;E-R图/实体关系图&#xff08;数据单元之间的结构关系&#xff09; 三、功能模型&#xff1a;数据流图DFD&#xff08;逻辑运算&#xff0c;包括输入和输出&#xff0c;实体之间的关系&#xff09;&#xff1a;输入》处理 》 输出 四…

快速入门go语言学习笔记

文章目录 1、初识go1.1、go语言1.2 第一个Go程序 2、基础类型2.1、命名2.2、变量2.2.1 变量声明2.2.2 变量初始化2.2.3 变量赋值2.2.4 匿名变量 2.3、常量2.3.1 字面常量(常量值)2.3.2 常量定义2.3.3 iota枚举 2.4、基础数据类型2.4.1 分类2.4.2 布尔类型2.4.3 整型2.4.4 浮点型…

【漏洞复现】金蝶云星空管理中心 ScpSupRegHandler接口存在任意文件上传漏洞 附POC

漏洞描述 金蝶云星空是一款云端企业资源管理(ERP)软件,为企业提供财务管理、供应链管理以及业务流程管理等一体化解决方案。金蝶云星空聚焦多组织,多利润中心的大中型企业,以 “开放、标准、社交”三大特性为数字经济时代的企业提供开放的 ERP 云平台。服务涵盖:财务、供…

2022 第十四届蓝桥杯模拟赛第一期(题解与标程)

第十四届蓝桥杯模拟赛第一期 1. 二进制位数问题描述答案提交参考答案 2. 晨跑问题描述答案提交参考答案 3. 调和级数问题描述答案提交参考答案程序验证 4. 山谷问题描述答案提交参考答案 5. 最小矩阵问题描述答案提交参考答案 6. 核酸日期问题描述输入格式输出格式样例输入样例…

wagtail-安装配置

系列文章目录 文章目录 系列文章目录安装虚拟环境安装wagtail查看安装后的包 创建wagtail项目安装依赖迁移创建超级用户运行项目 安装虚拟环境 https://blog.csdn.net/gsl371/article/details/117917857 安装wagtail (wagenv) C:\djproject\wagprj>pip list Package V…

【MySQL】mysql中不推荐使用uuid或者雪花id作为主键的原因以及差异化对比

文章目录 前言什么是UUID?什么是雪花ID?什么是MySql自增ID?优缺点对比UUID:优点1.全球唯一性2.无需数据库支持 缺点1.存储空间大2.索引效率低3.查询效率低 雪花ID&#xff1a;优点1.分布式环境下唯一性 缺点1.依赖于机器时钟2.存储空间较大3.查询效率低 MYSQL自增:优点1.简单…

第五天 用Python批量处理Excel文件,实现自动化办公

用Python批量处理Excel文件&#xff0c;实现自动化办公 一、具体需求 有以下N个表&#xff0c;每个表的结构一样&#xff0c;如下&#xff1a; 需要把所有表数据汇总&#xff0c;把每个人的得分、积分分别加起来&#xff0c;然后按总积分排名&#xff0c;总积分一致时&#xff…

运放如何进行全波整流

对于一个双极性的交流信号&#xff0c;如果想要把负半轴的信号镜像到正半轴&#xff0c;我们可以接一个整流桥&#xff0c;这种叫做全波整流。 如果双极性的交流信号经过一个二极管&#xff0c;则交流信号的负半轴不能通过二极管&#xff0c;输出只有正半轴的信号&#xff0c;这…

010 OpenCV中的4种平滑滤波

目录 一、环境 二、平滑滤波 2.1、均值滤波 2.2、高斯滤波 2.3、中值滤波 2.4、双边滤波 三、完整代码 一、环境 本文使用环境为&#xff1a; Windows10Python 3.9.17opencv-python 4.8.0.74 二、平滑滤波 2.1、均值滤波 在OpenCV库中&#xff0c;blur函数是一种简…

【SpringMVC】 三层架构

一.lombok工具包 中央仓库查找这个工具包:https://mvnrepository.com/ 给类添加Data注解就可以获取gettter和setter方法 , 这样我们就不必写getter 和 setter 方法. 也可以给成员属性添加单独的getter 和 setter , 针对某个成员属性单独添加setter或setter方法. 二.如果使用spr…