【深度思考】聊聊JDK动态代理原理

1. 示例

首先,定义一个接口:

public interface Staff {
    void work();
}

然后,新增一个类并实现上面的接口:

public class Coder implements Staff {
    @Override
    public void work() {
        System.out.println("认真写bug……");
    }
}

假设现在有这么一个需求:在不改动以上类代码的前提下,对该方法增加一些前置操作或者后置操作。

接下来就来讲解下,如何使用JDK动态代理来实现这个需求。

首先,自定义一个调用处理器,实现java.lang.reflect.InvocationHandler接口并重写invoke方法:

public class AttendanceInvocationHandler implements InvocationHandler {
    private final Object target;

    public AttendanceInvocationHandler(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("上班打卡……");

        Object invoke = method.invoke(target, args);

        System.out.println("下班打卡……");

        return invoke;
    }
}

重点看下Object invoke = method.invoke(target, args);,该行代码会执行真正的目标方法,在这前后,我们可以添加一些增强逻辑。

然后,新建个测试类,看下JDK动态代理如何使用:

public class JdkProxyTest {
    public static void main(String[] args) {
        Coder coder = new Coder();
        AttendanceInvocationHandler h = new AttendanceInvocationHandler(coder);
        // 创建代理对象
        Object proxyInstance = Proxy.newProxyInstance(coder.getClass().getClassLoader(),
                coder.getClass().getInterfaces(),
                h);
        Staff staff = (Staff) proxyInstance;
        staff.work();
    }
}

运行以上代码,效果如下图所示:

【深度思考】聊聊JDK动态代理原理

从运行结果可以看出,在目标方法的前后,执行了自定义的操作。

2. 原理

这里理解2个概念,目标对象和代理对象,

目标对象是真正要调用的对象,上面示例中的Coder类就是目标对象,

代理对象是JDK自动生成的对象,在代理对象内部会去调用目标对象的目标方法。

JDK动态代理的核心就是上面示例中的Proxy.newProxyInstance方法,方法签名如下图所示:

【深度思考】聊聊JDK动态代理原理

第1个参数传入的是目标对象的ClassLoader,第2个参数传入的是目标对象的接口信息,第3个参数传入的是自定义的InvocationHandler。

然后看下该方法的实现逻辑,先看第1处重点:

【深度思考】聊聊JDK动态代理原理

注释翻译过来是:查找或者生成指定的代理类。

该方法会生成代理类的字节码文件(也可能是从缓存中读取),核心逻辑在ProxyClassFactory类的apply方法中,

该方法中定义了生成的代理类的包名以及文件名:

【深度思考】聊聊JDK动态代理原理

【深度思考】聊聊JDK动态代理原理

因此默认情况下,自动生成的代理类名称是com.sun.proxy.$Proxy0

该方法最后会生成代理类的字节码,默认情况下不会保存到文件系统,但可以通过参数指定保存到文件系统:

【深度思考】聊聊JDK动态代理原理

【深度思考】聊聊JDK动态代理原理

可以看出,保存不保存到文件系统,受saveGeneratedFiles的影响,其定义如下所示:

private static final boolean saveGeneratedFiles = (Boolean)AccessController.doPrivileged(new GetBooleanAction("sun.misc.ProxyGenerator.saveGeneratedFiles"));

所以可以通过指定sun.misc.ProxyGenerator.saveGeneratedFiles参数来让生成的代理类字节码文件保存到文件系统中。

然后看第2处重点:

【深度思考】聊聊JDK动态代理原理

先是获取构造函数,然后是生成代理类对象的实例。

3. 为什么必须要基于接口?

思考一个问题,为什么JDK动态代理必须要基于接口,带着这个问题,我们看下动态生成的代理类com.sun.proxy.$Proxy0长什么样子?

JVM参数里添加参数-Dsun.misc.ProxyGenerator.saveGeneratedFiles=true,然后启动上面示例中的测试代码:

【深度思考】聊聊JDK动态代理原理

【深度思考】聊聊JDK动态代理原理

生成的代理类字节码文件保存在项目根目录下的com/sun/proxy目录下:

【深度思考】聊聊JDK动态代理原理

在IDEA中打开后,如下图所示:

【深度思考】聊聊JDK动态代理原理

在静态代码块中,对静态变量m0、m1、m2、m3进行了赋值,其中m3是要执行的目标方法。

在构造方法中,执行的是super(var1);,也就是父类Proxy的构造方法:

【深度思考】聊聊JDK动态代理原理

该方法是将我们自定义的InvocationHandler赋值给了父类的变量h。

而以下测试代码实际执行的是代理类$Proxy0里的work方法:

Staff staff = (Staff) proxyInstance;
staff.work();

【深度思考】聊聊JDK动态代理原理

代理类$Proxy0里的work方法实际执行的是自定义InvocationHandler里的invoke方法:

【深度思考】聊聊JDK动态代理原理

因此在执行目标方法前后,执行了自定义的前置操作和后置操作。

了解了这个调用过程,就理解了为什么JDK动态代理必须要基于接口,因为动态生成的代理类已经继承了类java.lang.reflect.Proxy

Java又是单继承的,如果想要继续对类进行扩展,只能通过实现接口的方式。

文章持续更新,欢迎关注微信公众号「申城异乡人」第一时间阅读!

原文链接:https://www.cnblogs.com/zwwhnly/p/17324797.html

本站文章如无特殊说明,均为本站原创,如若转载,请注明出处:【深度思考】聊聊JDK动态代理原理 - Python技术站

(0)
上一篇 2023年4月17日
下一篇 2023年4月17日

相关文章

  • SpringBoot2使用Jetty容器操作(替换默认Tomcat)

    Spring Boot 2 使用 Jetty 容器操作(替换默认 Tomcat) Spring Boot 默认使用 Tomcat 作为内嵌的 Servlet 容器,但是实际上我们可以很方便地替换为其他的 Servlet 容器,例如 Jetty。下面是使用 Spring Boot 2 如何集成 Jetty。 集成 Jetty Spring Boot 2 中默认…

    Java 2023年6月2日
    00
  • 利用数组实现栈(Java实现)

    下面就详细讲解一下“利用数组实现栈(Java实现)”的完整攻略。 一、栈的概念 栈是一种具有特殊性质的线性结构,它只允许在一端进行插入和删除操作,这一端被称为栈顶。具体来说,栈的特点是后进先出(Last In First Out,LIFO)。 二、栈的实现 栈可以使用数组实现,这里我们介绍一种基于数组的简单栈实现方法: public class MyStac…

    Java 2023年5月26日
    00
  • 浅谈Java生成唯一标识码的三种方式

    以下是详细讲解“浅谈Java生成唯一标识码的三种方式”的完整攻略。 浅谈Java生成唯一标识码的三种方式 在实际开发中,常常需要生成唯一标识码。Java提供了多种方式来生成唯一标识码,下面将介绍其中三种方式。 1. UUID UUID(Universally Unique Identifier)是一种由网络软件工程师在分布式计算环境中,为了在此环境下生成唯一…

    Java 2023年5月20日
    00
  • SpringBoot集成Jpa对数据进行排序、分页、条件查询和过滤操作

    下面是关于“SpringBoot集成Jpa对数据进行排序、分页、条件查询和过滤操作”的完整攻略。 简介 首先,SpringBoot是一个基于Spring框架的快速开发框架。而Jpa则是Java持久层API的规范,通过使用Jpa规范,我们可以很方便地实现与数据库的交互。本文主要介绍如何使用SpringBoot集成Jpa,对数据进行排序、分页、条件查询和过滤操作…

    Java 2023年5月20日
    00
  • Java基于正则实现的日期校验功能示例

    下面我给出一份详细的Java基于正则实现的日期校验功能示例的攻略。 1. 确定校验的日期格式 在进行日期校验前,需要先确定待校验的日期格式。例如,我们可以使用yyyy-MM-dd作为日期的格式,它表示年份、月份、日期之间以“-”符号隔开。 2. 正则表达式的构建 构建日期校验的正则表达式时,需要考虑以下几点: 年份必须为4位数字,可以使用\d{4}表示。 月…

    Java 2023年5月20日
    00
  • IDEA反编译出整个jar包源码

    你好,关于“IDEA反编译出整个jar包源码”的完整攻略,我可以提供以下几个步骤: 步骤一:安装插件 首先,你需要在 IDEA 中安装一个名为 “JD-Eclipse”的插件。这个插件可以在 IDEA 中实现反编译的功能。安装插件可以按照 IDEA 的标准步骤进行,在 IDEA 的插件中心选择安装即可。 步骤二:打开jar包 打开 IDEA,选择 “File…

    Java 2023年5月26日
    00
  • Eclipse中配置Maven的图文教程

    下面是Eclipse中配置Maven的详细攻略: 步骤一:下载安装Maven 首先,我们需要下载并安装Maven,这里我们推荐使用Apache Maven官方网站提供的二进制版本。以下是下载Maven的步骤: 访问官方网站:https://maven.apache.org/download.cgi 选择最新的二进制版本进行下载。 下载完成后,解压到本地某个目…

    Java 2023年5月19日
    00
  • Mybatis实现动态增删改查功能的示例代码

    让我们来详细讲解”Mybatis实现动态增删改查功能的示例代码”的完整攻略吧。 1. 引入Mybatis-Plus依赖 首先,我们需要在项目中引入Mybatis-Plus的依赖。在pom.xml文件中添加以下内容: <dependency> <groupId>com.baomidou</groupId> <artif…

    Java 2023年5月20日
    00
合作推广
合作推广
分享本页
返回顶部