设计模式之死磕代理模式(原创)

在解释代理模式(静态代理和动态代理)之前,先讲一段故事:

小武跟大多数人一样在广州这座大城市工作,老家在外地,辛苦忙活一整年就期盼寒假那几天可以回老家陪家人吃团年饭放冲天炮感受下家的温暖。春运那几天是人口大迁徙啊,北上广深的大批人马都顺势回流到内陆地区...火车票什么的相应的就是一票难求,使用一些官方购票APP根本抢不到车票好嘛,但小武跟其他很多人一样需要回家啊。所以他必须需要寻求外力的帮助,这时朋友推荐了一个黄牛给他,黄牛说我也可以帮你买票,但是你需要提供乘车人基本信息以及票价和手续费然后我在去帮你抢。小武说,没问题啊只要能买到就行。

我想,这是很多平凡人比较真实的写照。

这段故事简洁点描述就是自己想要在正规渠道买票,发现基本买不了。但是委托黄牛(代理)让他带我们去买却有很大几率买到车票。那么,这种通过增加一个中间件然后去进行实际操作的模式,我们一般称之为代理模式。对于代理模式比较科学的定义是这样:给某一个对象(目标对象)提供一个代理,并由代理对象控制原对象(目标对象)的引用进行操作,这种模式一般称之为代理模式。

代理模式是结构型模式中的一种。既然要弄清楚代理模式,那么必须要先了解什么是结构性设计模式。结构性设计模式存在的目的主要是:在解决了对象的创建问题之后,对象的组成以及对象之间的依赖关系就成了开发人员关注的焦点。因为如何设计对象的结构、继承和依赖关系会影响到后续程序的维护性、代码的健壮性、耦合性等等。因此,结构性模式最主要涉及和关心的是如何组合类和对象来获得更大的结构。结构性模式采用继承机制来组合接口或实现(也称为:类结构性模式),或者通过组合一些对象从而实现新的功能(也称为对象结构性模式)。综上,结构性模式它不是对接口和实现进行组合,而是描述了如何对一些对象进行组合。

那么,如何通过代码去描绘这种代理模式的行为?继续回到上面的例子,通过这段故事仔细分析下可以有以下结论:

A:既然小武和黄牛都有买票的行为规范,那么可以定义一个接口让他们都实现这个接口

B:虽然是黄牛在操作买票,但是买票的乘车人信息都是小武本人(毕竟实名制验证),也就是说黄牛需要持有小武的个人信息才可以买票(用代码的方式表达就是,黄牛这个类 需要持有小武这个对象的引用)

C:虽然进行买票操作的是黄牛,但实际的逻辑还是小武买票(小武可以在代售点、APP、火车站、黄牛进行具体的操作)

D:黄牛可以有多个,小武可以找多个黄牛;小武也可以有多种购票方式,除了寻求黄牛帮助、也可以去火车站、代售点、APP、12306官网、电话订票进行订票,所以,这种方式拓展性较强

理清上面几个基本结论之后,我们首先定义一个买票接口(上面也说到了小武跟黄牛都需要遵循这个行为规范)

购票接口

接着,让小武实现这个接口,目前,小武只是要买票(但是他要提供个人信息,个人信息可以提供给黄牛去操作也可以在12306上面进行购买,但是他必须要提供出来)于是可以有以下代码:

小武

接着我们在定义黄牛,上面说了黄牛首先需要持有小武的对象引用;然后,黄牛买票其实就是小武在间接买票(所以实际操作的是还是小武买票的逻辑):

黄牛

那么,红色矩形代表的就是小武的买票操作(因为黄牛只是代理)

以上代码简单定义完了小武和黄牛,既然黄牛这个对象我们已经构建完毕了,我们只需要实例化一个对象,让他帮我们进行购买即可,下面是测试代码的编写:

代理模式测试

通过以上代码我们可以看到,我们调用黄牛的购票接口实际上是调用小武的功能逻辑。为了加深对代理模式的理解,这里在举个例子:天朝的墙可谓是又高又厚,想要了解外面世界的一些内容需要翻墙才可以。如果要成功翻墙,大家能够想到的是通过一些工具进行代理(你如果说我人直接在墙外这样不就可以了嘛,要是这样那就尴尬了),代理成功以后才可以进行查阅,那么这本质上也是一种代理模式。

总结:

代理模式主要针对的是直接访问对象时可能会带来的问题,比如有些对象由于某些原因(例如对象创建开销很大,或者某些操作需要安全控制,或者需要进程外的访问),直接访问会给使用者或者系统结构带来很多麻烦,因此我们可以在访问此对象时,加上一个对此对象的访问中间层。其优点就在于拓展性极高(黄牛不仅可以卖火车票,它也可以推荐你买飞机票、买长途大巴票)、内部功能隐蔽;缺点就是因为增加了中间件,这样多余的一层可能会造成请求的处理速度相对变慢。

上面代码描绘的代理模式,更加细分的话一般称为静态代理模式,为什么称为静态代理?因为代理对象需要与目标对象实现一样的接口(也就是黄牛和小武实现购票接口),假设现在有这种情况,小武为了保守起见不仅找黄牛,自己还去代售点、去火车站、去各种APP上面进行买票,这种情况下就会有很多代理类(黄牛、代售点、车站、APP),代理类如果太多会出现什么样的问题?一旦接口增加方法,目标对象与代理对象都要维护,这样的话代码改起来就相当烦琐和冗余。为了解决这个问题,Java给我们提供了一种解决方式,这种解决方式大家一般称为动态代理。

动态代理:

Java使用动态代理的关键是使用Proxy这个类以及InvocationHandler这个接口,调用Proxy这个类里面的newProxyInstance 方法进行具体的动态代理操作,首先简单瞄下这个方法的源码:

newProxyInstance 
英文注释

其中红色矩形的是方法的注释,英文翻译过来就是:返回指定接口的代理类的实例,该接口将方法调用分发给指定的调用处理程序。这个静态方法有三个参数:

参数一:ClassLoader loader,指定当前目标对象使用类加载器,我们知道获取加载器的写法是固定的

也就是,Object.getClass().getClassLoader()

参数二:Class<?>[ ] interfaces,目标对象实现的接口的类型,使用泛型方式确认类型,我们知道要想知道一个类是否实现某个接口,可以使用    Object.getClass().getInterfaces() ,这个方法是获取类是否实现接口,如果此对象表示一个类,则返回值是一个数组,它包含了表示该类所实现的所有接口的对象。

参数三:InvocationHandler h,事件处理,执行目标对象的方法时,会触发事件处理器的方法,会把当前执行目标对象的方法作为参数传入

为了方便测试,我把动态代理的类单独写一个类这样方便我们测试和对比,代理模式的写法如下:

动态代理例子

写完之后我们测试一下动态代理模式(老司机可能在上面看到了method.invoke这个方法):

动态代理测试

多提一嘴,可能各位看官更习惯这种写法(也就是将InvocationHandler这个接口单独拿出来写)

实现InvocationHandler

然后开始编写测试类:

测试动态代理

这两种动态代理写法本质是一样的,其实就是将实现了接口的代理类传到 Proxy.newProxyInstance 中。基本的写法就是这样(后面有源码分析)

动态代理模式和静态代理模式这两种模式有那些区别?我们首先通过对比两种模式下的测试代码来分析:

两种模式对比

其中:蓝色矩形代表的是动态代理模式,红色矩形代表的是静态代理模式 

通过这两种模式可以很清晰看到 静态代理模式指向的是实现了接口的具体代理类;而动态模式指向的是代理对象和目标对象使用的共同接口。很明显,动态代理(接口)会比静态代理模式(实现接口的子类)拓展性强

说完了动态模式的使用,下面开始深挖动态代理机制以及源码:

细心的老司机会发现,我们使用代理对象调用接口时候均调用了InvocationHandler这个接口的invoker方法,但内部逻辑使用的是Method反射机制来执行被代理对象(也就是目标对象)的接口方法。首先点进newProxyInstance这个方法一探究竟:

newProxyInstance - 1
newProxyInstance - 2

首先看newProxyInstance-2 这幅图中的蓝色矩形,这个英文注释翻译过来就是: 查找或生成指定的代理类。也就是说getProxyClass0()这个方法生成的是具体的代理类;接着我们看下红色矩形中的英文注释,翻译过来就是:用指定的调用处理程序调用它的构造函数,简单点说就是将InvocationHandler 这个对象传入代理对象中。首先它通过类对象的getConstructor()方法获得构造器对象,然后并调用其newInstance()方法创建对象。这个方法里面的其他代码大概意思,复制代理类实现的所有接口 、获取安全管理器 、进行一些权限检验等等。但是,我们还是把注意力集中在getProxyClass0()这个方法,要分析它是如何生成的具体代理类?点进源码看看:

getProxyClass0

首先,看下这个方法的英文注释(也就是蓝色矩形)翻译过来就是:生成一个代理类。必须调用checkProxyAccess方法,在调用这个方法之前需要检查权限。下面的红色箭头是不是看到了在Android开发中熟悉的65535异常?这里也做了相应的判断;接着我们再看看红色矩形的注释,翻译过来就是:如果由给定的装载机定义的代理类实现给定的接口存在,这将会返回缓存;否则,它将通过ProxyClassFactory创建代理类。由于第一次运行加载是没有缓存的,所以我们需要进入ProxyClassFactory去了解这里如何创建代理类。

ProxyClassFactory - 1
ProxyClassFactory  - 1
ProxyClassFactory  - 2
ProxyClassFactory - 3

源码很长,所以分了几段代码截图(没办法谁叫我们是程序员,必要的耐心还是要有的)下面我们就逐个分析:

首先是ProxyClassFactory - 1,首先看下这个方法的英文注释,翻译过来就是:这是一个工厂函数它主要生成、定义和返回给定的代理类加载器和接口数组;然后看下蓝色矩形,这里面分别定义了代理类名称前缀;用原子类来生成代理类的序号, 以此来确定唯一的代理类。接着,定义了一个Map,接着遍历这个Map,遍历Map的目的是判断 intf 是否可以由指定的类加载进行加载、是否是一个接口、在数组中是否有重复。

然后是ProxyClassFactory - 2,这里面有代码主要的意思是生成代理类的包名、生成代理类的访问标志, 默认是public final(Modifier.PUBLIC | Modifier.FINAL)。蓝色矩形内的注释翻译过来就是:记录一个非公共代理接口的包,以便代理类将在同一个包中定义。验证所有非公有代理接口都在同一个包中。红色矩形里面的意思主要有判断权限、生成包名、截取包名、进行包名判断(代理类如果实现不同包的接口, 并且接口都不是public的, 那么就会在这里抛异常)、代理类生成的包路径位置,最后整合成一个代理类的最终的命名规则 = 包名 + 前缀 + 序号

最后是ProxyClassFactory - 3,黄色矩形就是上面说的命名规则(包名 + 前缀 + 序号),红色矩形的英文注释翻译过来就是:生成指定的代理类。也就是说代理类的最后生成是在这里完成的。也就是:ProxyGenerator.generateProxyClass()。由于ProxyGenerator这个类在sun.misc这个包下,这个包下的代码直接访问不了,ProxyGenerator 源码链接 这里给大家提供了外链方便查阅。

ProxyGenerator.generateProxyClass

截图中的第323行代码,这里通过调用  generateClassFile()实例方法来生成Class文件。这个方法又做了什么,继续跟进源码(这里的源码我单独截图出来,代码里面已经写好相应的注释,这样方便阅读)

源码 - 注释1
源码 - 注释2
源码 - 注释3

这里主要是写入具体的内容,部分源码省略

源码 - 注释4

总结:上面的代码截图主要是做了以下几个工作:

A:收集要生成的代理方法,将其包装成ProxyMethod对象并注册到集合中。

B:收集所有要为Class文件生成的字段信息和方法信息。

C:将B步骤的字段和方法组装成Class文件。

这里多提一嘴,我们平时编写的Java文件是以 .java 结尾的,在编写好了之后通过编译器进行编译会先生成.class文件(通过命令行可以生成class文件,命令符是: javac  java文件)然后在运行。实际上Java程序的执行只依赖于Class文件。这个Class文件描述了很多信息,当我们需要使用到一个类时,Java虚拟机就会提前去加载这个类的Class文件并进行初始化和相关的检验工作,Java虚拟机能够保证在你使用到这个类之前就会完成这些工作。

那么,我们如何还原动态代理生成的class文件,打破这最后一道壁垒?我们可以在上面测试动态代理的代码中加入下面这一段(红色区域):

还原class文件

最后生成的class文件是这样(我使用的是 jd_gui 来进行对class文件阅读)下面省略部分源码

class文件 - 1
class 文件 -2 

从这个class文件 - 1截图我们可以看到,由于Java架构起初的单继承设计机制,生成的代理类默认是继承Porxy这个类,因为单继承的特性,所以JDK动态代理只能去实现接口。class文件 - 2截图就很清楚了,这里的invoke中的m3,是通过反射调用接口中的方法(两个红色矩形),所以就解释了动态代理为什么会要重写InvocationHandler接口中的invoke方法。

动态代理模式源码总结:

1:动态代理主要的方法是Proxy.newProxyInstance,生成动态代理的类调用的是getProxyClass0这个方法,实际是在ProxyClassFactory这里进行操作

2:ProxyClassFactory这个类里面主要进行权限判断,包名的拼装等一些操作最后通过ProxyGenerator.generateProxyClass这个方法生成二进制文件

3:ProxyGenerator这个类是最底层的核心,它收集所有要生成的代理方法,将其包装成ProxyMethod对象并注册到集合、收集字段信息和方法信息将其拼装成class文件

4:通过查看生成的class文件,内部使用的是反射,重点是invoke方法。

如果这篇文章对您有开发or学习上的些许帮助,希望各位看官留下宝贵的star,谢谢。

Ps:著作权归作者所有,转载请注明作者, 商业转载请联系作者获得授权,非商业转载请注明出处(开头或结尾请添加转载出处,添加原文url地址),文章请勿滥用,也希望大家尊重笔者的劳动成果。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343

推荐阅读更多精彩内容