从设计上理解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.mzph.cn/news/166788.shtml

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

相关文章

上门预约小程序开发优势

想要放松身心&#xff0c;享受按摩的舒适感&#xff1f;那就需要一个专业的按摩师来上门服务。我们开发的预约按摩小程序app系统&#xff0c;汇聚各类上门按摩服务&#xff0c;包括推拿SPA、小儿推拿、中医等&#xff0c;为您提供高价值、高标准的养生健康体验。24小时随时提供…

【前端】让列表像Excel单元格一样编辑

前言 领导说了一堆的话,最后总结一句就是客户很懒,客户的员工更加懒。 本着让别人节省时间的原则,提倡出了让列表和Excal的单元格一样,不仅看数据还可以随时更改数据。 查资料 根据 Jeecg-Vue3 源码介绍,从而知道是基于 Vben Admin 开源项目进行改造的。 因此在 Vben…

沉头孔和埋头孔的区别

埋头空和沉头孔的区别在于螺栓孔上部扩孔&#xff1a;沉头孔是直筒结构&#xff1b;埋头孔是四十五度结构&#xff0c;比沉头孔较为平顺。 螺栓孔上部扩孔能容纳螺栓头部&#xff0c;使螺头部不高于周围表面。埋头空和沉头孔只是两种不同的叫法。 沉头孔是 PCB 上的圆柱形凹槽…

将form表单中的省市区的3个el-select下拉框的样式调成统一的间隔距离和长度,vue3项目iot->供应商管理

省市区是用3个el-select组成的 在表单中用el-col&#xff0c;会导致3个下拉的距离不统一&#xff0c;市和区的前面也是不需要文字label的 如何解决:用vue3的:deep()进行样式穿透&#xff0c;由于el-form-item标签都是一样的&#xff0c;为了能准确的找到市的el-form-item&…

解决:前端js下载文件流出现“未知文件格式”错误

第一中情况&#xff1a; 出现的问题&#xff0c;前端已经设置了responseType: blob,下载下来还是格式不对。 最后经过排查&#xff0c;后端缺少charsetutf-8&#xff0c;所以前端可以设置编码&#xff1a; 第二中情况&#xff1a; 后端已经设置了charsetutf-8&#xff0c;前…

机器学习/sklearn 笔记:K-means,kmeans++,MiniBatchKMeans,二分Kmeans

1 K-means介绍 1.0 方法介绍 KMeans算法通过尝试将样本分成n个方差相等的组来聚类&#xff0c;该算法要求指定群集的数量。它适用于大量样本&#xff0c;并已在许多不同领域的广泛应用领域中使用。KMeans算法将一组样本分成不相交的簇&#xff0c;每个簇由簇中样本的平均值描…

数据结构-树

参考&#xff1a;https://www.hello-algo.com/chapter_tree/binary_tree/#711 1. 介绍 树存储不同于数组和链表的地方在于既可以保证数据检索的速度&#xff0c;又可以保证数据插入删除修改的速度&#xff0c;二者兼顾。 二叉树是一种很重要的数据结构&#xff0c;是非线性的…

【学习篇】Linux中grep、sed、awk

Linux 文本处理三剑客 – awk, sed, grep grep过滤文本 https://zhuanlan.zhihu.com/p/561445240 grep 是 Linux/Unix 系统中的一个命令行工具&#xff0c;用于从文件中搜索文本或字符串。grep 代表全局正则表达式打印。当我们使用指定字符串运行 grep 命令时&#xff0c;如…

Mysql并发时常见的死锁及解决方法

使用数据库时&#xff0c;有时会出现死锁。对于实际应用来说&#xff0c;就是出现系统卡顿。 死锁是指两个或两个以上的事务在执行过程中&#xff0c;因争夺资源而造成的一种互相等待的现象。就是所谓的锁资源请求产生了回路现象&#xff0c;即死循环&#xff0c;此时称系统处于…

星河创新,开拓新纪!2023“星河产业应用创新奖”报名全面开启!

科技的浪潮汹涌而至&#xff0c;人工智能正悄无声息地渗透进我们生活的每一个角落&#xff0c;成为推动社会奔腾向前的强大引擎。 随着大模型时代到来&#xff0c;更多的创新者涌现出来&#xff0c;他们正积极探索AI与实体的深度融合&#xff0c;解决行业难题&#xff0c;开拓…

算法的奥秘:种类、特性及应用详解(算法导论笔记1)

算法&#xff0c;是计算机科学领域的灵魂&#xff0c;是解决问题的重要工具。在算法的世界里&#xff0c;有着各种各样的种类和特性。今天&#xff0c;我将带各位踏上一段探索算法种类的旅程&#xff0c;分享一些常见的算法种类&#xff0c;并给出相应的实践和案例分析。希望通…

【目标检测】保姆级别教程从零开始实现基于Yolov8的一次性筷子计数

前言 一&#xff0c;环境配置 一&#xff0c;虚拟环境创建 二&#xff0c;安装资源包 前言 最近事情比较少&#xff0c;无意间刷到群聊里分享的基于百度飞浆平台的一次性筷子检测&#xff0c;感觉很有意思&#xff0c;恰巧自己最近在学习Yolov8&#xff0c;于是看看能不能复…

【Go语言从入门到实战】反射编程、Unsafe篇

反射编程 reflect.TypeOf vs reflect.ValueOf func TestTypeAndValue(t *testing.T) {var a int64 10t.Log(reflect.TypeOf(a), reflect.ValueOf(a))t.Log(reflect.ValueOf(a).Type()) }判断类型 - Kind() 当我们需要对反射回来的类型做判断时&#xff0c;Go 语言内置了一个…

【23真题】最简单的211!均分141分!

今天分享的是23年河海大学863的信号与系统试题及解析。 我猜测是由于23年太简单&#xff0c;均分都141分&#xff0c;导致24考研临时新增一门数字信号处理&#xff01;今年考研的同学赶不上这么简单的专业课啦&#xff01; 本套试卷难度分析&#xff1a;平均分为102和141分&a…

ECharts与DataV:数据可视化的得力助手

文章目录 引言一、ECharts简介优势&#xff1a;劣势&#xff1a; 二、DataV简介优势&#xff1a;劣势&#xff1a; 三、ECharts与DataV的联系四、区别与选择五、如何选择根据需求选择技术栈考虑预算和商业考虑 结论我是将军&#xff0c;我一直都在&#xff0c;。&#xff01; 引…

3.10-容器的操作

这一节讲解一下对于container我们可以进行哪些操作&#xff1f; 可以使用以下命令来停止正在运行的Docker容器&#xff1a; docker container stop <CONTAINER ID> 关于运行中的容器&#xff0c;我们可以进行的操作&#xff1a; 第一个是docker exec命令&#xff0c;这个…

实时语音克隆:5 秒内生成任意文本的语音 | 开源日报 No.84

CorentinJ/Real-Time-Voice-Cloning Stars: 43.3k License: NOASSERTION 这个开源项目是一个实时语音克隆工具&#xff0c;可以在5秒内复制一种声音&#xff0c;并生成任意文本的语音。 该项目的主要功能包括&#xff1a; 从几秒钟的录音中创建声纹模型根据给定文本使用参考…

数字化转型没钱?没人?没IT?低代码平台轻松帮你搞定

随着数字技术的不断渗透&#xff0c;数字化已经不仅仅是一个趋势&#xff0c;而是深入人心的日常生活部分。在这样的时代背景下&#xff0c;企业面临的挑战也愈发严峻&#xff1a;如何不断创新&#xff0c;满足用户日益增长的业务需求&#xff1f; 传统的开发方式&#xff0c;随…

基于单片机设计的大气气压检测装置(STC89C52+BMP180实现)

一、前言 本项目设计一个大气气压检测装置&#xff0c;该装置以单片机为基础&#xff0c;采用STC89C52作为核心控制芯片&#xff0c;结合BMP180模块作为气压传感器。大气气压&#xff0c;也就是由气体重力在大气层中产生的压力&#xff0c;其变化与天气预报、气象观测以及高度…

11.7统一功能处理

一.登录拦截器 1.实现一个普通的类,实现HeadlerInterceptor接口,重写preHeadler方法. 2.将拦截器添加到配置中,并设定拦截规则. 二.访问前缀添加 方法1: 方法2:properties 三.统一异常处理 以上返回的是空指针异常,如果是别的异常就不会识别,建议加上最终异常 . 四.统一数据格…