JDK 动态代理从入门到掌握

news2024/9/24 15:24:36

快速入门

本文介绍 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 {

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

    @Override
    public 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{
    @SneakyThrows
    public 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 
    // 父类是Proxy
    extends 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.h
        super.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.coloradmin.cn/o/1272232.html

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

相关文章

孩子都能学会的FPGA:第十七课——用FPGA实现定点数的乘法

&#xff08;原创声明&#xff1a;该文是作者的原创&#xff0c;面向对象是FPGA入门者&#xff0c;后续会有进阶的高级教程。宗旨是让每个想做FPGA的人轻松入门&#xff0c;作者不光让大家知其然&#xff0c;还要让大家知其所以然&#xff01;每个工程作者都搭建了全自动化的仿…

SpringBoot-监听Nacos动态修改日志级别

目录 一、pom文件 二、项目配置文件 三、日志配置文件 四、日志监听类 五、日志动态修改服务类 线上系统的日志级别一般都是 INFO 级别&#xff0c;有时候需要查看 WARN 级别的日志&#xff0c;所以需要动态修改日志级别。微服务项目中使用 Nacos 作为注册中心&#xff0c…

什么是网络攻击?阿里云服务器可以避免被攻击吗?

网络攻击是指:损害网络系统安全属性的任何类型的进攻动作。进攻行为导致网络系统的机密性、完整性、可控性、真实性、抗抵赖性等受到不同程度的破坏。 网络攻击有很多种&#xff0c;网络上常见的攻击有DDOS攻击、CC攻击、SYN攻击、ARP攻击以及木马、病毒等等&#xff0c;所以再…

CTO对生活和工作一点感悟

陌生人&#xff0c;你好啊。 感谢CSDN平台让我们有了隔空认识&#xff0c;交流的机会。 我是谁&#xff1f; 我呢&#xff0c;毕业快11年&#xff0c;在网易做了几年云计算&#xff0c;后来追风赶上了大数据的浪潮&#xff0c;再到后来混迹在AI、智能推荐等领域。 因为有一颗…

SS8847T 双通道 H 桥驱动芯片 替代DRV8847

SS8847E是一款双桥电机驱动器&#xff0c;具有两个H桥驱动器&#xff0c;可以驱动两个直流有刷电机&#xff0c;一个双极步进电机&#xff0c;螺线管或其他感性负载。该器件的工作电压范围为 2.7V 至 15V&#xff0c;每通道可提供高达 1.0A 的负载电流。每个H桥的输出驱动器模块…

2023年安全员-A证证模拟考试题库及安全员-A证理论考试试题

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 2023年安全员-A证证模拟考试题库及安全员-A证理论考试试题是由安全生产模拟考试一点通提供&#xff0c;安全员-A证证模拟考试题库是根据安全员-A证最新版教材&#xff0c;安全员-A证大纲整理而成&#xff08;含2023年…

navigator.clipboard is undefined in JavaScript issue [Fixed]

navigator.clipboard 在不安全的网站是无法访问的。 在本地开发使用localhost或127.0.0.1没有这个问题。因为它不是不安全网站。 在现实开发中&#xff0c;可能遇到测试环境为不安全网站。 遇到这个问题&#xff0c;就需要将不安全网站标记为非不安全网站即可。 外网提供了3…

python动态加载内容抓取问题的解决实例

问题背景 在网页抓取过程中&#xff0c;动态加载的内容通常无法通过传统的爬虫工具直接获取&#xff0c;这给爬虫程序的编写带来了一定的技术挑战。腾讯新闻&#xff08;https://news.qq.com/&#xff09;作为一个典型的动态网页&#xff0c;展现了这一挑战。 问题分析 动态…

【开源视频联动物联网平台】视频接入网关的用法

视频接入网关是一种功能强大的视频网关设备&#xff0c;能够解决各种视频接入、视频输出、视频转码和视频融合等问题。它可以在应急指挥、智慧融合等项目中发挥重要作用&#xff0c;与各种系统进行对接&#xff0c;解决视频能力跨系统集成的难题。 很多视频接入网关在接入协议…

Go 语言输出文本函数详解

Go语言拥有三个用于输出文本的函数&#xff1a; Print()Println()Printf() Print() 函数以其默认格式打印其参数。 示例 打印 i 和 j 的值&#xff1a; package mainimport "fmt"func main() {var i, j string "Hello", "World"fmt.Print(…

【力扣:526】优美的排列

状态压缩动态规划 原理如下&#xff1a; 遍历位图可以得到所有组合序列&#xff0c;将这些序列的每一位看作一个数&#xff0c;取序列中1总量的值作为每轮遍历的位&#xff0c;此时对每个这样的位都能和所有数进行匹配&#xff0c;因为一开始就取的是全排列&#xff0c;并且我们…

MySQL表的查询、更新、删除

查询 全列查询 指定列查询 查询字段并添加自定义表达式 自定义表达式重命名 查询指定列并去重 select distinct 列名 from 表名 where条件 查询列数据为null的 null与 (空串)是不同的&#xff01; 附&#xff1a;一般null不参与查询。 查询列数据不为null的 查询某列数据指定…

陈嘉庚慈善践行与卓顺发的大爱传承

陈嘉庚慈善践行&#xff0c;了解陈嘉庚后人与卓顺发的大爱传承。 2023年11月25日,卓顺发太平绅士以及陈家后人在分享他们对慈善领域见解的过程中,特别强调了慈善在促进社会和谐以及推动社会进步方面的关键作用。同时,他们深入探讨了如何在当今社会中继续传扬和实践家国情怀以及…

C++ CryptoPP使用AES加解密

Crypto (CryptoPP) 是一个用于密码学和加密的 C 库。它是一个开源项目&#xff0c;提供了大量的密码学算法和功能&#xff0c;包括对称加密、非对称加密、哈希函数、消息认证码 (MAC)、数字签名等。Crypto 的目标是提供高性能和可靠的密码学工具&#xff0c;以满足软件开发中对…

什么是木马

木马 1. 定义2. 木马的特征3. 木马攻击流程4. 常见木马类型5. 如何防御木马 1. 定义 木马一名来源于古希腊特洛伊战争中著名的“木马计”&#xff0c;指可以非法控制计算机&#xff0c;或在他人计算机中从事秘密活动的恶意软件。 木马通过伪装成正常软件被下载到用户主机&…

strstr 的使用和模拟实现

就位了吗&#xff1f;如果坐好了的话&#xff0c;那么我就要开始这一期的表演了哦&#xff01; strstr 的使用和模拟实现: char * strstr ( const char * str1, const char * str2); Returns a pointer to the first occurrence of str2 in str1, or a null pointer if str2 i…

030 - STM32学习笔记 - ADC(四) 独立模式多通道DMA采集

030 - STM32学习笔记 - ADC&#xff08;四&#xff09; 独立模式多通道DMA采集 中断模式和DMA模式进行单通道模拟量采集&#xff0c;这节继续学习独立模式多通道DMA采集&#xff0c;使用到的引脚有之前使用的PC3&#xff08;电位器&#xff09;&#xff0c;PA4&#xff08;光敏…

Peter算法小课堂—高精度乘法

给大家看个小视频13 高精度算法 乘法_哔哩哔哩_bilibili 乘法竖式 大家觉得Plan A好&#xff0c;还是Plan B好呢&#xff08;对于计算机来说&#xff09;&#xff1f;那显然是B啦 x*y问题 mul思路&#xff1a;mul()函数返回x数组乘y数组的积&#xff0c;保存在z数组。根据上…

基于SpringBoot的旅游网站的设计与实现

摘 要 随着科学技术的飞速发展&#xff0c;各行各业都在努力与现代先进技术接轨&#xff0c;通过科技手段提高自身的优势&#xff0c;旅游网站当然也不能排除在外&#xff0c;随着旅游网站的不断成熟&#xff0c;它彻底改变了过去传统的旅游网站方式&#xff0c;不仅使旅游管理…

three.js结合vue

作者&#xff1a;baekpcyyy&#x1f41f; 1.搭建环境 ps&#xff1a;这里要按照node.js在之前有关vue搭建中有介绍 新建文件夹并在vsc终端中打开 1.输入vite创建指令 npm init vitelatest然后我们cd进入刚才创建的目录下 npm install安装所需依赖 npm run dev启动该项目 …