java的JDK动态代理

JDK动态代理是指:代理类实例在程序运行时,由JVM根据反射机制动态的生成。也就是说代理类不是用户自己定义的,而是由JVM生成的。

由于其原理是通过Java反射机制实现的,所以在学习前,要对反射机制有一定的了解。传送门:Java反射机制:跟着代码学反射

下面是本篇讲述内容:

1. JDK动态代理的核心类

JDK动态代理有两大核心类,它们都在Java的反射包下(java.lang.reflect),分别为InvocationHandler接口和Proxy类。

1.1 InvocationHandler接口

代理实例的调用处理器需要实现 InvocationHandler接口,并且每个代理实例都有一个关联的调用处理器。当一个方法在代理实例上被调用时,这个方法调用将被编码并分派到其调用处理器的 invoke方法上。

也就是说,我们创建的每一个代理实例都要有一个关联的InvocationHandler,并且在调用代理实例的方法时,会被转到InvocationHandlerinvoke方法上。

public Object invoke(Object proxy, Method method, Object[] args)throws Throwable;

invoke方法的作用是:处理代理实例上的方法调用并返回结果。

其有三个参数,分别为:

  • proxy:是调用该方法的代理实例。
  • method:是在代理实例上调用的接口方法对应的Method实例。
  • args:一个Object数组,是在代理实例上的方法调用中传递的参数值。如果接口方法为无参,则该值为null。

其返回值为:调用代理实例上的方法的返回值。

1.2 Proxy类

Proxy类提供了创建动态代理类及其实例的静态方法,该类也是动态代理类的超类。

代理类具有以下属性:

  • 代理类的名称以 “$Proxy” 开头,后面跟着一个数字序号。
  • 代理类继承了Proxy类。
  • 代理类实现了创建时指定的接口(JDK动态代理是面向接口的)。
  • 每个代理类都有一个公共构造函数,它接受一个参数,即接口InvocationHandler的实现,用于设置代理实例的调用处理器。

Proxy提供了两个静态方法,用于获取代理对象。

1.2.1 getProxyClass

用于获取代理类的Class对象,再通过调用构造函数创建代理实例。

public static Class<?> getProxyClass(ClassLoader loader,Class<?>... interfaces)throws IllegalArgumentException

该方法有两个参数:

  • loader:为类加载器。
  • intefaces:为接口的Class对象数组。

返回值为动态代理类的Class对象。

1.2.2 newProxyInstance

用于创建一个代理实例。

public static Object newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h)throws IllegalArgumentException

该方法有三个参数:

  • loader:为类加载器。
  • interfaces:为接口的Class对象数组。
  • h:指定的调用处理器。

返回值为指定接口的代理类的实例。

1.3 小结

Proxy类主要用来获取动态代理对象,InvocationHandler接口主要用于方法调用的约束与增强。

2. 获取代理实例的代码示例

上一章中已经介绍了获取代理实例的两个静态方法,现在通过代码示例来演示具体实现。

2.1 创建目标接口及其实现类

JDK动态代理是基于接口的,我们创建一个接口及其实现类。

Foo接口:

public interface Foo {String ping(String name);}

Foo接口的实现类RealFoo:

public class RealFoo implements Foo {@Overridepublic String ping(String name) {System.out.println("ping");return "pong";}}

2.2 创建一个InvocationHandler

创建一个InvocationHandler接口的实现类MyInvocationHandler。该类的构造方法参数为要代理的目标对象。

invoke方法中的三个参数上面已经介绍过,通过调用methodinvoke方法来完成方法的调用。

这里一时看不懂没关系,后面源码解析章节会进行剖析。

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("proxy - " + proxy.getClass());System.out.println("method - " + method);System.out.println("args - " + Arrays.toString(args));return method.invoke(target, args);}
}

2.3 方式一:通过getProxyClass方法获取代理实例

具体实现步骤如下:

  1. 根据类加载器和接口数组获取代理类的Class对象
  2. 过Class对象的构造器创建一个实例(代理类的实例)
  3. 将代理实例强转成目标接口Foo(因为代理类实现了目标接口,所以可以强转)。
  4. 最后使用代理进行方法调用。
@Test
public void test1() throws Exception {Foo foo = new RealFoo();// 根据类加载器和接口数组获取代理类的Class对象Class<?> proxyClass = Proxy.getProxyClass(Foo.class.getClassLoader(), Foo.class);// 通过Class对象的构造器创建一个实例(代理类的实例)Foo fooProxy = (Foo) proxyClass.getConstructor(InvocationHandler.class).newInstance(new MyInvocationHandler(foo));// 调用 ping 方法,并输出返回值String value = fooProxy.ping("杨过");System.out.println(value);}

输出结果:

proxy - class com.sun.proxy.$Proxy4
method - public abstract java.lang.String io.github.gozhuyinglong.proxy.Foo.ping(java.lang.String)
args - [杨过]
ping
pong

通过输出结果可以看出:

  • 代理类的名称是以$Proxy开头的。
  • 方法实例为代理类调用的方法。
  • 参数为代理类调用方法时传的参数。

2.4 方式二:通过newProxyInstance方法获取代理实例

通过这种方法是最简单的,也是推荐使用的,通过该方法可以直接获取代理对象。

注:其实该方法后台实现实际与上面使用getProxyClass方法的过程一样。

@Test
public void test2() {Foo foo = new RealFoo();// 通过类加载器、接口数组和调用处理器,创建代理类的实例Foo fooProxy = (Foo) Proxy.newProxyInstance(Foo.class.getClassLoader(),new Class[]{Foo.class},new MyInvocationHandler(foo));String value = fooProxy.ping("小龙女");System.out.println(value);
}

2.5 通过Lambda表达式简化实现

其实InvocationHander接口也不用创建一个实现类,可以使用Lambad表达式进行简化的实现,如下代码:

@Test
public void test3() {Foo foo = new RealFoo();Foo fooProxy = (Foo) Proxy.newProxyInstance(Foo.class.getClassLoader(),new Class[]{Foo.class},(proxy, method, args) -> method.invoke(foo, args));String value = fooProxy.ping("雕兄");System.out.println(value);
}

3. 源码解析

3.1 代理类$Proxy是什么样子

JVM为我们自动生成的代理类到底是什么样子的呢?下面我们先来生成一下,再来看里面的构造。

3.1.1 生成$Proxy的.class文件

JVM默认不创建该.class文件,需要增加一个启动参数:
-Dsun.misc.ProxyGenerator.saveGeneratedFiles=true

在IDEA中点击【Edit Configurations...】,打开 Run/Debug Configurations 配置框。


将上面启动参数加到【VM options】中,点击【OK】即可。

再次运行代码,会在项目中的【com.sun.proxy】目录中找到这个.class文件,我这里是“$Proxy4.class”

3.1.2 为什么加上这段启动参数就能生成$Proxy的字节码文件

Proxy类中有个ProxyClassFactory静态内部类,该类主要作用就是生成静态代理的。

其中有一段代码ProxyGenerator.generateProxyClass用来生成代理类的.class文件。

其中变量saveGeneratedFiles便是引用了此启动参数的值。将该启动参数配置为true会生成.class文件。

3.1.3 这个代理类$Proxy到底是什么样子呢

神秘的面纱即将揭露,前面很多未解之迷在这里可以找到答案!

打开这个$Proxy文件,我这里生成的是$Proxy4,下面是内容:

// 该类为final类,其继承了Proxy类,并实现了被代理接口Foo
public final class $Proxy4 extends Proxy implements Foo {// 这4个Method实例,代表了本类实现的4个方法private static Method m1;private static Method m2;private static Method m3;private static Method m0;// 静态代码块根据反射获取这4个方法的Method实例static {try {m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));m2 = Class.forName("java.lang.Object").getMethod("toString");m3 = Class.forName("io.github.gozhuyinglong.proxy.Foo").getMethod("ping");m0 = Class.forName("java.lang.Object").getMethod("hashCode");} catch (NoSuchMethodException var2) {throw new NoSuchMethodError(var2.getMessage());} catch (ClassNotFoundException var3) {throw new NoClassDefFoundError(var3.getMessage());}}// 一个公开的构造函数,参数为指定的 InvocationHandler public $Proxy4(InvocationHandler var1) throws  {super(var1);}public final boolean equals(Object var1) throws  {try {return (Boolean)super.h.invoke(this, m1, new Object[]{var1});} catch (RuntimeException | Error var3) {throw var3;} catch (Throwable var4) {throw new UndeclaredThrowableException(var4);}}public final String toString() throws  {try {return (String)super.h.invoke(this, m2, (Object[])null);} catch (RuntimeException | Error var2) {throw var2;} catch (Throwable var3) {throw new UndeclaredThrowableException(var3);}}// Foo接口的实现方法,最终调用了 InvocationHandler 中的 invoke 方法public final String ping(String var1) throws  {try {// InvocationHandler 接口在Proxy中定义了return (String)super.h.invoke(this, m3, new Object[]{var1});} catch (RuntimeException | Error var3) {throw var3;} catch (Throwable var4) {throw new UndeclaredThrowableException(var4);}}public final int hashCode() throws  {try {return (Integer)super.h.invoke(this, m0, (Object[])null);} catch (RuntimeException | Error var2) {throw var2;} catch (Throwable var3) {throw new UndeclaredThrowableException(var3);}}
}

通过该文件可以看出:

  • 代理类继承了Proxy类,其主要目的是为了传递InvocationHandler
  • 代理类实现了被代理的接口Foo,这也是为什么代理类可以直接强转成接口的原因。
  • 有一个公开的构造函数,参数为指定的InvocationHandler,并将参数传递到父类Proxy中。
  • 每一个实现的方法,都会调用InvocationHandler中的invoke方法,并将代理类本身、Method实例、入参三个参数进行传递。这也是为什么调用代理类中的方法时,总会分派到InvocationHandler中的invoke方法的原因。

3.2 代理类是如何创建的

我们从Proxy类为我们提供的两个静态方法开始getProxyClassnewProxyInstance。上面已经介绍了,这两个方法是用来创建代理类及其实例的,下面来看源码。

3.2.1 getProxyClass 和 newProxyInstance方法

getProxyClass方法

newProxyInstance方法

通过上面源码可以看出,这两个方法最终都会调用getProxyClass0方法来生成代理类的Class对象。只不过newProxyInstance方法为我们创建好了代理实例,而getProxyClass方法需要我们自己创建代理实例。

3.2.2 getProxyClass0 方法

下面来看这个统一的入口:getProxyClass0

getProxyClass0方法

从源码和注解可以看出:

  • 代理接口的最多不能超过65535个
  • 会先从缓存中获取代理类,则没有再通过ProxyClassFactory创建代理类。(代理类会被缓存一段时间。)
3.2.3 WeakCache类

这里简单介绍一下WeakCache<K, P, V> 类,该类主要是为代理类进行缓存的。获取代理类时,会首先从缓存中获取,若没有会调用ProxyClassFactory类进行创建,创建好后会进行缓存。

WeakCache类的get方法

3.2.4 ProxyClassFactory类

ProxyClassFactoryProxy类的一个静态内部类,该类用于生成代理类。下图是源码的部分内容:

ProxyClassFactory类

  • 代理类的名称就是在这里定义的,其前缀是$Proxy,后缀是一个数字。
  • 调用ProxyGenerator.generateProxyClass来生成指定的代理类。
  • defineClass0方法是一个native方法,负责字节码加载的实现,并返回对应的Class对象。

3.3 原理图

为了便于记录,将代理类的生成过程整理成了一张图。

InvocationHandler通过 cl.getConstructor(constructorParams);获取构造函数从而传入Proxy的对象中,代理中的方法都是super.h.invoke(this, m3, new Object[]{var1});来执行,相当于策略模式

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

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

相关文章

【简单介绍下PostCSS】

&#x1f3a5;博主&#xff1a;程序员不想YY啊 &#x1f4ab;CSDN优质创作者&#xff0c;CSDN实力新星&#xff0c;CSDN博客专家 &#x1f917;点赞&#x1f388;收藏⭐再看&#x1f4ab;养成习惯 ✨希望本文对您有所裨益&#xff0c;如有不足之处&#xff0c;欢迎在评论区提出…

python爬虫 - 爬取图片

文章目录 1、爬取图片示例1&#xff1a;使用 .urlretrieve() 函数2、爬取图片示例2 - 使用 open/write 函数3、爬取图片示例33.1 使用 open/write 下载3.2 使用 urlretrieve下载 爬虫的本质&#xff1a;模拟对应的App&#xff0c;浏览器访问对应的地址获取到数据 1、爬取图片示…

57、通过EEG数据的SHAPE变化,揭开EEG-TCNet的黑匣子[看好了小子,我只教这一次]

之前在第18篇博客中对于EEG-TCNet这个处理EEG信号的sota模型进行了介绍&#xff0c;也给出了模型&#xff0c;目前也是全网对于EEG-TCNet浏览度最高的文章了&#xff0c;我觉得讲的已经很细致了&#xff0c;没想到还是有不少同学疑问&#xff0c;这也是全网缺少该模型pytorch代…

3D Gaussian Splatting技术原理

3D Gaussian Splatting 是一种用于体积渲染的技术,特别适用于科学和医学可视化。这种技术使得用户能够以一种直观的方式查看和分析三维数据集,如医学成像数据(MRI、CT扫描)或科学模拟数据。 技术原理 3D Gaussian Splatting基本上是一种将3D空间中的点数据转换成一个连续…

Redis中的Sentinel(五)

Sentinel 检测主观下线状态 在默认情况下&#xff0c;Sentinel会以每秒一次的频率向所有与它创建了命令连接的实例(包括主服务器、从服务器、其他Sentinel在内) 发送PING命令&#xff0c;并通过实例返回的PING命令回复来判断实例是否在线。如图所示&#xff0c;带箭头的连线显…

Word学习笔记之奇偶页的页眉与页码设置

1. 常用格式 在毕业论文中&#xff0c;往往有一下要求&#xff1a; 奇数页右下角显示、偶数页左下角显示奇数页眉为每章标题、偶数页眉为论文标题 2. 问题解决 2.1 前期准备 首先&#xff0c;不论时要求 1、还是要求 2&#xff0c;这里我们都要做一下设置&#xff1a; 鼠…

如何封装Vue组件并上传到npm

前言 环境准备 1.注册npm账号&#xff1a;npm | Home (npmjs.com) 2.保证当前环境安装了vue、webpack、node&#xff0c;以下工作将在该环境下进行&#xff08;没有的小伙伴自行百度安装哈~&#xff09; 3.一下用到的环境版本 webpack&#xff1a;v5.1.4node&#xff1a;v…

SAM5716B 法国追梦DREAM 音频DSP芯片

法国追梦/DERAM SAM5504/5704/5716/5808音频DSP芯片,开发板&#xff0c;方案 可用于电子鼓、电子琴、电吉他、效果器、均衡器、啸叫抑制器等电声产品领域 全系列芯片&#xff1a; SAM2634 SAM2695 SAM5504B SAM5704B SAM5708B SAM5808B SAM5716B SAM5916B... 原厂开发…

根据状态转移图实现时序电路

描述 某同步时序电路的状态转换图如下&#xff0c;→上表示“C/Y”&#xff0c;圆圈内为现态&#xff0c;→指向次态。 请使用D触发器和必要的逻辑门实现此同步时序电路&#xff0c;用Verilog语言描述。 如图所示&#xff1a; 电路的接口如下图所示&#xff0c;C是单bit数据…

密码学 | 承诺:常见的承诺方案

&#x1f951;原文&#xff1a;密码学原语如何应用&#xff1f;解析密码学承诺的妙用 - 知乎 1 简介 密码学承诺 涉及 承诺方、验证方 两个参与方&#xff0c;以及以下两个阶段&#xff1a; 承诺阶段&#xff1a;承诺方选择一个敏感数据 v v v&#xff0c;为它计算出相应…

【Jupyter Notebook】快捷键

在命令模式下&#xff0c;单元格边框是灰色&#xff08;缺省&#xff09;的。这些快捷键主要用于操作单元格。 Enter&#xff1a;进入编辑模式Shift Enter&#xff1a;运行当前单元格并选中下一个单元格Ctrl Enter&#xff1a;运行当前单元格Alt Enter&#xff1a;运行当前单…

Mac多媒体播放器 Movist Pro v2.11.4中文激活版下载

Movist Pro for Mac是一款专业的媒体播放器&#xff0c;特别为Mac用户设计。它不仅界面简洁美观&#xff0c;而且功能强大&#xff0c;能满足用户各种播放需求。 Movist Pro v2.11.4中文激活版下载 首先&#xff0c;Movist Pro for Mac支持多种媒体文件的播放&#xff0c;包括视…

关于Qt主窗口的菜单部件

前言 在介绍主窗口的两大部件之前&#xff0c;我们要先知道关于主窗口的一些知识。 主窗口 一个主窗口可以没有菜单条、工具条、状态条&#xff0c;但必须设置中心部件。在 Q 生成的 C头文件 ui_mainwindow.h 代码中,我们可以看到以下代码: centralWidget new Qwidget(MainWi…

CSS基础常用属性之颜色(如果想知道CSS的颜色知识点,那么只看这一篇就足够了!)

前言&#xff1a;在我们学习CSS的时候&#xff0c;主要学习选择器和常用的属性&#xff0c;而这篇文章讲解的就是最基础的属性——颜色。 ✨✨✨这里是秋刀鱼不做梦的BLOG ✨✨✨想要了解更多内容可以访问我的主页秋刀鱼不做梦-CSDN博客 目录 1.颜色属性 【1】使用颜色关键词表…

fatal error C1001: An internal error has occurred in the compiler

VS2008驱动项目A&#xff0c;集成一个Wzarid生成的驱动LIB项目B&#xff0c;在编译64位驱动时,出现以下错误&#xff1a; 1>------ Build started: Project: xxxx, Configuration: Release x64 ------ 1>Linking... 1>fatal error C1001: An internal error has occu…

怎么在 Spring 服务响应时控制响应时间?

在Spring应用程序中控制服务响应时间是确保系统性能和用户体验的关键方面之一。在处理请求时&#xff0c;响应时间是指从客户端发送请求到服务端返回响应所花费的时间。 在某些情况下&#xff0c;需要对响应时间进行控制&#xff0c;以确保系统能够及时响应用户请求&#xff0…

springboot中mongodb连接池配置-源码分析

yml下spring.data.mongodb 以前mysql等在spring.xxx下配置&#xff0c;现在springboot新版本&#xff08;小编3.2.3&#xff09;在spring.data.xxx下了&#xff0c;如下所示&#xff0c;mongodb的配置在spring.data.mongodb下&#xff1a; 连接池相关参数配置-源码分析 拼接在…

改进下记录学习的小网站

Strong改进 结束&#xff1a;2024-4-14 打算投入&#xff1a;10h 实际消耗&#xff1a;12h 3m 学习总是不在状态。 我的时间花得很零散&#xff0c;也有点茫然。所以想尝试一下集中式地、一块一块地花&#xff0c;比如投入30个小时&#xff0c;去干一件事&#xff0c;这样就可…

现在期权开户佣金最低的证券公司是哪家?1.7元/张是真的吗?

期权开户的要求主要包括以下几个方面&#xff1a; 1. **资金要求**&#xff1a;在申请开户时&#xff0c;保证金账户可用资金余额需要不低于人民币50万元。这是为了确保投资者有足够的资金来应对期权交易的风险。 2. **交易经验**&#xff1a;投资者需要具备股指期货交易经验&…

Vue的虚拟DOM是什么

核心思想 虚拟DOM/Virtual DOM&#xff0c;是数据驱动视图的一种解决方案。核心思想&#xff1a;使用 js对象的形式来表现html的dom结构。 背景 由于现代网络和浏览器的发展&#xff0c;网页的内容也变得很复杂&#xff0c;ajax 诞生让用户可以在不刷新页面的条件下获取到数…