浅谈JDK动态代理(中)

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

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

动态代理的使命

在做日志需求时,我们想到的第一种方案是直接修改原代码,它的缺点是:

  • 不符合开闭原则,即好的程序设计应该对扩展开放,对修改关闭
  • 如果Calculator类内部有几十个、上百个方法,为每个方法加上日志打印显然工作量太大
  • 存在重复代码(有多少个方法,就有多少处日志操作)
  • 日志打印的操作硬编码在源程序中,不利于后期维护:比如你花了一上午终于写完了,组长却告诉你这个功能不做了,于是你又要删除/回滚相关代码!

后来我们引入了静态代理:

静态代理解决了以下问题:

  • 不修改源程序,符合开闭原则(所有修改转移到代理类)
  • 既然不修改源程序,那么也就不存在增强操作与源程序硬编码的问题(但又变成代理类和增强操作耦合了)

但静态代理本质上并没有太大用处,它只是把原本在源程序上做的修改,转移到代理类而已!即便引入静态代理,仍旧需要重写全部方法、仍然存在重复的日志代码。

你如果停下来稍作思考,就会发现从修改原代码到引入静态代理,其实就是趋向解耦的过程:

  • 原本我们把代码直接写在目标类中,日志代码和目标类耦合了,所以一旦需求发生变更,我们又要去修改目标类
  • 静态代理则把日志代码抽取出来,放在代理类中,解决了增强代码和目标类的耦合,但又造成了增强代码和代理类的耦合

换言之,静态代理的解耦能力还是太薄弱了,要想对源程序实现不同的增强功能,必须编写不同的代理类,有多少种增强需求,就要写多少个静态代理类!

我们的诉求是:增强代码我可以写(这个省不了,不然鬼知道你要打印日志还是啥),但代理类能不能不写?

要想完成上面的诉求,至少需要解决两个问题:

  • 自动生成代理对象,让程序员免受编写代理类的痛苦
  • 将增强代码与代理类(代理对象)解耦,从而达到代码复用(可插拔式的增强,给我增强的代码,我就返回一个实现了该增强的代理对象)

当然,上面只是一个构思,你可能还看不明白。没关系,我们一个个解决。

思考的过程:如何自动生成代理对象

复习对象的创建

在很多初学者的印象中,类和对象的关系是这样的:

虽然知道源代码经过javac命令编译后会在磁盘中得到字节码文件(.class文件),也知道java命令会启动JVM将字节码文件加载进内存,但也仅仅止步于此了。至于从字节码文件加载进内存到堆中产生对象,期间具体发生了什么,他们并不清楚。

实际上,所谓“万物皆对象”,字节码文件也难逃“被对象”的命运。当字节码文件(.class文件)被加载进内存后,JVM也为其创建了一个对象,以后所有该类的实例,皆以它为模板。这个对象叫Class对象,它是Class类的实例。

可以看出,要创建一个实例,最关键的就是得到对应的Class对象。只不过对于初学者来说,new这个关键字配合构造方法,实在太好用了,底层隐藏了太多细节,一句 Person p = new Person();直接把对象返回给你了。我自己刚开始学Java时,也没意识到Class对象的存在。

回到我们之前的问题:如何不写代理类,直接得到代理对象。

按照上面的截图,代理类和实例对象之间其实还隔着一个Class对象。如果能得到Class对象,就能生成实例。所以,现在的问题又变成:如何不写代理类,直接得到Class对象。

Class类与Class对象

要得到Class对象,就要先明白Class对象是什么,又是怎么来的。

这有一个很重要的概念:Class类。

类是用来描述一类事物的,我们有Person类描述“人”,Student类描述“学生”,而Class类就是用来描述“类”的。是不是觉得有点绕?换句话说:

类可以用来描述任意事物,所以理论上我们也能定义一个类,用来描述类本身,但这个Class类不需要我们自己写,JDK已经帮我们定义好了,放在java.lang包下。Class类、Person类、Student类本质相同,只不过Class类描述的东西比较特殊罢了。

Person类内部有name、age等字段来描述“人”的姓名、年龄等特征,那么这个Class类,它里面应该有哪些字段呢?

关于Class类内部具体有哪些字段和方法,这里不再展开,有兴趣的同学可以移步《反射》一节。

理论上只要编写了类,那么通过JVM通常可以得到该类的对象,Class类的对象就是Class对象。现在我问大家一个问题:

Person类的实例对象是Person p,那么Person类的Class对象怎么表示?

是不是又有点绕呢?

还是看看刚才的那张图吧:

Person类有两个不同维度的对象:

  • 根据Person类实例化得到的Person p1、 p2、p3对象
  • Class类实例化得到的Class<Person> personClass对象

第一个“对象”好理解,就是我们经常new的那种对象,关键是Class对象。Class类只有一个,却要描述形形色色的各种类,比如Person类、Student类,那么如何区分谁是谁的Class对象呢?

答案是:泛型。

Class类是泛型类,JDK利用泛型区分不同的Class对象,比如Class<Person> personClass、Class<Student> studentClass。

要得到Person p只需要new Person(),那如何得到Class<Person> personClass呢?这样吗:

Class clazz = new Class();

好像不对。我们可以去看看Class类:

上面这张截图,至少传递两个信息:

  • Class类是泛型类
  • Class类私有构造,意味着我们无法通过new关键字自行构造Class对象

通过上面的注释,我们得知Class对象只能由JVM创建。虽然不能new,但Java还是提供了其他方式让我们得到Class对象,底层会告诉JVM帮我们创建:

  1. Class.forName(xxx):Class<Person> clazz = Class.forName("com.bravo.Person");
  2. xxx.class:Class<Person> clazz = Person.class;
  3. xxx.getClass():Class<Person> clazz = person.getClass();

OK,学到这,我们已经了解了Class对象到底是什么,以及得到Class对象的三种常见方式。但是,这三种方式都需要先有类,但我们不想编写代理类!

到这里,思路似乎断了!

从接口寻求突破口!

仔细想一下,代理类或者代理对象重要吗?它几乎是个空壳,最重要的其实是 增强代码 + 目标对象。换句话说,我们对代理对象的要求很低,只需要与目标对象拥有相同的方法即可。如此一来,别人调用proxy.add()得到的效果和调用target.add()是一样的,甚至因为两者都实现了相同接口,用接口类型接收后,calculator.add()根本分不出是代理还是原对象。

所以本质上,代理对象只要有方法申明即可,甚至不需要方法体,或者只要一个空的方法体即可,反正我们会把目标对象返回去。

那么,如何知道一个类有哪些方法信息呢?如果能得到类的方法信息,我们或许可以直接造一个代理对象。

有两个途径:

  • 目标类本身
  • 目标类实现的接口

这两个思路造就了两种不同的代理机制,一个被后人称为CGLib动态代理,另一个则被JDK收录,世人称之为JDK动态代理。本文重点介绍JDK动态代理。

我们先来验证一下,接口是否真的包含我们需要的方法信息:

public class ProxyTest {
    public static void main(String[] args) {
        /**
         * Calculator接口的Class对象
         * 得到Class对象的三种方式:
         * 1.Class.forName(xxx)
         * 2.xxx.class
         * 3.xxx.getClass()
         * 注意,这并不是我们new了一个Class对象,而是让虚拟机加载并创建Class对象
         */
        Class<Calculator> calculatorClazz = Calculator.class;
        //Calculator接口的构造器信息
        Constructor<?>[] calculatorClazzConstructors = calculatorClazz.getConstructors();
        //Calculator接口的方法信息
        Method[] calculatorClazzMethods = calculatorClazz.getMethods();
        //打印
        System.out.println("------接口Class的构造器信息------");
        printClassInfo(calculatorClazzConstructors);
        System.out.println("\n");
        System.out.println("------接口Class的方法信息------");
        printClassInfo(calculatorClazzMethods);
        System.out.println("\n");

		/**
		 * Calculator实现类的Class对象
		 */
		Class<CalculatorImpl> calculatorImplClazz = CalculatorImpl.class;
        //Calculator实现类的构造器信息
        Constructor<?>[] calculatorImplClazzConstructors = calculatorImplClazz.getConstructors();
        //Calculator实现类的方法信息
        Method[] calculatorImplClazzMethods = calculatorImplClazz.getMethods();
        //打印
        System.out.println("------实现类Class的构造器信息------");
        printClassInfo(calculatorImplClazzConstructors);
        System.out.println("\n");
        System.out.println("------实现类Class的方法信息------");
        printClassInfo(calculatorImplClazzMethods);
    }

    public static void printClassInfo(Executable[] targets) {
        for (Executable target : targets) {
            // 构造器/方法名称
            String name = target.getName();
            StringBuilder sBuilder = new StringBuilder(name);
            // 拼接左括号
            sBuilder.append('(');
            Class<?>[] clazzParams = target.getParameterTypes();
            // 拼接参数
            for (Class<?> clazzParam : clazzParams) {
                sBuilder.append(clazzParam.getName()).append(',');
            }
            //删除最后一个参数的逗号
            if (clazzParams.length != 0) {
                sBuilder.deleteCharAt(sBuilder.length() - 1);
            }
            //拼接右括号
            sBuilder.append(')');
            //打印 构造器/方法
            System.out.println(sBuilder.toString());
        }
    }
}

得到以下结论:

  • 接口Class对象没有构造方法,所以Calculator接口不能直接new对象
  • 实现类Class对象有构造方法,所以CalculatorImpl实现类可以new对象
  • 接口Class对象有两个方法add()、subtract()
  • 实现类Class对象除了add()、subtract(),还有从Object继承的方法

也就是说,接口Class的对象和实现类的Class对象除了构造器,其他信息基本相似(目标类由于继承了Object,所以内部包含了Object的方法,与代理无关)。

至此,我们至少知道从接口获取方法信息是可能的!接下来的努力方向就是:怎么根据一个接口得到代理对象。

引入JDK动态代理

通过刚才的实验,我们不仅知道了接口确实包含我们所需要的方法信息,还知道了接口为什么不能直接new对象:接口缺少构造器信息。那么,是否存在一种机制,能给接口安装上构造器呢?或者,不改变接口本身,直接拷贝接口的信息到另一个Class,然后给那个Class装上构造器呢?

很显然,不论是从开闭原则还是常规设计考虑,直接修改接口Class的做法相对来说不是很合理。JDK选择了后者:拷贝接口Class的信息,产生一个新的Class对象。

也就是说,JDK动态代理的本质是:用Class造Class,即用接口Class造出一个代理类Class。

具体API就不带大家找了,直接看:

Proxy.getProxyClass():返回代理类的Class对象。

也就说,只要传入接口的Class对象,getProxyClass()方法即可返回代理Class对象,而不用实际编写代理类。这相当于什么概念?

没错,好家伙,直接跳过了代理类的编写!

public class ProxyTest {
    public static void main(String[] args) {
        /*
         * 参数1:Calculator的类加载器(当初把Calculator加载进内存的类加载器)
         * 参数2:代理对象需要和目标对象实现相同接口Calculator
         * */
        Class<?> calculatorProxyClazz = Proxy.getProxyClass(Calculator.class.getClassLoader(), Calculator.class);
        //以Calculator实现类的Class对象作对比,看看代理Class是什么类型
        System.out.println(CalculatorImpl.class.getName());
        System.out.println(calculatorProxyClazz.getName());
        //打印代理Class对象的构造器
        Constructor<?>[] constructors = calculatorProxyClazz.getConstructors();
        System.out.println("----构造器----");
        printClassInfo(constructors);
        System.out.println("\n");
        //打印代理Class对象的方法
        Method[] methods = calculatorProxyClazz.getMethods();
        System.out.println("----方法----");
        printClassInfo(methods);
        System.out.println("\n");
    }

    public static void printClassInfo(Executable[] targets) {
        for (Executable target : targets) {
            // 构造器/方法名称
            String name = target.getName();
            StringBuilder sBuilder = new StringBuilder(name);
            // 拼接左括号
            sBuilder.append('(');
            Class<?>[] clazzParams = target.getParameterTypes();
            // 拼接参数
            for (Class<?> clazzParam : clazzParams) {
                sBuilder.append(clazzParam.getName()).append(',');
            }
            //删除最后一个参数的逗号
            if (clazzParams.length != 0) {
                sBuilder.deleteCharAt(sBuilder.length() - 1);
            }
            //拼接右括号
            sBuilder.append(')');
            //打印 构造器/方法
            System.out.println(sBuilder.toString());
        }
    }
}

大家还记得原先接口Class的打印信息吗?

没错,Proxy.getProxyClass()返回的Class对象是有构造器的!

开头说了,动态代理的使命有两个:

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

现在我们已经得到了代理Class,只需通过反射即可得到代理对象。具体的操作放在下一篇。

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

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

 

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

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

相关文章

如何用CHAT解释文章含义?

问CHAT&#xff1a;解释“ 本身乐善好施&#xff0c;令名远近共钦&#xff0c;待等二十左右&#xff0c;定有高亲可攀&#xff1b;而且四德俱备&#xff0c;帮夫之缘亦有。主持家事不紊&#xff0c;上下亦无闲言。但四十交进&#xff0c;家内谨防口舌&#xff0c;须安家堂&…

基于SpringBoot垃圾分类小程序系统的设计与实现

文章目录 一&#xff0c;系统演示二&#xff0c;系统核心代码介绍 最近开发了一个垃圾分类的小程序。 因为导师一直也是要学生做创新点&#xff0c;前前后改了几个版本&#xff0c;好在过了&#xff0c;也算不错。 我每天基本5点半起床&#xff0c;晚上11点睡觉&#xff0c;基…

大模型变身双面人:虚假新闻制造机VS假新闻鉴别大师!

大家是怎样看待大型语言模型生成信息的可靠性呢&#xff1f; 尽管大语言模型生成的内容“像模像样”&#xff0c;但这些模型偶尔的失误揭示了一个关键问题&#xff1a;它们生成的内容并不总是真实可靠的。 那么&#xff0c;这种“不保真”特性能否被用来制造虚假信息呢&#x…

百战python01-初识python_turtle绘图

文章目录 简介练习1.简易的进度条学习使用turtle在屏幕上绘制图形注:需要对python有基本了解,可查看本作者python基础专栏,有任何问题欢迎私信或评论(本专栏每章内容都将不定期进行内容扩充与更新) 简介 python简介及方向+pycharm安装使用请转 练习 注:尝试练习。了解…

基于51单片机的病床呼叫系统设计

**单片机设计介绍&#xff0c; 基于51单片机的病床呼叫系统设计 文章目录 一 概要二、功能设计设计思路 三、 软件设计原理图 五、 程序六、 文章目录 一 概要 基于51单片机的病床呼叫系统是一种用于医疗机构的设备&#xff0c;旨在提供快速、可靠的病人呼叫和监控功能。以下是…

Django框架环境的搭建(图文详解)

目录 day01 Web框架和Django基础 1.web框架底层 1.1 网络通信​编辑 1.2 常见软件架构 1.3 手撸web框架 2.web框架 2.1 wsgiref 2.2 werkzeug 2.3 各框架的区别 3.快速上手django框架 3.1 安装 3.2 命令行 3.3 Pycharm 4.虚拟环境 4.1 创建虚拟环境 - 命令行 4…

MySQL用户与权限管理

快捷查看指令 ctrlf 进行搜索会直接定位到需要的知识点和命令讲解&#xff08;如有不正确的地方欢迎各位小伙伴在评论区提意见&#xff0c;博主会及时修改&#xff09; MySQL用户与权限管理 登录 #本地登录 mysql -uroot -p123456#远程登录 #客户端语法&#xff1a;mysql -…

K8S部署mongodb-sharded-cluster(7.0.2)副本分片

添加源 helm repo add bitnami https://charts.bitnami.com/bitnami指定版本拉取 helm pull --repo https://charts.bitnami.com/bitnami mongodb-sharded --version 7.0.5安装时选择SCRAM-SHA-1默认是SCRAM-SHA-256 helm install -n prod mymongodb mongodb-sharded --value…

电线电缆行业生产管理怎么数字化?

行业介绍 随着市场环境的变化和现代生产管理理念的不断更新&#xff0c;电缆的生产模式也在发生转变&#xff0c;批量小&#xff0c;规格多&#xff0c;交期短的新型制造需求逐年上升&#xff0c;所以企业车间管理的重要性越发凸显&#xff0c;作为企业良性运营的关键&#xf…

PRD学习

产品经理零基础入门&#xff08;五&#xff09;产品需求文档PRD&#xff08;全16集&#xff09;_哔哩哔哩_bilibili 1. PRD的2种表现形式 ① RP格式 &#xff08;1&#xff09;全局说明 ② 文档格式 2. 交互说明撰写 ① 维度 ② 步骤 ③ 规则 &#xff08;1&#xff09;单位…

MySql表中添加emoji表情

共五处需要修改。 语句执行修改&#xff1a; ALTER TABLE xxxxx CONVERT TO CHARACTER SET utf8mb4;

C运算符与表达式

跟着肯哥&#xff08;不是我&#xff09;学运算符与表达式 运算符 在C语言中&#xff0c;运算符是一种用来执行特定操作的符号或关键字。它们用于对变量、常量和表达进行计算、逻辑判断和位操作等。 定义一般都当耳旁风了 运算符分类 算术运算符 -*/%加减乘除取模&#xff0c;…

2023 年 亚太赛 APMCM (C题)国际大学生数学建模挑战赛 |数学建模完整代码+建模过程全解全析

当大家面临着复杂的数学建模问题时&#xff0c;你是否曾经感到茫然无措&#xff1f;作为2022年美国大学生数学建模比赛的O奖得主&#xff0c;我为大家提供了一套优秀的解题思路&#xff0c;让你轻松应对各种难题。 问题一 为了分析中国新能源电动汽车发展的主要因素&#xf…

Linux常用命令——blockdev命令

在线Linux命令查询工具 blockdev 从命令行调用区块设备控制程序 补充说明 blockdev命令在命令调用“ioxtls”函数&#xff0c;以实现对设备的控制。 语法 blockdev(选项)(参数)选项 -V&#xff1a;打印版本号并退出&#xff1b; -q&#xff1a;安静模式&#xff1b; -v&…

华大基因认知障碍基因检测服务,助力认知障碍疾病防控

认知障碍是一种严重的神经系统疾病&#xff0c;对人类的脑健康产生了重大影响。据报告显示&#xff0c;在我国65岁以上的人群中&#xff0c;存在轻度认知障碍的患者约为3,800万&#xff0c;而中重度痴呆患者则约为1,500万&#xff0c;患病人口数量庞大。这种疾病不仅会对患者的…

【C++11并发】future库 笔记

简介 C11之前&#xff0c;主线程要想获取子线程的返回值&#xff0c;一般都是通过全局变量&#xff0c;或者类似机制。C11开始为我们提供了一组方法来获取子线程的返回值&#xff0c;并保证其原子性。 头文件 #include <future>std::promise 在promise中保存了一个值…

electron27-react-mateos:基于electron+react18仿matePad桌面系统

基于Electron27React18ArcoDesign搭建桌面版OS管理系统。 electron-react-mateos 基于最新前端跨端技术栈electron27.xreact18arco-designzustand4sortablejs构建的一款仿制matePad界面多层级路由管理OS系统。 ElectronReactOS支持桌面多路由配置&#xff0c;新开窗口弹窗开启路…

[Halcon检测] 划痕检测之高斯导数提取

&#x1f4e2;博客主页&#xff1a;https://loewen.blog.csdn.net&#x1f4e2;欢迎点赞 &#x1f44d; 收藏 ⭐留言 &#x1f4dd; 如有错误敬请指正&#xff01;&#x1f4e2;本文由 丶布布原创&#xff0c;首发于 CSDN&#xff0c;转载注明出处&#x1f649;&#x1f4e2;现…

【开源】基于Vue和SpringBoot的大学生相亲网站

项目编号&#xff1a; S 048 &#xff0c;文末获取源码。 \color{red}{项目编号&#xff1a;S048&#xff0c;文末获取源码。} 项目编号&#xff1a;S048&#xff0c;文末获取源码。 目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块三、系统展示四、核心代码4.1 查询会员4…