代理模式

设计模式笔记:代理模式 (Proxy Pattern)

一、一句话概括

为另一个对象提供一个替身或占位符,以控制对这个真实对象的访问。这个替身(代理)可以在将请求转发给真实对象的前后,执行一些附加操作。

二、为什么需要它 (Why - The Pain Point)

在很多情况下,我们不希望或不能直接访问一个对象。原因可能有很多:

  1. 访问控制:我们希望根据用户的权限,决定他是否能调用对象的某些方法。例如,普通用户只能读取数据,而管理员可以修改数据。
  2. 延迟加载(懒加载):一个对象的创建成本非常高昂(比如,加载一张高清大图或初始化一个复杂的数据库连接)。我们希望只有在真正需要它的时候才去创建,而不是在一开始就加载。
  3. 远程访问:真实对象位于远程服务器上,客户端需要通过网络来调用它的方法。直接进行网络通信非常复杂。
  4. 日志记录/性能监控:我们想在不修改原始代码的情况下,记录下对象方法的调用时间、参数、返回值等信息。
  5. 缓存:对于一些计算成本高但结果不常变的方法,我们希望将结果缓存起来,下次同样的请求直接返回缓存结果,而不是重新计算。

核心问题:如何在不改变原始对象(或无法改变它)的前提下,为其访问过程增加额外的控制逻辑?

三、它是什么 (What - The Solution)

代理模式通过引入一个代理对象(Proxy Object) 来解决这个问题。这个代理对象和真实对象实现了同一个接口,因此对于客户端来说,代理对象和真实对象是完全一样的,可以互换使用。

核心思想和角色:

  1. **主体接口 (Subject)**:
    • 定义了真实对象(Real Subject)代理对象(Proxy)共同的接口。
    • 这样,任何可以使用真实对象的地方,也都可以使用代理对象。
  2. **真实主体 (Real Subject)**:
    • 实现了 Subject 接口,是真正执行业务逻辑的那个对象。
    • 代理对象最终会将请求转发给它。
  3. **代理 (Proxy)**:
    • 也实现了 Subject 接口。
    • 它内部持有一个 Real Subject 对象的引用
    • 它负责控制对 Real Subject 的访问。当客户端调用代理的方法时,代理可以执行一些预处理(如权限检查),然后决定是否以及如何调用 Real Subject 的同名方法,最后还可以进行一些后处理(如记日志)。

几种常见的代理类型:

  • **保护代理 (Protection Proxy)**:用于控制访问权限。
  • **虚拟代理 (Virtual Proxy)**:用于延迟加载,当代理对象的方法被调用时,才真正创建和加载真实对象。
  • **远程代理 (Remote Proxy)**:作为远程对象的本地代表,封装网络通信的细节。
  • 日志代理 (Logging Proxy) / **缓存代理 (Caching Proxy)**:用于在方法调用前后添加日志或缓存逻辑。

Java 中的动态代理

除了手动编写代理类(静态代理),Java 还提供了一种更强大的机制——**动态代理 (Dynamic Proxy)**。

  • 静态代理:需要为每一个真实主体类手动编写一个代理类。如果接口增多,代理类也会增多。
  • 动态代理:不需要手动创建代理类文件。在运行时,通过 java.lang.reflect.Proxy 类和 InvocationHandler 接口,可以为一个或多个接口动态地生成代理对象。你只需要编写一个 InvocationHandler(调用处理器),在这个处理器里定义通用的代理逻辑(如日志、权限检查),这个逻辑就可以应用到所有被代理的接口方法上。
  • 应用场景:Spring AOP(面向切面编程)、RPC 框架(如 Dubbo)、MyBatis 的 Mapper 接口等,都大量使用了动态代理技术。

四、怎么做 (How - The Blueprint)

基本步骤(以静态的图片查看器虚拟代理为例):

  1. **定义主体接口 Image**,包含一个 display() 方法。
  2. **创建真实主体类 HighResolutionImage**,实现 Image 接口。它的构造函数会模拟一个耗时的加载过程。
  3. **创建代理类 ImageProxy**,也实现 Image 接口。
    • 它内部持有一个 HighResolutionImage 的引用,但初始时为 null
    • display() 方法中,检查引用是否为 null。如果是,则 new 一个 HighResolutionImage 实例(此时才真正加载图片),然后调用其 display() 方法。如果不是 null,则直接调用。
  4. 客户端创建和使用的是 ImageProxy 对象。

Java 示例代码 (静态虚拟代理)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
// 1. 主体接口 (Subject)
interface Image {
void display();
}

// 2. 真实主体 (Real Subject)
class HighResolutionImage implements Image {
private String imagePath;

public HighResolutionImage(String imagePath) {
this.imagePath = imagePath;
// 模拟从磁盘加载大图片的耗时操作
loadImageFromDisk();
}

private void loadImageFromDisk() {
System.out.println("Loading image from disk: " + imagePath);
try {
Thread.sleep(2000); // 模拟2秒加载时间
} catch (InterruptedException e) {
e.printStackTrace();
}
}

@Override
public void display() {
System.out.println("Displaying image: " + imagePath);
}
}

// 3. 代理 (Proxy)
class ImageProxy implements Image {
private String imagePath;
// 持有真实对象的引用,初始为 null
private HighResolutionImage realImage;

public ImageProxy(String imagePath) {
this.imagePath = imagePath;
}

@Override
public void display() {
// 虚拟代理的核心:懒加载
if (realImage == null) {
// 只有在第一次调用 display 时,才真正创建和加载真实对象
System.out.println("Proxy: Real image needs to be loaded.");
realImage = new HighResolutionImage(imagePath);
}
// 将请求委托给真实对象
realImage.display();
}
}

// 4. 客户端
public class ProxyPatternDemo {
public static void main(String[] args) {
// 创建代理对象,此时并不会加载大图片,非常快
Image image1 = new ImageProxy("path/to/my_photo1.jpg");
Image image2 = new ImageProxy("path/to/my_photo2.jpg");

System.out.println("--- Proxy objects created, real images not loaded yet ---");

// 第一次调用 display(),会触发真实对象的加载
System.out.println("\nCalling display() on image1 for the first time...");
image1.display();

// 第二次调用 display(),直接使用已加载的对象
System.out.println("\nCalling display() on image1 for the second time...");
image1.display();

System.out.println("\nCalling display() on image2...");
image2.display();
}
}

五、UML 类图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@startuml
skinparam classAttributeIconSize 0
hide empty members

interface Image {
+ void display()
}

class HighResolutionImage implements Image {
- String imagePath
- void loadImageFromDisk()
+ void display()
}

class ImageProxy implements Image {
- String imagePath
- HighResolutionImage realImage
+ void display()
}

' Proxy HAS-A RealSubject
ImageProxy o-- "1" HighResolutionImage

class ProxyPatternDemo {}
ProxyPatternDemo ..> Image : uses
@enduml

图示解读:

  • ImageProxyHighResolutionImage 都实现了 Image 接口,因此对客户端是透明的。
  • ImageProxy 内部持有一个对 HighResolutionImage 的引用,实现了对真实对象的控制。
  • 客户端 ProxyPatternDemo 只依赖于 Image 接口。

六、优缺点分析

优点 (Pros):

  1. 解耦:代理模式将客户端与真实主体解耦,客户端无需知道真实主体的存在。
  2. 职责清晰:真实主体只关心核心业务逻辑,而代理则负责处理额外的控制逻辑,符合单一职责原则。
  3. 高扩展性:可以在不修改真实主体的情况下,通过增加新的代理类来添加新功能。
  4. 强大的控制能力:代理模式提供了对真实对象访问的精细化控制,可以实现权限、缓存、懒加载等多种功能。

缺点 (Cons):

  1. 增加系统复杂性:引入了代理类,增加了系统中类的数量,可能会增加理解成本。
  2. 可能导致请求处理变慢:由于在客户端和真实主体之间增加了一层,某些情况下可能会轻微影响请求的处理速度(尽管对于懒加载、缓存等场景,总体性能是提升的)。

七、在 Java 中的应用

代理模式是 Java 世界的基石之一。

  • RMI (Remote Method Invocation): 这是远程代理的经典应用。当你获取一个远程对象的引用时,你得到的其实是一个本地的代理对象(称为 Stub)。你调用这个 Stub 的方法,它会负责打包参数、通过网络发送给远程服务器、等待结果、解包并返回给你,对你来说就像调用本地对象一样。
  • Spring AOP: Spring 的面向切面编程(AOP)功能,如事务管理(@Transactional)、安全控制、日志记录等,就是通过动态代理实现的。Spring 在运行时为你需要被增强的 Bean(真实主体)创建一个代理对象,在这个代理对象中织入了切面逻辑(如开启事务、提交/回滚事务)。
  • Hibernate/JPA: 这些 ORM 框架中的实体对象懒加载(Lazy Loading)功能,是虚拟代理的完美体现。当你从数据库加载一个 Order 对象时,它关联的 Customer 对象可能并不会被立即加载,此时 order.getCustomer() 返回的是一个 Customer 的代理对象。只有当你第一次调用这个代理对象的任何方法(如 customer.getName())时,Hibernate 才会发出 SQL 去数据库真正加载 Customer 的数据。