从设计上理解JDK动态代理

news2024/11/17 21:34:14
作者简介:大家好,我是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.获取目标方法Method
        m3 = 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.coloradmin.cn/o/1247149.html

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

相关文章

【Django-DRF】md笔记第6篇:Django-DRF的视图、认证、分页和其他功能详解

本文从分析现在流行的前后端分离Web应用模式说起&#xff0c;然后介绍如何设计REST API&#xff0c;通过使用Django来实现一个REST API为例&#xff0c;明确后端开发REST API要做的最核心工作&#xff0c;然后介绍Django REST framework能帮助我们简化开发REST API的工作。 DR…

win10 tensorrt源码编译onnx

直接利用官方源码&#xff0c;如下图&#xff0c;trtexec源码在TensorRT安装目录下&#xff0c;双击trtexec.sln文件&#xff0c;使用vs2019打开源码工程。 如下图&#xff0c;以yolov8为例子&#xff0c;编译成功项目之后&#xff0c;设置命令行参数&#xff1a; --onnxd:/yo…

表单邮箱密码登录 原生+Jquery实现

文章目录 效果代码邮箱验证正则表达式HTMLCSS JS 效果 正确密码为&#xff1a;123456 点击登录按钮校验。 代码 表单校验 - CodeSandbox 邮箱验证正则表达式 /(?:[a-z0-9!#$%&*/?^_{|}~-](?:\.[a-z0-9!#$%&*/?^_{|}~-])*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1…

实现HTTP服务监听,快来试试springboot服务端接口公网远程调试

&#x1f308;个人主页&#xff1a;聆风吟 &#x1f525;系列专栏&#xff1a;网络奇遇记、Cpolar杂谈 &#x1f516;少年有梦不应止于心动&#xff0c;更要付诸行动。 文章目录 &#x1f4cb;前言一. 本地环境搭建1.1 环境参数1.2 搭建springboot服务项目 二. 内网穿透2.1 安装…

AND/选品机制算法/用户表设计

3大步骤总结 大步骤总结&#xff1a; 第一大步骤&#xff1a; 生成AND算法机制所需要的8个表 AND musics Works Pool Table(音乐作品池表) 需要创建表 所需归类 AND算法池 AND videos Works Pool Table(视频作品池表) 需要创建表 所需归类 AND算法池 AND image…

基于Halcon的空间域图像滤波

任务描述&#xff1a; 图为HALCON中附带的例图“particle”。图中为某种液体&#xff0c;里面悬浮了微小颗粒&#xff0c;请分析出液体中的颗粒。 案例分析&#xff1a; 图中存在两种类型的对象&#xff1a;大的明亮物体和亮度较低的小物体&#xff08;颗粒&#xff09;。图像…

Rust UI开发(一):使用iced构建UI时,如何在界面显示中文字符

注&#xff1a;此文适合于对rust有一些了解的朋友 iced是一个跨平台的GUI库&#xff0c;用于为rust语言程序构建UI界面。 iced的基本逻辑是&#xff1a; UI交互产生消息message&#xff0c;message传递给后台的update&#xff0c;在这个函数中编写逻辑&#xff0c;然后通过…

经典的回溯算法题leetcode全排列问题思路代码详解

目录 全排列问题 leetcode46题.全排列 leetcode47题.全排列II 对回溯算法感兴趣的朋友也可以多多支持一下我的其他文章。 回溯算法详解-CSDN博客 经典的回溯算法题leetcode组合问题整理及思路代码详解-CSDN博客 经典的回溯算法题leetcode子集问题思路代码详解-CSDN博客 …

ResizeObserver loop limit exceeded报错解决方案

前言&#xff1a; 控制台没有报错&#xff0c;但是开发Vue项目过程中一直报ResizeObserver loop limit exceeded 错&#xff0c;找到以下解决方式。在main.js文件中重写 ResizeObserver 方法。 main.js文件 &#xff08;完整版&#xff09; import { createApp } from "v…

基于element自动表格

需求是根据JSON文件生成表格&#xff0c;包含配置和自动props属性以及过滤器&#xff1b; 数据示例&#xff1a; 表格设置&#xff1a; /*** 表格表头信息* chineseToPinYin: 这是封装的根据中文汉字转换为拼音的方法* prop: 表头字段名* filter: 数据过滤器* label: 表头显示…

从零学算法400

400.给你一个整数 n &#xff0c;请你在无限的整数序列 [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, …] 中找出并返回第 n 位上的数字。 示例 1&#xff1a; 输入&#xff1a;n 3 输出&#xff1a;3 示例 2&#xff1a; 输入&#xff1a;n 11 输出&#xff1a;0 解释&#xff1a;第…

Windows Python3安装salt模块失败处理

复现CVE-2020-11651时候运行CVE-2020-11651的poc时候需要salt模块 在下载时出现了错误 尝试在网上寻找解决方法&#xff1a; 1.更新 setuptools 和 wheel pip install --upgrade setuptools wheel 2. 安装Microsoft Visual C 14.0 因为salt模块包包使用了 C/C 扩展&#x…

Flutter开发警告Constructors in ‘@immutable‘ classes should be declared as ‘const‘

文章目录 警告信息报错代码警告原因修改后的代码 警告信息 Flutter开发遇到如下警告 Constructors in ‘immutable’ classes should be declared as ‘const’. 报错代码 class TaskWidget extends StatefulWidget {final String title;final bool isChecked;final int ord…

AD9528学习笔记

前言 AD9528是ADI的一款时钟芯片&#xff0c;由2-stage PLL组成&#xff0c;并且集成JESD204B/JESD204C SYSREF信号发生器&#xff0c;SYSREF发生器输出单次、N次或连续信号&#xff0c;并与PLL1和PLL2输出同步&#xff0c;从而可以实现多器件之间的同步。 AD9528总共有14路输…

电源模块重轻载变化测试的测试目的、测试标准、测试方法介绍

电源自动测试系统针对电源模块的各项测试项目提供最合适的测试方案&#xff0c;解决测试需求&#xff0c;提高测试效能。重轻载变化测试是电源模块测试的项目之一&#xff0c;是为了检测负载变化时输出电压的变化情况。为此&#xff0c;纳米软件将介绍电源重轻载变化测试的方法…

盘点43个Python登录第三方源码Python爱好者不容错过

盘点43个Python登录第三方源码Python爱好者不容错过 学习知识费力气&#xff0c;收集整理更不易。 知识付费甚欢喜&#xff0c;为咱码农谋福利。 项目名称 bnuz中国电信校园网模拟登录&#xff0c;python selenium BNUZ教务系统认证爬虫Python语言实现&#xff0c;你可以用…

为什么在Pycharm中使用Pandas画图,却不显示?

问题描述&#xff1a; 在 Pycharm 中使用 Pandas 的 plot() 方法画图&#xff0c;却不显示图像&#xff0c;源代码如下&#xff1a; import pandas as pd import numpy as np# 从文件中读取数据 starbucks pd.read_csv(./file_csv/directory.csv)# 按照国家分组&#xff0c;…

【T3】畅捷通T3登录时操作日期的日历中日期与星期对应关系错误。

【问题描述】 畅捷通T3软件登录的时候&#xff0c; 在选择操作日期位置&#xff0c;点击出日历后发现&#xff1a; 几月几日和星期几是对不上的。但是查看电脑系统右下角日期&#xff0c;是正确的。 例如&#xff1a;2023年11月24日应该是星期五&#xff0c;这里反而是星期三。…

如何去掉图片水印不伤原图?无痕去水印教程分享!

如何去掉图片水印不伤原图&#xff1f;在电商广告设计和营销领域&#xff0c;水印已经成为一种常见的版权保护手段。不过&#xff0c;水印也给淘宝商家带来了一些困扰。那么如何去掉图片水印还能不伤原图呢&#xff0c;接下来&#xff0c;将分享简单好用的无痕去水印教程&#…

高级IO—select

高级IO—select 文章目录 高级IO—selectIO的概念 五种IO模型阻塞IO非阻塞IO信号驱动IOIO多路转接异步IO I/O多路转接之select IO的概念 通常指数据在内部存储器和外部存储器或其他周边设备之间的输入和输出。输入是系统接收的信号或数据&#xff0c;输出则是从其发送的信号或…