java的JDK动态代理

news2024/11/25 2:47:18

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 {

    @Override
    public 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;
    }


    @Override
    public 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.coloradmin.cn/o/1604858.html

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

相关文章

网络安全的守护者:防火墙的五个主要功能解析

防火墙是一种网络安全设备&#xff0c;用于保护计算机网络免受未经授权的访问、攻击和恶意软件的侵害。它通过监控、过滤和控制网络流量&#xff0c;实施安全策略&#xff0c;防止不安全的数据包进入或离开受保护的网络。 防火墙的五个主要功能&#xff1a; 1. 访问控制&#…

CleanMyMac破解版官方试用补丁器下载2024最新

CleanMyMac是一款由MacPaw公司研发的Mac清理工具。以下是对CleanMyMac的详细介绍&#xff1a; 一、主要功能 一键智能清理&#xff1a;CleanMyMac能智能扫描Mac磁盘空间中的垃圾文件&#xff0c;这包括但不限于识别重复文件、无用语言安装包、iTunes垃圾、重复照片、邮件附件…

15V转5V3A降压同步WT6019

15V转5V3A降压同步WT6019 WT6019则是一种高效的同步降压转换器。它可以将15V的输入电压稳定转换为5V的输出电压&#xff0c;并保证最大3A的电流输出。这种转换器的核心在于其内部的功率MOSFET&#xff0c;它能够以较低的导通电阻和快速的开关速度&#xff0c;实现高效率的能量…

ETLCloud中多并行分支运行的设计技巧

在大数据处理领域&#xff0c;ETL&#xff08;Extract, Transform, Load&#xff09;流程是至关重要的一环&#xff0c;它涉及数据的提取、转换和加载&#xff0c;以确保数据的质量和可用性。而在ETL流程中&#xff0c;多并行分支的运行设计是一项关键技巧&#xff0c;可以有效…

Facebook隐私保护:用户数据安全的关键挑战

在数字化时代&#xff0c;数据已成为最宝贵的资源之一。社交媒体平台如Facebook为用户提供了便捷的交流和信息分享工具&#xff0c;但同时也面临着如何保护用户数据安全和隐私的挑战。本文将深入探讨Facebook在数据安全方面面临的关键挑战&#xff0c;以及其如何应对这些挑战&a…

电商数据接口开发|淘宝商品接口|天猫商品接口|京东商品接口|拼多多商品接口|API接口申请指南

电商数据接口开发涉及到多个电商平台&#xff0c;包括淘宝、天猫、京东和拼多多等。这些平台都提供了丰富的API接口&#xff0c;以便开发者能够获取商品信息、订单数据等&#xff0c;从而构建出各种电商应用和服务。 1.请求方式&#xff1a;HTTP POST GET &#xff08;复制薇&…

一套java+ spring boot与 vue+ mysql技术开发的UWB高精度工厂人员定位全套系统源码有应用案例

一套java spring boot vue mysql技术开发的UWB高精度工厂人员定位全套系统源码有应用案例 UWB (ULTRA WIDE BAND, UWB) 技术是一种无线载波通讯技术&#xff0c;它不采用正弦载波&#xff0c;而是利用纳秒级的非正弦波窄脉冲传输数据&#xff0c;因此其所占的频谱范围很宽。一套…

Git学习与码云实战

Git学习与码云实战 Git安装 概述&#xff1a; Git 是一个开源的分布式版本控制系统&#xff0c;可以有效、高速的处理从很小到非常大的项目版本管理&#xff0c;是目前使用范围最广的版本管理工具。 下载安装&#xff1a; 下载地址&#xff1a;https://git-scm.com/ 下载后傻瓜…

diffusion model 简单demo

参考自&#xff1a; Probabilistic Diffusion Model概率扩散模型理论与完整PyTorch代码详细解读 diffusion 简单demo 扩散模型之DDPM 核心公式和逻辑 q_x 计算公式&#xff0c;后面会用到&#xff1a; 推理&#xff1a; 代码 import matplotlib.pyplot as plt import nump…

08-GPtimer

通用定时器 &#xff08;GPTimer&#xff09; 通用定时器简介 通用定时器可用于准确设定时间间隔、在一定间隔后触发&#xff08;周期或非周期的&#xff09;中断或充当硬件时钟。如下图所示&#xff0c;ESP32-S3 包含两个定时器组&#xff0c;即定时器组 0 和定时器组 1。每…

力扣练习题(2024/4/14)

1接雨水 给定 n 个非负整数表示每个宽度为 1 的柱子的高度图&#xff0c;计算按此排列的柱子&#xff0c;下雨之后能接多少雨水。 示例 1&#xff1a; 输入&#xff1a;height [0,1,0,2,1,0,1,3,2,1,2,1] 输出&#xff1a;6 解释&#xff1a;上面是由数组 [0,1,0,2,1,0,1,3,2…

vue3 -- 项目使用自定义字体font-family

在Vue 3项目中使用自定义字体(font-family)的方法与在普通的HTML/CSS项目中类似。可以按照以下步骤进行操作: 引入字体文件: 首先,确保你的字体文件(通常是.woff、.woff2、.ttf等格式)位于项目中的某个目录下,比如src/assets/font/。 在全局样式中定义字体: 在你的全局…

mysql常见语法操作笔记

1. 数据库的基本操作 1.1. MYSQL登录与退出 D:\phpstudy_pro\Extensions\MySQL5.7.26\bin 输入 mysql -uroot -proot -h127.0.0.1 退出的三种方法 mysql > exit; mysql > quit; mysql > \q; 1.2. MYSQL数据库的一些解释 注意&#xff1a;数据库就相当于文件夹 …

IDEA 控制台中文乱码 4 种解决方案

前言 IntelliJ IDEA 如果不进行相关设置&#xff0c;可能会导致控制台中文乱码、配置文件中文乱码等问题&#xff0c;非常影响编码过程中进行问题追踪。本文总结了 IDEA 中常见的中文乱码解决方法&#xff0c;希望能够帮助到大家。 IDEA 中文乱码 解决方案 一、设置字体为支…

挣钱新玩法,一文带你掌握流量卡推广秘诀

手机流量卡推广项目是什么&#xff1f;听名字我相信大家就已经猜出来了&#xff0c;就是三大运营商为了开发新用户&#xff0c;发起的有奖推广活动&#xff0c;也是为了长期黏贴用户。在这个活动中&#xff0c;用户通过我们的渠道&#xff0c;就能免费办理低套餐流量卡&#xf…

Obsidian 插件安装

方法一&#xff1a; Obsidian 最简单的插件安装当然是通过第三方插件库进行搜索&#xff0c;但是由于魔法上网的问题&#xff0c;经常连不上github&#xff0c;或者下载不了&#xff0c;导致插件无法安装。 方法二&#xff1a; obsidian 社区插件汇总&#xff1a;Airtable -…

【第三十一篇】Autorize插件安装使用教程(结合Burp实现越权实战案例)

Burp Suite是一款功能强大的渗透测试工具,被广泛应用于Web应用程序的安全测试和漏洞挖掘中。 本专栏将结合实操及具体案例,带领读者入门、掌握这款漏洞挖掘利器 读者可订阅专栏:【Burp由入门到精通 |CSDN秋说】 文章目录 前言安装教程使用教程垂直越权垂直越权实战注意前言 …

群晖 NAS rsync 远程文件同步

客户机是外网的 Windows 11&#xff0c;服务器是群晖。 客户机上安装 WSL Alpine Linux 来运行 rsync 进行文件下载。Alpine 相对比 Ubuntu、Debian&#xff0c;要小巧轻量&#xff0c;占用存储空间少&#xff0c;启动速度也很快。 一、安装 WSL Alpine Linux 在 Windows 中&…

scala---基础核心知识(变量定义,数据类型,流程控制,方法定义,函数定义)

一、什么是scala Scala 是一种多范式的编程语言&#xff0c;其设计初衷是要集成面向对象编程和函数式编程的各种特性。Scala运行于Java平台&#xff08;Java虚拟机&#xff09;&#xff0c;并兼容现有的Java程序。 二、为什么要学习scala 1、优雅 2、速度快 3、能融合到hado…

ADOP-400G光模块问题发布会

前沿光学&#xff08;ADOP&#xff09;400G光模块为客户提供各种超高密度的400G以太网连接方案&#xff0c;广泛应用于数据中心、企业网和服务提供商。 &#x1f4e3;&#x1f4e3;以下一些问题是我们新一代400G光模块常能遇见问题&#xff0c;所以我们决定在这里开一场小小的…