浅谈JDK动态代理(下)

作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO

联系qq:184480602,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬

动态代理的使命有两个:

  • 自动生成代理对象,让程序员免受编写代理类的痛苦
  • 将增强代码与代理类(代理对象)解耦,从而达到代码复用

上一篇我们已经顺利通过接口得到代理Class对象,有了代理Class对象意味着代理对象唾手可得。

JDK动态代理的底层逻辑

接下来,我们打算通过代理Class对象得到代理对象。

仔细观察之前的打印信息:

代理Class有一个构造器,需要传入InvocationHandler,虽然我们不知道这是啥,但可以试着传一下(JDK源码自带InvocationHandler):

public class ProxyTest {
    public static void main(String[] args) throws Exception {
        /*
         * 参数1:类加载器,随便给一个
         * 参数2:需要生成代理Class的接口,比如Calculator
         * */
        Class<?> calculatorProxyClazz = Proxy.getProxyClass(Calculator.class.getClassLoader(), Calculator.class);

        // 得到唯一的有参构造 $Proxy(InvocationHandler h),和反射的Method有点像,可以理解为得到对应的构造器执行器
        Constructor<?> constructor = calculatorProxyClazz.getConstructor(InvocationHandler.class);

        // 用构造器执行器执行构造方法,得到代理对象。构造器需要InvocationHandler入参
        Calculator calculatorProxyImpl = (Calculator) constructor.newInstance(new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                return 10086;
            }
        });

        // 看,有同名方法!
        System.out.println(calculatorProxyImpl.add(1, 2));
    }

}

完美,我们顺利得到了梦寐以求的代理对象。

但,InvocationHandler是干嘛的呢?从实验结果看,会发现每次调用代理对象的方法,最终都会调用InvocationHandler的invoke()方法:

怎么做到的呢?

上面不是说了吗,根据代理Class的构造器创建对象时,需要传入InvocationHandler。通过构造器传入一个引用,那么必然有个成员变量去接收。没错,代理对象的内部确实有个成员变量invocationHandler,而且代理对象的每个方法内部都会调用handler.invoke()!也就是说,动态代理为了实现代理对象和增强代码的解耦,把增强代码也抽取出去了,让InvocationHandler作为它与目标对象的桥梁。

JDK动态代理最终生成的其实是Class<Proxy>,最终代理对象是proxy对象,而且实现了Calculator接口。

注意,静态代理的做法是把目标对象传入代理对象,而动态代理则把增强代码传入代理对象。那么,目标对象怎么办,这样一来虽然能执行增强代码,但执行不到目标方法了!

别慌!来看看invoke()方法的参数有哪些:

  • Object proxy:很遗憾,是代理对象本身,而不是目标对象(不要调用,会无限递归,一般不会使用)
  • Method method:方法执行器,用来执行方法(有点不好解释,Method只是一个执行器,传入目标对象就执行目标对象的方法)
  • Obeject[] args:方法参数

为什么参数中没有目标对象呢?其实用大腿想想就能明白:JDK要也懵逼呀,你就传给我一个接口,我给你整出这么一堆东西已经仁至义尽,况且一个接口可以被多个类实现,我哪知道你将来要用这个InvocationHandler给哪个目标对象增强呀!所以JDK怎么可能提前预留目标对象的参数类型呢?

算了,知足吧,好歹有Method代理方法和args参数了,至于目标对象,咱自己解决吧:

至此,我们初步实现动态代理。

如何复用增强代码

抽取方法,简化操作

上面这种方式,太low了,不忍直视...改进一下:

public class ProxyTest {
    public static void main(String[] args) throws Throwable {
        CalculatorImpl target = new CalculatorImpl();
        // 传入目标对象
        Calculator calculatorProxy = (Calculator) getProxy(target);
        calculatorProxy.add(1, 2);
    }

    /**
     * 传入目标对象,获取代理对象
     *
     * @param target
     * @return
     * @throws Exception
     */
    private static Object getProxy(final Object target) throws Exception {
        // 参数1:随便找个类加载器给它 参数2:需要代理的接口
        Class<?> proxyClazz = Proxy.getProxyClass(target.getClass().getClassLoader(), target.getClass().getInterfaces());
        Constructor<?> constructor = proxyClazz.getConstructor(InvocationHandler.class);
        return constructor.newInstance(new InvocationHandler() {
            @Override
            public Object invoke(Object proxy1, Method method, Object[] args) throws Throwable {
                System.out.println(method.getName() + "方法开始执行...");
                Object result = method.invoke(target, args);
                System.out.println(result);
                System.out.println(method.getName() + "方法执行结束...");
                return result;
            }
        });
    }
}

改进后的代码可读性大大增强,简单明了。

解耦代理对象与增强代码

上面的代码还有问题:虽然传入任意对象我们都可以返回增强后的代理对象,但增强代码是写死的。如果我需要的增强不是打印日志而是其他操作呢?难道重新写一个getProxy()方法吗?所以,我们应该抽取InvocationHander,将增强代码和代理对象解耦(其实重写getProxy()和抽取InvocationHander本质相同,但后者细粒度小一些)。

public class ProxyTest {
    public static void main(String[] args) throws Throwable {
        // 1.得到目标对象
        CalculatorImpl target = new CalculatorImpl();
        // 2.传入目标对象,得到增强对象(如果需要对目标对象进行别的增强,可以另外编写getXxInvocationHandler)
        InvocationHandler logInvocationHandler = getLogInvocationHandler(target);
        // 3.传入目标对象+增强代码,得到代理对象
        Calculator calculatorProxy = (Calculator) getProxy(target, logInvocationHandler);
        calculatorProxy.add(1, 2);
    }

    /**
     * 传入目标对象+增强代码,获取代理对象
     *
     * @param target
     * @param handler
     * @return
     * @throws Exception
     */
    private static Object getProxy(final Object target, final InvocationHandler handler) throws Exception {
        // 参数1:随便找个类加载器给它 参数2:需要代理的接口
        Class<?> proxyClazz = Proxy.getProxyClass(target.getClass().getClassLoader(), target.getClass().getInterfaces());
        Constructor<?> constructor = proxyClazz.getConstructor(InvocationHandler.class);
        return constructor.newInstance(handler);
    }

    /**
     * 日志增强代码
     *
     * @param target
     * @return
     */
    private static InvocationHandler getLogInvocationHandler(final CalculatorImpl target) {
        return new InvocationHandler() {
            @Override
            public Object invoke(Object proxy1, Method method, Object[] args) throws Throwable {
                System.out.println(method.getName() + "方法开始执行...");
                Object result = method.invoke(target, args);
                System.out.println(result);
                System.out.println(method.getName() + "方法执行结束...");
                return result;
            }
        };
    }
}

优化代码语义

上面的代码抽取了两个方法,仔细观察你会发现,getLogInvocationHandler(target)的target参数是必要的,但getProxy(target, invocationHandler)的target参数是没必要的:

  • 首先,getProxy()其实只需要知道要实现的接口是什么,就能返回该接口的代理对象不是吗?
  • 其次,invocationHandler已经包含目标对象

所以,代码最后可以优化成这样:

public class ProxyTest {
    public static void main(String[] args) throws Throwable {
        // 1.得到目标对象
        CalculatorImpl target = new CalculatorImpl();
        // 2.传入目标对象,得到增强对象(如果需要对目标对象进行别的增强,可以另外编写getXxInvocationHandler)
        InvocationHandler logInvocationHandler = getLogInvocationHandler(target);
        // 3.传入接口+增强对象(含目标对象),得到代理对象
        Calculator calculatorProxy = (Calculator) getProxy(
                logInvocationHandler,                 // 增强对象(包含 目标对象 + 增强代码)
                target.getClass().getClassLoader(),   // 随便传入一个类加载器
                target.getClass().getInterfaces()     // 需要代理的接口
        );
        calculatorProxy.add(1, 2);
    }

    /**
     * 传入接口+增强(已经包含了目标对象),获取代理对象
     *
     * @param handler
     * @param classLoader
     * @param interfaces
     * @return
     * @throws Exception
     */
    private static Object getProxy(final InvocationHandler handler, final ClassLoader classLoader, final Class<?>... interfaces) throws Exception {
        // 参数1:随便找个类加载器给它 参数2:需要代理的接口
        Class<?> proxyClazz = Proxy.getProxyClass(classLoader, interfaces);
        Constructor<?> constructor = proxyClazz.getConstructor(InvocationHandler.class);
        return constructor.newInstance(handler);
    }

    /**
     * 日志增强代码
     *
     * @param target
     * @return
     */
    private static InvocationHandler getLogInvocationHandler(final CalculatorImpl target) {
        return new InvocationHandler() {
            @Override
            public Object invoke(Object proxy1, Method method, Object[] args) throws Throwable {
                System.out.println(method.getName() + "方法开始执行...");
                Object result = method.invoke(target, args);
                System.out.println(result);
                System.out.println(method.getName() + "方法执行结束...");
                return result;
            }
        };
    }
}

更好用的API:Proxy.newProxyInstance()

目前为止,我们学习都是Proxy.getProxyClass():

  • 先获得proxyClazz
  • 再根据proxyClazz.getConstructor()获取构造器
  • 最后constructor.newInstance()生成代理对象

为了简化代码,我们把这三步封装为getProxy()方法。然而,其实JDK已经提供了一步到位的方法Proxy.newProxyInstance(),你会发现它的参数和我们上面最终封装的getProxy()是一样的:

这说明什么?这说明我们的封装思路已经赶上JDK那帮老秃驴了!

废话不多说,把getProxy()删了,直接用Proxy.newProxyInstance()。

public class ProxyTest {
    public static void main(String[] args) throws Throwable {
        // 1.得到目标对象
        CalculatorImpl target = new CalculatorImpl();
        // 2.传入目标对象,得到增强对象(如果需要对目标对象进行别的增强,可以另外编写getXxInvocationHandler)
        InvocationHandler logInvocationHandler = getLogInvocationHandler(target);
        // 3.传入目标对象+增强代码,得到代理对象(直接用JDK的方法!!!)
        Calculator calculatorProxy = (Calculator) Proxy.newProxyInstance(
                target.getClass().getClassLoader(),   // 随便传入一个类加载器
                target.getClass().getInterfaces(),    // 需要代理的接口
                logInvocationHandler                  // 增强对象(包含 目标对象 + 增强代码)

        );
        calculatorProxy.add(1, 2);
    }

    /**
     * 日志增强代码
     *
     * @param target
     * @return
     */
    private static InvocationHandler getLogInvocationHandler(final CalculatorImpl target) {
        return new InvocationHandler() {
            @Override
            public Object invoke(Object proxy1, Method method, Object[] args) throws Throwable {
                System.out.println(method.getName() + "方法开始执行...");
                Object result = method.invoke(target, args);
                System.out.println(result);
                System.out.println(method.getName() + "方法执行结束...");
                return result;
            }
        };
    }
}

完美。

至此,我们又完成了动态代理的第二个目标:代码复用。不仅目标对象与增强代码解耦,代理对象也和增强代码解耦了。

探究代理Class和代理对象

如果你只是想学会动态代理,上面的内容足够了。但我相信,对于JDK生成的代理Class对象和最终生成的代理对象,大家都有点云里雾里,想刨根问底。

所以,这一小节我们一起来聊聊Proxy。

Proxy.getProxyClass()会先克隆接口信息得到新的Class对象,然后进行后续的一系列处理。

最终得到的Class对象其实是Class<Proxy>,也就意味着代理对象是Proxy的实例:

部分同学可能会对实例类型的判断有点懵,这里再举一个例子:

myProxy既是AnyClass类型,又是Collection类型。extends相当于亲爹,只能有一个,而implements相当于认干爹,可以多个。但不论是哪种形式,都是对象的爹呀!

JDK动态代理小结

我想了个很骚的比喻,希望能解释清楚:

接口Class对象是大内太监,里面的方法和字段比做他的一身武艺,但是他没有XDD(构造器),所以不能new实例,一身武艺后继无人。

那怎么办呢?

正常途径(静态代理):

写一个类,实现该接口。这个就相当于大街上拉了一个人,认他做干爹。一身武艺传给他,只是比他干爹多了小DD,可以new实例。只要再传入目标对象,就能得到增强后的代理对象。

非正常途径(动态代理):

通过妙手圣医Proxy的克隆大法(Proxy.getProxyClass()),克隆一个Class,但是有小DD。所以这个克隆人Class可以创建实例,也就是代理对象。代理Class其实就是附有构造器的接口Class,一样的类结构信息,却能创建实例。

JDK根据接口生成的其实是Proxy的Class对象,然后根据ProxyClass得到proxy代理对象。proxy代理对象实现了接口,同时也是Proxy类型的。proxy对象的原理是:内部维护一个InvocationHandler,而InvocationHandler是对增强代码的抽象。通过抽取InvocationHandler,将代理对象和增强代码解耦。

但此时代理对象内部只有InvocationHandler,没有目标对象,如何最终调用目标方法呢?只需要把目标对象传给InvocationHandler就好啦。

public class ProxyTest {
    public static void main(String[] args) throws Throwable {
        // 1.得到目标对象
        CalculatorImpl target = new CalculatorImpl();
        // 2.传入目标对象,得到增强对象(如果需要对目标对象进行别的增强,可以另外编写getXxInvocationHandler)
        InvocationHandler logInvocationHandler = getLogInvocationHandler(target);
        // 3.传入目标对象+增强代码,得到代理对象(直接用JDK的方法!!!)
        Calculator calculatorProxy = (Calculator) Proxy.newProxyInstance(
                target.getClass().getClassLoader(),   // 随便传入一个类加载器
                target.getClass().getInterfaces(),    // 需要代理的接口
                logInvocationHandler                  // 增强对象(包含 目标对象 + 增强代码)

        );
        calculatorProxy.add(1, 2);
    }

    /**
     * 日志增强代码
     *
     * @param target
     * @return
     */
    private static InvocationHandler getLogInvocationHandler(final CalculatorImpl target) {
        return new InvocationHandler() {
            @Override
            public Object invoke(Object proxy1, Method method, Object[] args) throws Throwable {
                System.out.println(method.getName() + "方法开始执行...");
                Object result = method.invoke(target, args);
                System.out.println(result);
                System.out.println(method.getName() + "方法执行结束...");
                return result;
            }
        };
    }
}

其实就是调用链路拉长了,原本代理对象直接调用目标对象,现在是代理对象调InvocationHandler,InvocationHandler再调目标对象。Proxy代理对象内部有InvocationHandler对象,而InvocationHandler对象内部有我们塞进去的目标对象,所以最终通过代理对象可以调用到目标对象,并且得到了增强。所以,代理模式就是俄罗斯套娃...

最后我们还对代码做了优化,当然有些人可能还是觉得第一版好,哈哈。

作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO

进群,大家一起学习,一起进步,一起对抗互联网寒冬

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

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

相关文章

【开源】基于Vue.js的用户画像活动推荐系统

项目编号&#xff1a; S 061 &#xff0c;文末获取源码。 \color{red}{项目编号&#xff1a;S061&#xff0c;文末获取源码。} 项目编号&#xff1a;S061&#xff0c;文末获取源码。 目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 数据中心模块2.2 兴趣标签模块2.3 活…

visionOS空间计算实战开发教程Day 6 拖拽和点击

在之前的学习中我们在空间中添加了3D模型&#xff0c;但在初始摆放后就无法再对其进行移动或做出修改。本节我们在​​Day 5​​显示和隐藏的基础上让我们模型可以实现拖拽效果&#xff0c;同时对纯色的立方体实现点击随机换色的功能。 首先是入口文件&#xff0c;无需做出改变…

leedcode 刷题 - 除自身以外数组的乘积 - 和为 K 的子数组

I238. 除自身以外数组的乘积 - 力扣&#xff08;LeetCode&#xff09; 给你一个整数数组 nums&#xff0c;返回 数组 answer &#xff0c;其中 answer[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积 。 题目数据 保证 数组 nums之中任意元素的全部前缀元素和后缀的乘积都在…

『Linux升级路』基础开发工具——gcc/g++篇

&#x1f525;博客主页&#xff1a;小王又困了 &#x1f4da;系列专栏&#xff1a;Linux &#x1f31f;人之为学&#xff0c;不日近则日退 ❤️感谢大家点赞&#x1f44d;收藏⭐评论✍️ 目录 一、快速认识gcc/g 二、预处理 &#x1f4d2;1.1头文件展开 &#x1f4d2;1…

qgis添加postgis数据

左侧浏览器-PostGIS-右键-新建连接 展开-双击即可呈现 可以点击编辑按钮对矢量数据编辑后是直接入库的&#xff0c;因此谨慎使用。

Rust语言入门教程(一) - 简介及Cargo使用

Rust编程入门 为什么学习Rust 我本人是一个DevOps工程师&#xff0c;并不是专职的开发人员&#xff0c;但需要了解各种各样的语言的基本知识和特性&#xff0c;以便在不同的项目中帮助开发人员设计软件架构&#xff0c;部署流程以及进行错误排查和调试。但是对任何新生的优秀…

Linux常用命令——blkid命令

在线Linux命令查询工具 blkid 查看块设备的文件系统类型、LABEL、UUID等信息 补充说明 在Linux下可以使用blkid命令对查询设备上所采用文件系统类型进行查询。blkid主要用来对系统的块设备&#xff08;包括交换分区&#xff09;所使用的文件系统类型、LABEL、UUID等信息进行…

JavaScript异步编程

同步与异步 #先看 2 段代码 <span style"background-color:#282c34"><span style"color:#2c3e50"><span style"color:#cccccc"><code><span style"color:#999999">//代码1</span> <span styl…

C++学习之路(一)什么是C++?如何循序渐进的学习C++?【纯干货】

C是一种高级编程语言&#xff0c;是对C语言的扩展和增强。它在C语言的基础上添加了面向对象编程&#xff08;OOP&#xff09;的特性&#xff0c;使得开发者能够更加灵活和高效地编写代码。 C的名字中的“”符号表示在C语言的基础上向前发展一步&#xff0c;即“加加”&#x…

多功能回馈式交流电子负载的应用

多功能回馈式交流电子负载是用于模拟和测试电源、电池等电子设备的负载工具。它具有多种应用&#xff0c;可以用于测试和评估各种类型的电源&#xff0c;包括直流电源和交流电源。它可以模拟各种负载条件&#xff0c;如恒定电流、恒定电压和恒定功率&#xff0c;以验证电源的性…

电脑技巧:推荐八个非常实用的在线网站值得收藏

目录 1、wikihow 干货分享网站 2、次元小镇 二次元必备网站 3、AI创作家 4、SKRbt 搜索引擎网站 5、barbg 全球资源网站 6、书签地球 7、4KHDR世界 8、a real me 今天小编给大家推荐八个非常实用的在线网站值得收藏&#xff01; 1、wikihow 干货分享网站 这个网站是一…

微信小程序制作

如果你也想搭建一个小程序&#xff0c;但不知道如何入手&#xff0c;那么今天我就教你如何使用第三方制作平台&#xff0c;在短短三十分钟内搭建一个小程序。 一、登录小程序制作平台 首先&#xff0c;登录到小程序制作平台的官方网站或应用程序&#xff0c;进入后台管理系统。…

geemap学习笔记013:为遥感动态GIF图添加图名

前言 遥感动态GIF图可以展示地理区域随时间的变化&#xff0c;这对于监测自然灾害、湿地变化、城市扩展、农田变化等方面非常有用&#xff0c;并且可以反复观察图像&#xff0c;以更深入地了解地表的动态变化。本节主要是对遥感动态GIF图添加图名&#xff0c;以便于更好地理解…

【Redis】前言--redis产生的背景以及过程

一.介绍 为什么会出现Redis这个中间件&#xff0c;从原始的磁盘存储到Redis中间又发生了哪些事&#xff0c;下面进入正题 二.发展史 2.1 磁盘存储 最早的时候都是以磁盘进行数据存储&#xff0c;每个磁盘都有一个磁道。每个磁道有很多扇区&#xff0c;一个扇区接近512Byte。…

基于STC12C5A60S2系列1T 8051单片读写掉电保存数据IIC总线器件24C02一字节并显示在液晶显示器LCD1602上应用

基于STC12C5A60S2系列1T 8051单片读写掉电保存数据IIC总线器件24C02一字节并显示在液晶显示器LCD1602上应用 STC12C5A60S2系列1T 8051单片机管脚图STC12C5A60S2系列1T 8051单片机I/O口各种不同工作模式及配置STC12C5A60S2系列1T 8051单片机I/O口各种不同工作模式介绍IIC通信简单…

【深度学习】如何选择神经网络的超参数

1. 神经网络的超参数分类 神经网路中的超参数主要包括: 1. 学习率 η 2. 正则化参数 λ 3. 神经网络的层数 L 4. 每一个隐层中神经元的个数 j 5. 学习的回合数Epoch 6. 小批量数据 minibatch 的大小 7. 输出神经元的编码方式 8. 代价函数的选择 9. 权重初始化的方法 …

IP 代理的基础知识有哪些?

本文将介绍流冠IP代理的基础知识&#xff0c;帮助您了解IP代理的概念、类型、作用、设置方法和注意事项。 一、IP代理的概念 IP代理是一种网络代理服务&#xff0c;它通过代理服务器帮助用户访问互联网&#xff0c;并将用户的请求转发到目标网站&#xff0c;同时将目标网站的响…

Pycharm 教育版下载

1 访问 Jetbrains 主站-->Developer Tools-->PyCharm JetBrains: Essential tools for software developers and teams 2 页面往下划找到 PyCharm for Education-->CHECK IT OUT! PyCharm: the Python IDE for Professional Developers by JetBrains 3 点击 Downloa…

【SpringMVC】 对请求的不同响应

前言 本文学习如何运用不同的注解来返回不同的响应. 1.返回静态页面Controller 返回index.html页面 Controller 和 RestController的区别 controller 只有加上这个注解,Spring才会帮我们管理这个代码.后续我们访问时才能访问到. RestController 等同于 Controller ResponseBo…

域控操作五:统一熄屏睡眠时间

直接看图路径&#xff0c;我只设置了熄屏&#xff0c;如果要睡眠就下面那个启用设置时间