引言
在软件设计领域,代理模式(Proxy Pattern)作为一种经典的结构型设计模式,其核心思想是在客户端与目标对象之间引入一个代理对象,以控制对目标对象的访问。这种模式不仅能够在不修改原有代码的基础上增强功能,还能有效解耦客户端与目标对象,提升系统的灵活性和可维护性。本文将从代理模式的基本概念出发,深入探讨其应用场景、不同类型(静态代理与动态代理)的实现方式,并通过具体的Java代码案例进行详细解析,最后总结其优缺点以及与装饰器模式的区别,旨在帮助读者全面理解并掌握代理模式的设计精髓与实践技巧。
代理模式UML类图(静态代理)
为了更好地理解静态代理的结构,下面通过Mermaid语法绘制了代理模式的UML类图,展示了客户端、代理类、抽象接口和真实类之间的关系:
classDiagram class Client { +main() } class Subject { <<interface>> +request() } class RealSubject { +request() } class Proxy { -realSubject: RealSubject +request() +preRequest() +postRequest() } Client --> Subject Subject <|.. RealSubject Subject <|.. Proxy Proxy --> RealSubject note for Proxy "代理类持有真实对象的引用,\n并在调用真实对象方法前后\n执行额外操作(如权限检查、\n日志记录、延迟初始化等)" note for Client "客户端通过代理对象访问服务,\n无需直接接触真实对象"类图说明:
- Client(客户端):程序的调用者,通过代理对象访问服务。
- Subject(抽象接口):定义代理类和真实类共同实现的接口,确保两者具有相同的行为。
- RealSubject(真实类):实际执行业务逻辑的对象,实现Subject接口。
- Proxy(代理类):实现Subject接口,持有RealSubject的引用,在调用真实对象方法前后可以执行额外的操作(如权限验证、日志记录、缓存等)。
在静态代理中,代理关系在编译期就已经确定,代理类和真实类都实现相同的接口,代理类通过组合方式持有真实对象的引用,并在调用真实对象方法前后添加额外的控制逻辑。
介绍
代理模式中,在客户端与对象之间增加了一个代理层。客户端在进行访问时候,不是直接访问对象,而是访问代理。代理模式是一种结构型的设计模式。通过代理模式,可以解决直接访问对象带来的一些问题,并且可以进行访问控制。
代理模式的实现中,代理类中依赖实体类,但两者共同实现相同的接口。代理类中对应接口中,会调用实体类的对应接口。听上去与装饰器的实现一样,是的,两者的实现基本上是相同的,只是使用意义上的侧重点不同。两者的区别,会在文末的总结中进行分析。
一、代理模式的应用场景
代理模式的应用场景比较广泛,跟"代理"两个字沾边的,一般都会涉及代理模式。比如:
- 火车票代售站:对于乘客来说,他不需要去真正的火车站,而通过代售点,就可以实现买票取票的功能。
- VPN:由于众所周知的原因,我们不能访问youtube,那么我们可以设置一个VPN,通过VPN进行访问。
- Windows的快捷方式:也是一种代理。
- Spring的AOP:面向切面编程中大量使用了代理模式。
除了上述生活化的例子,代理模式在软件开发中还有多种技术实现,以下是几个典型的技术应用场景:
- 远程代理(RPC框架):在分布式系统中,客户端需要调用远程服务时,远程代理充当本地代表。客户端调用本地代理对象的方法,代理负责网络通信、序列化/反序列化等细节,将请求转发给远程服务,并将结果返回给客户端。这样客户端无需关心网络通信的复杂性,就像调用本地方法一样简单。
- 虚拟代理(图片懒加载):在Web开发中,当页面包含大量图片时,虚拟代理可以延迟图片的实际加载。代理对象先显示占位图或加载动画,当图片进入可视区域时,代理才真正加载原始图片资源。这能显著提升页面初始加载速度,优化用户体验。
- 保护代理(权限控制):在需要访问控制的系统中,保护代理在客户端访问目标对象前进行权限验证。例如,用户管理系统中的代理可以检查用户角色和权限,只有具备相应权限的用户才能执行敏感操作,否则拒绝访问或返回错误信息。
- 缓存代理:为昂贵的操作(如数据库查询、复杂计算)提供缓存功能。代理首先检查缓存中是否存在结果,如果存在则直接返回,否则调用真实对象执行操作并将结果缓存起来,后续相同请求可以直接使用缓存,提高系统性能。
二、代理模式的类型
代理模式从类型上来说分为两种:
- 静态代理:在编译期间就确定的代理关系,一般是一对一的。
- 动态代理:一对多的代理关系,是在运行时态才能确定的代理关系。
动态代理的原理比较复杂,本篇文章只简单介绍动态代理的使用,在下一篇文章中,会从原理和代码的层面,对动态代理做专门的解读。
三、静态代理案例
3.1 背景
以购买火车票为例。乘客(客户端)在购买火车票的时候,可以在代售点(代理类)进行购买(功能),而不需要花费很长的时间跑去火车站(真实类)进行购买(功能)。
3.2 实现
3.2.1 定义抽象接口
public interface Shop { void buy(); void take(); }3.2.2 定义真实火车站
public class Station implements Shop { @Override public void buy() { System.out.println("购买火车票"); } @Override public void take() { System.out.println("领取火车票"); } }3.2.3 定义代售点(代理类)
public class ProxyStation implements Shop { private Station station = new Station(); @Override public void buy() { beforeBuy(); station.buy(); afterBuy(); } private void beforeBuy() { System.out.println("购买之前操作"); } private void afterBuy() { System.out.println("购买之后的操作"); } @Override public void take() { beforeTake(); station.take(); afterTake(); } private void afterTake() { System.out.println("取票之前的操作"); } private void beforeTake() { System.out.println("取票之后的操作"); } }3.2.4 验证程序
public class Test { public static void main(String args[]) { ProxyStation proxyStation = new ProxyStation(); proxyStation.buy(); proxyStation.take(); } }3.2.5 运行结果
购买之前操作
购买火车票
购买之后的操作
取票之后的操作
领取火车票
取票之前的操作Process finished with exit code 0
四、动态代理
动态代理的场景是一对多的关系,即是一个代理类,多个被代理的类。
在动态代理中,不再关注向客户端屏蔽原始对象(客户端可以看到原始对象),而重点是对于一系列的原始实现类,能够对抽象中的方法进行统一的横向扩展(方法执行前/后的操作),而不需要为每一个原始实现类都创建一个代理。
动态代理的原理,会在下篇文章中进行分析。
4.1 案例
以汽车(抽象)行驶为例子,我们要统计多种车型(原始实现类)通过某段距离的用时。在汽车行驶前,需要进行开始计时的操作(功能扩展)。汽车行驶后,要进行结束计时的操作(功能扩展)。
4.2 实现
4.2.1 抽象接口
public interface Moveable { void move() throws Exception; void move_back() throws Exception; }4.2.2 定义实现类
import java.util.Random; public class Car implements Moveable { public void move() throws Exception { Thread.sleep(new Random().nextInt(1000)); System.out.println("轿车行驶中…"); } public void move_back() throws Exception { Thread.sleep(new Random().nextInt(1000)); System.out.println("轿车向后行驶中…"); } }import java.util.Random; public class Truck implements Moveable { public void move() throws Exception { Thread.sleep(new Random().nextInt(1000)); System.out.println("卡车行驶中…"); } public void move_back() throws Exception { Thread.sleep(new Random().nextInt(1000)); System.out.println("卡车向后行驶中…"); } }4.2.3 定义动态代理工具类
动态代理工具类需要实现InvocationHandler接口,实现invoke方法,通过反射实现对抽象接口方法的调用method.invoke(target, args);。
import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; public class TimeHandler implements InvocationHandler { private Object target; public TimeHandler(Object target) { super(); this.target = target; } public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { long startTime = System.currentTimeMillis(); System.out.println("汽车开始行驶…"); method.invoke(target, args); long stopTime = System.currentTimeMillis(); System.out.println("汽车结束行驶…汽车行驶时间:" + (stopTime - startTime) + "毫秒!"); return null; } }4.2.4 验证程序
import java.lang.reflect.InvocationHandler; import java.lang.reflect.Proxy; public class Test { public static void main(String[] args) throws Exception { Car car = new Car(); Class cls = car.getClass(); InvocationHandler h = new TimeHandler(car); Moveable m = (Moveable) Proxy.newProxyInstance(cls.getClassLoader(), cls.getInterfaces(), h); m.move(); System.out.println(""); m.move_back(); System.out.println(""); System.out.println(""); Truck truck = new Truck(); cls = truck.getClass(); h = new TimeHandler(truck); m = (Moveable) Proxy.newProxyInstance(cls.getClassLoader(), cls.getInterfaces(), h); m.move(); System.out.println(""); m.move_back(); System.out.println(""); System.out.println(""); } }4.2.5 运行结果
汽车开始行驶…
轿车行驶中…
汽车结束行驶…汽车行驶时间:200毫秒!汽车开始行驶…
轿车向后行驶中…
汽车结束行驶…汽车行驶时间:681毫秒!汽车开始行驶…
卡车行驶中…
汽车结束行驶…汽车行驶时间:492毫秒!汽车开始行驶…
卡车向后行驶中…
汽车结束行驶…汽车行驶时间:338毫秒!Process finished with exit code 0
五、总结
5.1 代理模式优缺点
| 代理类型 | 优点 | 缺点 |
|---|---|---|
| 静态代理 |
|
|
| 动态代理 |
|
|
5.2 代理模式与装饰器模式的区别
- 扩展维度不同:代理模式强调对于访问的控制,在调用实体类的方法之前或者之后,会添加一些操作,是扩展的广度。装饰器模式强调对于方法的加强,扩展的深度。打个比方,张无忌学习乾坤大挪移。装饰器模式,则增加其深度,从第3层境地学习到第4层境地,注重功能的加强。代理模式则是让其在学习乾坤大挪移之前,先学习一下九阳神功,两种武功能够有所联系。从外界看来,都是武功的增强,但是一个是深度,一个是广度。
- 可见性不同:代理模式中,在客户看来,无法感知到原始对象,只能接触代理对象。装饰器模式中,客户看来,是能够感知到原始对象的。