JDK 动态代理从入门到掌握

快速入门

本文介绍 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 对象则是连接代理类对象和目标类对象的关键。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Linux:docker的网络通信(7)

1.端口映射 端口映射---端口映射机制将容器内的服务提供给外部网络访问 启动容器时&#xff0c;不指定对应的端口&#xff0c;在容器外无法通过网络访问容器内的服务 可随机或指定映射端口范围 -P ---------大写P&#xff0c;开启随机端口 -p 宿主机端口&#xff1a;容器端口…

【java扫盲贴】final修饰变量

引用类型&#xff1a;地址不可变 //Java中的引用类型分为类&#xff08;class&#xff09;、接口&#xff08;interface&#xff09;、数组&#xff08;array&#xff09;和枚举&#xff08;enum&#xff09;。//string是特殊的引用类型&#xff0c;他的底层是被final修饰的字…

Python下利用Selenium获取动态页面数据

利用python爬取网站数据非常便捷&#xff0c;效率非常高&#xff0c;但是常用的一般都是使用BeautifSoup、requests搭配组合抓取静态页面&#xff08;即网页上显示的数据都可以在html源码中找到&#xff0c;而不是网站通过js或者ajax异步加载的&#xff09;&#xff0c;这种类型…

【排序,直接插入排序 折半插入排序 希尔插入排序】

文章目录 排序排序方法的分类插入排序直接插入排序折半插入排序希尔插入排序 排序 将一组杂乱无章的数据按照一定规律排列起来。将无序序列排成一个有序序列。 排序方法的分类 储存介质&#xff1a; 内部排序&#xff1a;数据量不大&#xff0c;数据在内存&#xff0c;无需…

学习笔记-接口测试(postman、jmeter)

一、什么是接口测试 通常做的接口测试指的是系统对外的接口&#xff0c;比如你需要从别的系统来获取到或者同步资源与信息&#xff0c;他们会提供给你一个写好的接口方法供你调用&#xff0c;比如常用的app&#xff0c;用户同步这些在处理数据的时候需要通过接口进行调用。 w…

一文讲透Python函数中的局部变量和全局变量

变量的作用域就是变量能够发挥作用的区域&#xff0c;超出既定区域后就无法发挥作用。根据变量的作用域可以将变量分为局部变量和全局变量。 1.局部变量 局部变量是在函数内部定义并使用的变量&#xff0c;也就是说只有在函数内部&#xff0c;在函数运行时才会有效&#xff0…

Flask SocketIO 实现动态绘图

Flask-SocketIO 是基于 Flask 的一个扩展&#xff0c;用于简化在 Flask 应用中集成 WebSocket 功能。WebSocket 是一种在客户端和服务器之间实现实时双向通信的协议&#xff0c;常用于实现实时性要求较高的应用&#xff0c;如聊天应用、实时通知等&#xff0c;使得开发者可以更…

#zookeeper集群+kafka集群

kafka3.0之前是依赖于zookeeper的。 zookeeper是开源&#xff0c;分布式的架构。提供协调服务&#xff08;Apache项目&#xff09; 基于观察者模式涉及的分布式服务管理架构。 存储和管理数据。分布式节点上的服务接受观察者的注册。一旦分布式节点上的数据发生变化&#xf…

【Linux学习】文件描述符重定向缓冲区

目录 九.文件描述符 9.1 文件描述符概念 9.2 文件描述符的分配规则 9.3 重定向 9.3.1 常见的重定向操作 9.3.2 重定向的原理 9.4 缓冲区 9.4.1 缓冲区概念 9.4.2 缓冲区刷新策略 9.4.3 C语言的缓冲区在哪里? 九.文件描述符 9.1 文件描述符概念 在上一篇讲到基础IO时,我们说到…

Java项目学生管理系统二查询所有

学生管理 近年来&#xff0c;Java作为一门广泛应用于后端开发的编程语言&#xff0c;具备了广泛的应用领域和丰富的开发资源。在前几天的博客中&#xff0c;我们探讨了如何搭建前后端环境&#xff0c;为接下来的开发工作打下了坚实的基础。今天&#xff0c;我们将进一步扩展我…

10.0 输入输出 I/O

IO操作主要是指使用Java程序完成输入&#xff08;Input&#xff09;、输出&#xff08;Output&#xff09;操作。所谓输入是指将文件内容以数据流的形式读取到内存中&#xff0c;输出是指通过Java程序将内存中的数据写入到文件中&#xff0c;输入、输出操作在实际开发中应用较为…

Rust UI开发(5):iced中如何进行页面布局(pick_list的使用)?(串口调试助手)

注&#xff1a;此文适合于对rust有一些了解的朋友 iced是一个跨平台的GUI库&#xff0c;用于为rust语言程序构建UI界面。 这是一个系列博文&#xff0c;本文是第五篇&#xff0c;前四篇链接&#xff1a; 1、Rust UI开发&#xff08;一&#xff09;&#xff1a;使用iced构建UI时…

Springboot+vue的客户关系管理系统(有报告),Javaee项目,springboot vue前后端分离项目

演示视频&#xff1a; Springbootvue的客户关系管理系统&#xff08;有报告&#xff09;&#xff0c;Javaee项目&#xff0c;springboot vue前后端分离项目 项目介绍&#xff1a; 本文设计了一个基于Springbootvue的前后端分离的客户关系管理系统&#xff0c;采用M&#xff08…

C++学习之继承中修改成员权限细节

看看下面的代码 这是错误的 class A { public:int x 10; }; class B :public A {using A::x;int x 100; };看看函数 class A { public:void fun(){cout << "uuuu" << endl;} }; class B :public A { public:using A::fun;void fun(){cout << …

C++基础——文件操作

文章目录 1 概述2 文本文件2.1 写文件2.1.1 写文件流程2.1.2 文件打开方式 2.2 读文件 3 二进制文件3.1 写文件3.2 读文件 1 概述 程序最基本的操作之一就是文件操作&#xff0c;程序运行时的数据都是临时数据&#xff0c;当程序结束后就不复存在了。通常都是通过文件或其他持…

2000-2021年各省人口密度数据

2000-2021年各省人口密度数据 1、时间&#xff1a;2000-2021年 2、指标&#xff1a;地区、年份、年末常住总人口(万人&#xff09;、面积&#xff08;平方千米&#xff09;、人口密度&#xff08;人/平方千米&#xff09; 3、来源&#xff1a;各省年鉴、统计年鉴、各省统计局…

File类

File 概述 File: 路径 IO流: 传输 路径 相对路径, 绝对路径 File File对象就表示一个路径&#xff0c;可以是文件的路径、也可以是文件夹的路径这个路径可以是存在的&#xff0c;也允许是不存在的 构造方法 代码示例: package FileTest1;import java.io.File;public c…

Verilog 入门(四)(门电平模型化)

文章目录 内置基本门多输入门简单示例 内置基本门 Verilog HDL 中提供下列内置基本门&#xff1a; 多输入门 and&#xff0c;nand&#xff0c;or&#xff0c;nor&#xff0c;xor&#xff0c;xnor 多输出门 buf&#xff0c;not 三态门上拉、下拉电阻MOS 开关双向开关 门级逻辑…

CAN总线学习(STM32的CAN寄存器使用)(笔记二)

CAN总线基础基础知识的文章&#xff1a;CAN总线学习&#xff08;CAN总线基础知识&#xff09;&#xff08;笔记一&#xff09;-CSDN博客 在前面CAN总线基础知识和报文中介绍的是报文内容是比较全面的&#xff0c;STM32在CAN协议的基础上做了一些简单的简化&#xff0c;例如下图…

C++ 抽象类和接口 详解

目录 0 引言1 抽象类2 接口2.1 Java与C接口的区别 &#x1f64b;‍♂️ 作者&#xff1a;海码007&#x1f4dc; 专栏&#xff1a;C专栏&#x1f4a5; 标题&#xff1a;C 抽象类和接口 详解❣️ 寄语&#xff1a;书到用时方恨少&#xff0c;事非经过不知难&#xff01;&#x1f…