写点什么

设计模式之代理模式:武器附魔之道

  • 2024-12-31
    北京
  • 本文字数:8713 字

    阅读完需:约 29 分钟

作者:京东保险 孙昊宇


大家好,今天我们聊聊设计模式中的代理模式。作为一种经典设计模式,它的应用极为广泛。不论你是刚刚入门,还是已经熟悉设计模式,相信这篇文章都会让你有所收获。

一、引子:叫个代驾

让我们从一个引子开始:司机和代驾。「私家车司机」和「代驾」是什么关系?很简单,「私家车司机」是客户,「代驾」负责提供服务,帮他们开车。


不同点: 「私家车司机」有自己的车,他们可能自己开车,也可能找代驾开车;


「代驾」没有自己的车,但他们会接到代驾订单,从而开「私家车司机」的车。


相同点: 他们都会开车,有驾照。换句话说,他们都属于「司机」。


说到这里,我想他们的关系就很清楚了:「司机」是「私家车司机」和「代驾」的父类。虽然都会开车,但他们对“开车”这个行为有不同的实现。


让我们把这三个类的关系表示出来。首先定义一个抽象类「司机」,就叫 Driver 好了:


@Datapublic abstract class Driver {
String name;
abstract void driveCar();}
复制代码


我们要求每个司机都有一个名字,且都必须会开车。


接下来看看「私家车司机」,CommonDriver 类:


@Datapublic class CommonDriver extends Driver {
CommonDriver(String name) { this.name = name; }
@Override public void driveCar() { System.out.println(this.getName() + "的汽车正在行驶..."); }}
复制代码


也很简单,每次开车的时候打印一行日志即可。最后看看「代驾」,就叫 ProxyDriver 吧:


@Datapublic class ProxyDriver extends Driver {
private Driver realDriver;
ProxyDriver(String name, Driver realDriver) { this.name = name; this.realDriver = realDriver; }
@Override void driveCar() { System.out.printf("代驾「%s」正在为%s服务...\n", this.getName(), this.getRealDriver().getName()); this.realDriver.driveCar(); }}
复制代码


我们要求每个代驾都要有一个服务的客户,也就是被代理的司机。我们将这位被代理的司机——realDriver 作为了代驾类的私有变量存起来。当代驾在开车时,他实际上开的是客户的车。因此,他直接去调用 realDriver 的开车方法即可。


三个类定义好了,让我们先创建一个「私家车司机」——小张,让小张自己开车;再帮他叫一个「代驾」——就叫他小代吧,让小代帮他开车:


public class Main {    public static void main(String[] args) {        CommonDriver zhang = new CommonDriver("小张");        zhang.driveCar();        ProxyDriver proxyDriver = new ProxyDriver("小代", zhang);        proxyDriver.driveCar();    }}
复制代码


运行一下:


小张的汽车正在行驶... 代驾小代正在为小张服务... 小张的汽车正在行驶...


结果符合预期:不管是谁在开车,结果都是一样的,开的都是小张的汽车。

二、代理模式:武器附魔之道

代理模式的定义

以上例子展示了一个代理模式的基本实现。代理模式(Proxy Pattern) 的定义是:使用代理以代替对真实对象的访问。 它属于一种结构型设计模式。


例子中的「司机」、「私家车司机」和「代驾」三个角色,分别对应了代理模式中的三个基本元素:


「私家车司机」——真实主题: 被代理的角色,是业务逻辑的具体执行者。


「代驾」——代理主题: 负责代理真实主题,所有对其业务方法的调用,都会被委托给其真实主题实现。


「司机」——抽象主题: 可以是接口,也可以是抽象类。代理主题和真实主题都会去实现/继承同一个抽象主题。


下面是代理模式的类图:


优点及应用

为真实的对象设置一个代理,可以带来什么好处?在哪些应用场景下,我们需要用到代理模式?


要回答这个问题,我们不妨想想代理的特点:间接访问。没错,代理模式的优点就在于通过代理间接访问真实对象。通过间接访问,我们就可以让代理做许多中间操作,通过这些中间操作,我们就可以在不修改真实对象的前提下,实现功能增强。


我的理解:如果把真实主题比做一把**「宝剑」,专门用来处理核心逻辑**,那么就可以将它的代理比作**「附魔」,用来给真实主题提供一些强化功能**,附魔的种类就很多啦:火焰🔥、冰霜❄️、毒素🧪、雷电⚡️等等...能造成各种属性伤害,它们的共同点都是增强这把宝剑,但不会侵入式地去修改宝剑本身。



那么一般可以选择哪些“附魔”呢?一些常见的应用场景:


(1)「日志代理」: 让代理帮忙记录方法出入参、调用记录等等日志;


(2)「保护代理」: 让代理帮忙做权限控制,拦截异常访问,保护真实对象;


(3)「缓存代理」: 让代理帮忙缓存真实对象的调用结果,从而减少对真实对象的调用量;


(4)「虚拟代理」: 延迟真实对象的初始化直到真正需要时,从而提高应用的启动速度和运行效率,多用于创建一个对象需要消耗大量资源时,也被称作“懒加载”。


说到这里,我想大家就可以感到代理模式的强大了,代理模式的优点:


(1)职责清晰: 前面提到,作为真实主题的一种“增强”,代理与真实主题的职责划分十分清晰,这有利于维持真实主题的简洁,让真实主题专注于处理核心逻辑。


(2)高扩展性: 正因为职责清晰,代理与真实主题是松耦合的,对任何一方的修改都不会对另一方造成影响,适合业务逻辑需要经常扩展的场景。

三、强制代理

让我们回到代驾的例子,话题又回到小张身上:这天小张喝醉了。“道路千万条,安全第一条”,看来今天这车必须要让代驾开了。如何在方法中限制一下,规定只允许代驾开车呢?这就需要用到强制代理(Forced Proxy)。


强制代理的定义是:对真实对象的访问,必须通过特定的代理对象进行。这句话包含了两层含义:1. 不允许对真实对象的直接访问。 2.必须通过特定的代理访问真实对象。

《设计模式之禅》勘误

如何实现强制代理?简单来说,在真实对象方法的调用中,增加对“是否使用了代理访问”的判断即可。在设计模式经典著作《设计模式之禅》中,作者给出了一种实现方式(但我认为存在一些问题),让我们先看看用他的方式如何实现吧~


只需要对真实主题进行修改。在 CommonDriver 类,我们存储一个 proxyDriver 变量——用于记录这个司机叫的代驾,就如同代驾记录他代理的司机一样。接下来写一个 “叫代驾” 的方法——callProxy,用来给司机生成一个代驾对象。最后在实际开车的逻辑中,判断自己是否存在代驾对象。如果不存在,则认为司机没有叫代驾。具体实现如下:


@Datapublic class CommonDriver extends Driver {
private ProxyDriver proxyDriver = null;
CommonDriver(String name) { this.name = name; }
public ProxyDriver callProxy(String proxyName) { System.out.printf("%s叫了个代驾:%s\n", this.getName(), proxyName); this.proxyDriver = new ProxyDriver(proxyName, this); return this.proxyDriver; }
@Override void driveCar() { if (this.isProxy()) { System.out.println(this.getName() + "的汽车正在行驶..."); } else { System.out.println("酒后不开车,请叫代驾!"); } }
/** * 校验是否是代理访问 */ private boolean isProxy() { return this.proxyDriver != null; }}
复制代码


让我们试一试:先让小张开车,再给他叫一个代驾小代,让小代帮他开车:


public class Main {    public static void main(String[] args) {        CommonDriver zhang = new CommonDriver("小张");        zhang.driveCar();        ProxyDriver proxy = zhang.callProxy("小代");        proxy.driveCar();    }}
复制代码


运行一下,符合预期,即满足了“不允许对真实对象的直接访问”:


酒后不开车,请叫代驾! 小张叫了个代驾:小代 代驾小代正在为小张服务... 小张的汽车正在行驶...


强制代理的第二个要求:必须通过特定的代理访问真实对象。这次我们自己 new 一个假冒的代驾,让他去给小张做代驾:


public class Main {    public static void main(String[] args) {        CommonDriver zhang = new CommonDriver("小张");        ProxyDriver proxy = new ProxyDriver("假冒的代驾", zhang);        proxy.driveCar();    }}
复制代码


运行结果也符合预期:


代驾「假冒的代驾」正在为小张服务... 酒后不开车,请叫代驾!


上述实现,看似符合强制代理的要求,但真的如此吗?眼尖的读者应该已经发现不对劲了。略微修改上面的用例,就可以证明这种实现存在缺陷:


public class Main {    public static void main(String[] args) {        CommonDriver zhang = new CommonDriver("小张");        ProxyDriver proxy1 = zhang.callProxy("小代");        ProxyDriver proxy2 = new ProxyDriver("假冒的代驾", zhang);        proxy2.driveCar();    }}
复制代码


运行如下:


小张叫了个代驾:小代 代驾假冒的代驾正在为小张服务... 小张的汽车正在行驶...


如上,给小张先叫一个代驾小代,但我们又自己 new 了一个假冒的代驾,让他去给小张开车。结果车真的被假代驾开走了!这说明该实现并没有满足 “必须通过特定的代理访问真实对象” 的要求。


接下来,我们再试着让小张叫一个代驾,结果小张等不及代驾来,自己先上车了:


public class Main {    public static void main(String[] args) {        CommonDriver zhang = new CommonDriver("小张");        ProxyDriver proxy = zhang.callProxy("小代");        zhang.driveCar();    }}
复制代码


运行如下:


小张叫了个代驾:小代 小张的汽车正在行驶...


可见,这种实现也无法满足 “不允许对真实对象的直接访问” 的要求。因此,这种强制代理的实现是有严重缺陷的。究其根本原因,就在于在上述实现中,仅通过该真实对象是否拥有代理对象来判断是否可以访问,而并未检查实际调用者的身份。因此只要事先通过真实对象创建了代理对象,以后就可以任意调用了。

改进后的实现

****明确了问题所在,修改起来就容易了:只需在真实对象执行前,判断调用者是否是指定的调用者即可。同时,我们需要在调用方法时传入调用者。实现如下:


Driver:


@Datapublic abstract class Driver {
String name; abstract void driveCar(Driver driver);}
复制代码


CommonDriver:


@Datapublic class CommonDriver extends Driver {
private ProxyDriver proxyDriver = null;
CommonDriver(String name) { this.name = name; }
public ProxyDriver callProxy(String proxyName) { System.out.printf("%s叫了个代驾:%s\n", this.getName(), proxyName); this.proxyDriver = new ProxyDriver(proxyName, this); return this.proxyDriver; }
@Override void driveCar(Driver driver) { if (this.isProxy(driver)) { System.out.println(this.getName() + "的汽车正在行驶..."); } else { System.out.println("酒后不开车,请叫代驾!"); } }
/** * 校验是否是代理访问 */ private boolean isProxy(Driver driver) { return this.proxyDriver == driver; }}
复制代码


ProxyDriver:


@Datapublic class ProxyDriver extends Driver {
private Driver realDriver;
ProxyDriver(String name, Driver realDriver) { this.name = name; this.realDriver = realDriver; }
@Override void driveCar(Driver driver) { System.out.printf("代驾「%s」正在为%s服务...\n", this.getName(), this.getRealDriver().getName()); this.realDriver.driveCar(this); }}
复制代码


运行刚才的用例,这次不论是假冒的代驾还是小张自己,都无法正常访问了:


public class Main {    public static void main(String[] args) {        CommonDriver zhang = new CommonDriver("小张");        ProxyDriver proxy1 = zhang.callProxy("小代");        ProxyDriver proxy2 = new ProxyDriver("假冒的代驾", zhang);        zhang.driveCar(zhang);        proxy2.driveCar(proxy1);        proxy1.driveCar(proxy1);    }}
复制代码


小张叫了个代驾:小代 酒后不开车,请叫代驾! 代驾「假冒的代驾」正在为小张服务... 酒后不开车,请叫代驾! 代驾「小代」正在为小张服务... 小张的汽车正在行驶...

四、动态代理

静态代理和动态代理

上面介绍的代理模式,全部属于“静态代理”。与之相对的还有“动态代理”。相比于静态代理,动态代理有更广泛的应用。二者区别如下:


静态代理:代理类和被代理类的关系在编译时就已经固定。 其逻辑简单直观,易于理解和实现,但缺乏灵活性,适合简单的代理逻辑场景。 动态代理:代理对象在程序运行时被动态生成,通常依赖反射机制实现。 其灵活性高、可扩展性强,虽然相比静态代理增加些微性能开销,但完全可以接受。

JDK 动态代理

让我们看看如何使用 JDK 方式实现动态代理。举一个最简单的例子:有一个 HelloWorld 接口,其实现类 HelloWorldImpl 实现了其 helloWorld()方法,并返回“Hello world!”。


public interface HelloWorld {    String helloWorld();}public class HelloWorldImpl implements HelloWorld {    @Override    public String helloWorld() {        return "Hello world!";    }}
复制代码


如果是静态代理,我们会再定义一个代理类(也许会叫 ProxyHelloWorld),再让它去实现 HelloWorld,对吧?而在动态代理中,我们需要创建一个通用的动态代理类,该类需要实现 java.lang.reflect.InvocationHandler 接口,并重写 invoke()方法——该方法负责处理所有通过代理对真实主题的访问。同时,类似于静态代理,我们会在这个动态代理类中保存一个私有变量 target,记录被代理的真实主题。代码如下:


public class DynamicProxy implements InvocationHandler {
private Object target;
public DynamicProxy(Object target) { this.target = target; }
@Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.printf("进入动态代理,调用方法:%s开始,入参:%s\n", method.getName(), JSON.toJSONString(args)); Object result = method.invoke(target, args); System.out.printf("进入动态代理,调用方法:%s结束,出参:%s\n", method.getName(), JSON.toJSONString(result)); return result; }}
复制代码


如上所示,我们同时在 invoke 方法中记录方法的出入参,这样就实现了一个日志代理的功能。


写好了动态代理类,我们就可以这个类就可以动态代理任何接口。让我们代理一下 HelloWorld 接口吧~实现如下:


public class Main {    public static void main(String[] args) {        HelloWorld helloWorld = new HelloWorldImpl();        System.out.println(helloWorld.helloWorld());
HelloWorld helloWorldProxy = (HelloWorld) Proxy.newProxyInstance(HelloWorldImpl.class.getClassLoader(), new Class[]{HelloWorld.class}, new DynamicProxy(helloWorld));
System.out.println(helloWorldProxy.helloWorld()); }}
复制代码


我们先创建了一个真实主题 helloworld,并直接访问了其方法;接下来为其使用的方法是 java.lang.reflect.Proxy#newProxyInstance(),它可以生成一个动态代理对象,其中三个参数分别为:


(1)被代理的真实主题的类加载器;


(2)需要代理的接口类列表;


(3)动态代理类,即我们刚才定义的,实现了 java.lang.reflect.InvocationHandler 接口的类。


该方法默认返回 Object 类型的对象,我们将其显式转换为 HelloWorld 类型,就可以访问其方法啦。运行一下:


Hello world! 进入动态代理,调用方法:helloWorld 开始,入参:null 进入动态代理,调用方法:helloWorld 结束,出参:"Hello world!" Hello world!

CGlib 动态代理

CGlib(Code Generation Library)是一个基于字节码生成的 Java 库。相比于使用反射的 JDK 动态代理,CGlib 通过操作字节码实现了更为高效的动态代理。类似于 JDK 动态代理需要创建一个实现了 InvocationHandler 的代理类,CGlib 动态代理同样需要创建一个实现了 net.sf.cglib.proxy.MethodInterceptor 接口的代理类,并重写 intercept()方法以控制所有通过代理对真实对象的访问,同时记录被代理的真实主题。代码如下:


public class DynamicProxyCglib implements MethodInterceptor {
private Object target; public DynamicProxyCglib(Object target) { this.target = target; }
@Override public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable { System.out.printf("进入CGlib动态代理,调用方法:%s开始,入参:%s\n", method.getName(), JSON.toJSONString(args)); Object result = methodProxy.invoke(target, args); System.out.printf("进入CGlib动态代理,调用方法:%s结束,出参:%s\n", method.getName(), JSON.toJSONString(result)); return result; }}
复制代码


别忘了引入 CGlib 依赖:


<dependency>    <groupId>cglib</groupId>    <artifactId>cglib</artifactId>    <version>3.3.0</version></dependency>
复制代码


CGlib 的动态代理不是基于接口的,而是基于类的,这意味着我们可以跳过接口直接代理实现类,甚至是没有实现接口的类。这是因为 CGlib 的动态代理是基于类的继承实现的——其代理类是被代理类的子类,它会覆盖被代理类的所有非 final 方法,并插入拦截和回调机制。举个例子,让我们直接创建一个不实现任何接口的类 Hello:


public class Hello {    public String hello(String name) {        return "Hello, " + name;    }}
复制代码


用 CGlib 对 Hello 类进行代理:


public static void main(String[] args) {    Hello hello = new Hello();    System.out.println(hello.hello("world"));
Enhancer helloEnhancer = new Enhancer(); helloEnhancer.setSuperclass(Hello.class); helloEnhancer.setCallback(new DynamicProxyCglib(hello));
Hello helloProxy = (Hello) helloEnhancer.create(); System.out.println(helloProxy.hello("world"));}
复制代码


在以上代码中,我们使用了 net.sf.cglib.proxy.Enhancer 类,这是 CGlib 用于生成动态代理类的核心工具类。我们创建了一个 Enhancer 实例,并使用 setSuperclass()方法指定了被代理的类(即代理类的父类)、使用 setCallback()方法指定拦截回调对象(即实现了 MethodInterceptor 的实例)。


CGlib 同样支持接口维度的动态代理,当代理接口时,使用 Enhancer.setInterfaces()方法,传入一个或多个接口的类对象数组,以指定被代理接口。


这样,当我们调用其 create()方法时,就可以创建一个代理对象,将其转换为被代理的类型,就可以访问其方法啦。以上代码的运行结果:


Hello, world 进入 CGlib 动态代理,调用方法:hello 开始,入参:["world"] 进入 CGlib 动态代理,调用方法:hello 结束,出参:"Hello, world" Hello, world

动态代理与 AOP

面向切面编程 面向切面编程(Aspect-Oriented Programming,AOP),是一种基于「横切关注点」的编程范式。作为传统的面向对象编程(OOP)的一种补充,其注重于解决在 OOP 过程中出现的跨越多个类或对象的「横切关注点」问题。 「横切关注点」是指多个模块或类中都存在的、但是又不属于任何一个单独模块或类的特定功能或行为,如日志记录、权限控制等非功能性需求。这些功能往往跨越多个模块和类,很容易导致代码重复,难以维护。在传统的 OOP 中,横切关注点往往不易模块化。而在 AOP 中,横切关注点被单独抽离出来,并封装为切面(Aspect),将需要执行切面的地方称为切入点(Pointcut),并通过织入(Weaving)将切面逻辑和目标代码结合起来。这种对横切关注点的模块化有利于减少代码重复,提升可读性、可复用性和可扩展性。常见的 AOP 框架有 Spring AOP、AspectJ 等。


简要介绍完了 AOP,不难发现:AOP 与动态代理似乎非常接近。实际上,动态代理和 AOP 确实有着紧密的联系:


(1)动态代理和 AOP 要解决的问题相似:


在 AOP 中力求解决的「横切关注点」问题实际上也是代理模式,尤其是动态代理能够解决的问题——都是通过将重复的非功能性逻辑进行抽离封装以实现软件架构的优化


(2)动态代理是 AOP 的一种实现技术:


在 Spring AOP 框架中,通过动态代理模式实现切面的织入。


当切入点所在的对象(简称目标对象)实现了一个/多个接口时,Spring AOP 将使用 JDK 动态代理,为其创建一个实现了相同接口的类作为代理类;


当目标对象没有实现接口时,Spring AOP 将使用 CGlib 动态代理,为其创建一个重写其方法的子类作为代理类。


需要明确的是:虽然二者联系紧密,看似十分接近,但不能简单地认为 AOP 是升级版的动态代理,二者并非等价的概念:


(1)动态代理本质是一种设计模式,方便我们通过代理对象实现拦截调用,其核心功能是代理,同时适合通过代理解决横切关注点问题;


(2)AOP 本质是一种编程范式,核心在分离和模块化横切关注点。动态代理可以实现 AOP,但 AOP 并不局限于用动态代理实现,如 AspectJ 框架也使用了静态织入(编译期)方式实现 AOP。




以上就是关于代理模式的全部介绍啦,作为一种非常流行且强大的设计模式,结合实际场景活学活用,相信它一定可以在您的开发之路上有用武之地。希望这篇文章可以对大家有所帮助,也欢迎大家交流、指正!

发布于: 2024-12-31阅读数: 2
用户头像

拥抱技术,与开发者携手创造未来! 2018-11-20 加入

我们将持续为人工智能、大数据、云计算、物联网等相关领域的开发者,提供技术干货、行业技术内容、技术落地实践等文章内容。京东云开发者社区官方网站【https://developer.jdcloud.com/】,欢迎大家来玩

评论

发布
暂无评论
设计模式之代理模式:武器附魔之道_京东科技开发者_InfoQ写作社区