与其说这篇文章是讲解代理模式的,不如说它是对我正在使用的人工智能工具notion AI的功能展现。我的这篇文章,里面所有的Java代码和UML类图,都是采用notion AI生成的,原本至少一周的工作量,不到三天就写完了。里面的插图,是用文心一言生成的,虽然很丑,但是我不用再担心图片侵权了。
极客架构师——专注架构师成长。
大家好,我是码农老吴。
本期是《架构师基本功之设计模式》第20期。
自从第19期,分享了组合模式之后,后面分享的几个设计模式(适配器模式,桥接模式,装饰器模式)都没有写长文,主要原因是我这一段时间,正在探索新的知识分享形式,研究如何编写剧本,写完剧本直接录制视频了,文章一直没顾上写。代理模式是我们分享的最后一个设计模式,感觉不写长文,不足以表达对它的重视。
案例-订单服务模块
第一版代码:无设计模式
案例升级-打印方法的参数信息
第二版代码:基于静态代理模式
为什么是静态的代理模式
代理模式概念解析
什么是动态代理模式
静态代理和动态代理的比较
JDK中的动态代理
第三版代码:基于JDK的动态代理
CGLIB的动态代理
REIS分析模型解析代理模式
spring aop模块
第四版代码:基于spring aop的动态代理
下期预告
为了演示代理模式,我们先编写一个基础代码,也就是第一版代码,无设计模式版本。订单服务接口提供了两个方法,用于创建订单和取消订单。
大家注意下面代码里面的注释,为了强调这些代码都是notion自动生成的,我专门让notion在注释中添加了一句“该代码是由 Notion 自动生成的”。
本篇文章所有代码,都是notion生成的,不过都是在我和notion的反复沟通中完成的。
测试代码:
现在我们升级一下需求,为了测试方便,我们需要在用户调用订单服务的方法时,将用户输入的参数写入日志。这个功能我们就可以通过代理模式来解决,下面看第二版代码。
UML类图
notion不能直接生成UML图,需要先生成plantUML代码,然后我再把它转为图片。
图片上面的红色文字,是我后加的,不是notion生成的。
OrderServiceProxy是我们新增的类,在代理模式中,它属于代理角色,具有以下特点:
包含一个实际的订单服务对象,也就是orderService属性(被代理角色或者叫原角色)
通过构造函数注入实际的订单服务对象(不是必须的)
实现了IOrderService接口(确保代理角色与被代理角色具有相同的接口)
每个方法的实现中,都调用了实际的订单服务对象的相应方法
在每个方法的实现中,都调用了printMethodArgs方法,用于输出方法参数及其名称
要强调的是,printMethodArgs方法,它属于代理角色自己的业务功能。
静态代理模式的定义:
指在程序运行前就已经存在代理类的字节码文件,代理类和被代理类的关系在运行前就已经确定。在代理类编译时,被代理类的接口信息已经确定,以后无论调用任何被代理类对象的方法,都是通过代理类对象来间接调用。因此,静态代理模式在运行期间不会再动态地产生新的代理类,所以被称为静态代理模式。
大家看看OrderServiceProxy,是否符合静态代理模式的要求。
我们程序员手动编写的代理模式案例,基本上都是静态的,因为要从零开始实现动态代理模式,没那么容易,需要做很多准备工作,而这些准备工作,就是JDK中的代理模块,CGLIB类库和spinrg aop帮助我们完成的。
Provide a surrogate or placeholder for another object to control access to it.
—— GOF《Design Patterns: Elements of Reusable Object-Oriented Software》
为其他对象提供一种代理以控制对这个对象的访问。
——GOF《设计模式:可复用面向对象软件的基础》
代理模式的概念看起来很简单,就一句话,说明了代理模式的目的,是控制对被代理对象的访问,而恰恰是这句话,暴露了当前GOF四位前辈在定义代理模式时的历史局限性。
对访问的控制,通常我们可以理解为,对访问者权限的验证,而代理模式在实际的业务场景中,作用远远大于这个,不然的话,也不会出现下面这么多种类型的代理模式。
(注:本图是由文心一言生成,一名房产代理商业务员的照片,看起来有点吓人,双眼无神。)
本段内容由智能助手notion生成
代理模式的常见种类:
虚拟代理:在访问一个资源时,代理先把访问的权限拦下来,然后再去请求真正的资源。虚拟代理的一个常见应用场景是图片的延迟加载,即在页面渲染时,先显示一张占位图,等图片资源加载完毕再替换为真正的图片。例如,Java中的AWT中就使用了虚拟代理,来实现对图片的延迟加载。
远程代理:用于管理不同地址空间的对象,使得这些对象可以在不同的地址空间中进行通信。远程代理的一个常见应用场景是分布式服务框架,例如Dubbo,它使用了远程代理技术来实现跨进程或跨机器的服务调用。另外,Java中的RMI也是一种远程代理。
安全代理:用于控制对一个对象的访问权限。安全代理的一个常见应用场景是控制对一个文件的访问权限,即只有拥有特定权限的用户才能访问该文件。例如,Java中的安全管理器就使用了安全代理。
智能代理:在代理中加入了一些额外的逻辑,比如对被代理对象的使用进行计数、记录日志等。智能代理的一个常见应用场景是数据库连接池,即在代理中加入连接池的逻辑,控制连接的数量和使用次数。另外,Spring AOP中的切面就是一种智能代理。
上面这些代码模式的分类,基本上都是根据应用的场景来分类的,而从技术角度,将代理模式分为静态代理和动态代理,才是我们学习的重点,静态代理前面我们已经说过了,下面我们看动态代理及两者的区别。
动态代理是指在程序运行时动态生成代理类的代码。与静态代理相比,动态代理更加灵活,可以代理任意类型的对象,不需要为每一个被代理的类都创建一个代理类。
静态代理和动态代理都可以实现对目标对象的访问控制,但两者的实现方式有所不同。下表列出了静态代理和动态代理在不同方面的支持情况。
总之,动态代理相比静态代理更加灵活和可扩展,并且能够避免代码冗余和维护难度增加的问题。因此,在实际的开发中,动态代理更加常用。
JDK中对动态代理的支持,主要是通过Proxy和InvocationHandler来提供支持的。
java.lang.reflect.Proxy
是Java中用于生成动态代理类的类。使用它可以在运行时动态创建实现特定接口的类,是实现动态代理的关键类。
java.lang.reflect.InvocationHandler
是一个接口,定义了一个方法invoke,该方法负责处理代理类的方法调用。在实现动态代理时,需要编写一个实现了该接口的类,该类负责对代理类的方法调用进行处理。
下面我们基于它们,来升级我们的代码。
从动态代理模式的角度,OrderServiceDynamicProxy具有以下特点:
实现了JDK的InvocationHandler接口
在invoke方法中,通过Java的反射机制将目标对象的方法调用转发到自己的invoke方法中,在调用目标方法之前和之后,增加了自己的业务逻辑(打印方法入参)
getProxy方法是获取代理对象的方法,使用Java的Proxy类创建了一个代理对象,这个代理对象实现了IOrderService接口,调用代理对象的方法时,实际上是调用了OrderServiceDynamicProxy的invoke方法
注意,代理角色也有自己的业务逻辑代码,在这里就是printMethodArgs方法。
在Java的软件生态中,对于一个比较热门的,基础功能的支持,JDK往往是最早的,但是往往也是最简陋的,要体验优质的服务,还需要看业内第三方框架或者类库,而对于代理模式的支持,也有不少框架,比如spring aop,Javassist,cglib,AspectJ等等。
CGLIB是一个强大的、高性能的Code Generation Library,它可以在运行时动态生成字节码,从而实现对Java类的动态代理。
总的来说,CGLIB动态代理相比JDK动态代理,性能更高,支持更多的代理类型,但需要注意的是,由于CGLIB是通过生成目标对象的子类来实现代理,所以如果目标对象被final修饰,那么就无法生成代理类了。
在实际项目中直接使用CGLIB比较少见,我们就不提供代码了。我们先总结一下代理模式,然后看spring 的aop模块实现的代码。
REIS分析模型是我总结的分析设计模式的一种方法论,主要包括场景、角色、交互、效果四个要素。
在什么情况下,遇到了什么问题,需要使用某个设计模式。
当出现以下情况时,可能需要使用代理模式:
在访问一个对象时,需要对访问进行控制。
在访问一个对象时,需要增强其功能。
角色,一般为设计模式出现的类,或者对象。每种角色有自己的职责。
在代理模式中,通常包含三种角色,原角色(Original role),代理角色(Proxy role),客户方角色(client role)。 原角色(Original role)
在GOF原始的代理模式中,原角色被称为Subject角色,在我的设计模式知识体系,统一约定为原角色,在代理模式的语境中,原角色就是被代理的角色,提供原始的业务逻辑,与我前面刚刚分享的装饰器模式中的原角色,是同一个道理。
原角色里面通常包含接口和具体类,在GOF原始的代理模式中,把原角色的接口和具体类,定义为了不同的角色,这是我不赞同的地方。我认为他们应该属于同一个角色,只不过有的是接口,有的是类而已。
代理角色(Proxy role)
既然是代理模式,代理角色当然是代理模式的核心,对于代理角色的,认为它至少有两个重要职责。一个是自己的业务逻辑,代理角色通常是用来增强被代理角色的功能,或者限制对被代理角色的访问,这些都属于它自身的业务逻辑。
另一个,就是管理它与被代理角色的关系,代理角色最终还是要调用被代理类的,所以如何建立它与被代理角色的关系,非常重要。
在静态代理模式中,我们在代理类中,把被代理对象定义为它的属性,而在动态代理中,这种关系更为复杂,需要JDK或者spring aop模块帮助我们完成。
所以,我认为代理角色,根据它的功能,可以分为管理型代理对象和业务型代理对象。
管理型代理对象:通常由框架提供,我们不用操心。
业务型代理对象:用来实现代理对象自身的业务,这个我们程序员需要关心,框架无法给我们提供。
客户方角色(client role)
这个不用多说,每个设计模式都有,对于客户方角色,需要关注的就是,它是和谁交互的,很显然,在代理模式中,它是和代理角色交换的。
交互,是指设计模式中,各种角色是如何交互的,一般用UML中的序列图,活动图来表示。简单的说就是角色之间是如何配合,完成设计模式的使命的。
在代理模式中,交互还是比较简单的,客户方角色调用代理方角色,代理方角色调用原角色。
使用该设计模式之后,达到了什么效果,有何意义,当然,也可以说说它的缺点,或者风险。 从我们前面的案例可以看出,代理模式达到了以下效果:
在不改变真实角色的情况下,增强其功能或控制其访问。
能够让客户方使用代理角色访问真实角色,从而简化了客户方的操作。
提到spring框架,大家往往能想到的,是两个比较重要的模式,IOC和AOP,而AOP的背后,则是代理模式。
我们的重点是代理模式,所以关于AOP相关的内容,这里就不展开讲解了。如果你对代理模式能有一个很好的理解,在学习aop复杂多样的各种概念时,将会非常简单。
我们的第四版代码,就是基于spring aop实现的,里面使用的是前置通知。
测试代码:
运行结果
下期,我将开始分享各种设计模式的评比以及知名开源软件中的设计模式。
极客架构师——专注架构师成长。
我们下期见。