img

🍐⚱️:(代理模式举例)爱因斯坦和他司机的故事——司机负责演讲,有人提问的时候就叫爱因斯坦起来回答问题 [1]

小故事:爱因斯坦和他的司机

爱因斯坦提出相对论后,震惊世界,于是被很多大学邀请去做报告,爱因斯坦因此而被弄得疲惫不堪。

有一天,司机对他说:“你太累了,今天我帮你作报告吧?”

爱因斯坦问:“你能行吗?”

司机说:“我闭着眼睛都能背出来。”

那天司机上台,果然讲得滴水不漏。

但刚想下台时,一位博士站了起来,然后提了一个非常深奥刁钻的问题。

司机不知怎么作答,幸好脑瓜转得快:“你这问题太简单了,我司机都能回答。”

爱因斯坦站起来,几句话就解决了问题。

博士惊呆了:“没想到他的司机也远胜于我。”

但在回去的路上,司机对爱因斯坦说:“我知道的只是概念,你懂得的才是知识。”

代理模式可以在不改变原始类(被代理类)代码的情况下,通过引入代理类来给原始类附加功能。

登场角色:

  • Subject(主体):Subject 角色定义了使 Proxy 角色和 RealSubject 角色之间具有一致性的接口。由于存在 Subject 角色,所以 Client 角色不必在意它所使用的究竟是 Proxy 角色还是 RealSubject 角色。
  • Proxy(代理人):Proxy 角色会尽量处理来自 Client 角色的请求。只有当自己不能处理时,它才会将工作交给 RealSubject 角色。Proxy 角色只有在必要时才会生成 RealSubject 角色。Proxy 角色实现了在 Subject 角色中定义的接口(API)。在示例程序中,由 PrinterProxy 类扮演此角色。
  • RealSubject(实际的主体、被代理类):“本人”RealSubject 角色会在“代理人”Proxy 角色无法胜任工作时出场。它与 Proxy 角色一样,也实现了在 Subject 角色中定义的接口(API)。RealSubject 并不知道 Proxy 的存在。
  • Client(请求者):使用 Proxy 模式的角色。在 GoF 书中,Client 角色并不包含在 Proxy 模式中。

image.png

代码示例:

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
interface Subject {
void request1();

void request2();

void request3();
}

// 被代理类
class RealSubject implements Subject {

@Override
public void request1() {
System.out.println("被代理类执行 request 1");
}

@Override
public void request2() {
System.out.println("被代理类执行 request 2");
}

@Override
public void request3() {
System.out.println("被代理类执行 request 3");
}
}

// 代理类
class Proxy implements Subject {
RealSubject realSubject = new RealSubject(); // 构造函数或IoC容器注入

@Override
public void request1() {
/* 写自己的逻辑 */
realSubject.request1();
/* 写自己的逻辑 */
}

@Override
public void request2() {
realSubject.request2();
}

@Override
public void request3() {
realSubject.request3();
}
}

使用方式:

1
2
3
4
5
6
7
8
9
public static void main(String[] args) {
Subject subject = new Proxy();
subject.request1();
}
/*
输出:
Proxy 代理类的自定义逻辑
被代理类执行 request 1
*/

代理模式在访问实际对象时引入一定程度的间接性,利用这种间接性我们可以附加多种用途。拓展思路:

  • 使用代理人来提升处理速度
  • 我们可以不划分 Proxy 类和 RealSubject 类,而是直接在 RealSubject 类中加入惰性求值功能(即只有必要时才生成实例的功能)。不过划分 ProxyRealSubject 角色可以使它们成为独立的组件,在修改的时候也不会互相之间产生影响(分而治之)。
  • 代理与委托。代理人只代理他能解决的问题。当遇到他不能解决的问题时,还是会「委托」给本人去解决。(在现实世界中,应当是本人将事情委托给代理人负责,而在设计模式中则是反过来的。)
  • 透明性。在 MainClient 直接使用 Subject 接口,Main 不必在意调用的究竟是哪个类。在这种情况下,可以说 Proxy 类是具有「透明性」的。
  • 各种代理模式:代理模式有很多种变化形式。
    • Virtual Proxy(虚拟代理):Virtual Proxy 就是本章节演示的代理模式。只有当真正需要实例时,它才生成和初始化实例。
    • Remote Proxy(远程代理):Remote Proxy 可以让我们完全不必在意 RealSubject 角色是否在远程网络上,可以如同它在自己身边一样(透明性地)调用它的方法。Java 的 RMI (Remote Method Invocation 远程方法调用) 就相当于 Remote Proxy。或者 RPC 框架。
    • Access Proxy:Access Proxy 用于在调用 RealSubject 角色的功能时设置访问限制。例如,这种代理可以只允许指定的用户调用方法,而当其他用户调用方法时则报错。
打印机案例

image.png

  • Printer 打印机 print() 函数调用了 heavyJob 函数。
  • PrinterProxy 不实现真正的 print 函数。 realize 函数用于实现真正的打印机,这个函数只在调用了代理类的 print 函数时调用。不论 setPrinterName 方法和 getPrinterName 方法被调用多少次,都不会生成 Printer 类的实例。只有当真正需要本人时,才会生成 Printer 类的实例。
  • 上图疑似有误:Client 应当使用抽象 Subject 才对

相关的设计模式:

  • 适配器模式 Adapter:Adapter 模式适配了两种具有不同接口(API)的对象,以使它们可以一同工作。而在 Proxy 模式中,Proxy 角色与 RealSubject 角色的接口(API)是相同的(透明性)。
  • 装饰者模式 Decorator:Decorator 模式与 Proxy 模式在实现上很相似,不过它们的使用目的不同。Decorator 模式的目的在于增加新的功能。而在 Proxy 模式中,与增加新功能相比,它更注重通过设置代理人的方式来减轻本人的工作负担。

通过继承的方式代理第三方类

在上面的例子中,代理类 Proxy 和被代理类 RealSubject 都继承同一个接口。如果被代理类没有定义接口,且无法修改源码(如第三方类库的类),我们就不能用上面的方法实现代理模式。

解决方案:继承。让代理类继承原始类,然后扩展附加功能。

image.png

代理类 Proxy 通过 super.request1() 的方式委托被代理类。

动态代理 Dynamic Proxy

由程序员创建或特定工具自动生成代理模式的源代码,即在编译前就已经将接口,被代理类,代理类等确定下来的代理方式,叫做静态代理。

如果通过静态代理的方式实现代理模式,对于每个代理类 Proxy,我们都需要将被代理类的所有方法重新实现一遍,且代理类 Proxy 每个方法都有相似的代码逻辑。如果被代理类很多,那么代理类也会很多,增加代码的维护成本。

动态代理:不事先为每个原始类编写代理类,而是在运行的时候,根据我们在 Java 代码中的「指示」,动态地创建原始类对应的代理类,然后在系统中用代理类替换掉原始类。相比于静态代理, 动态代理的优势在于可以很方便的对代理类的函数进行统一的处理,而不用修改每个代理类中的方法。

代理方式 静态代理 动态代理
JVM 编译时生成代理类的 class 文件 运行时动态生成类字节码并加载到 JVM
灵活性 🔴接口一旦新增加方法,目标对象和代理对象都要进行修改 🟢不需要针对每个目标类都创建一个代理类

Java 标准库提供了动态代理机制,可以在运行期动态创建某个 interface 的实例,不用编写实现类。动态代理依赖 Java 站内文章反射 语法。

Java 常用的动态代理实现方式有:

动态代理 JDK CGLIB
代理对象 实现了接口的类或者直接代理接口 未实现任何接口的类
效率 🟢 🟡
特点 简单易用 引入第三方库

在 Spring 中的 站内文章AOP 模块中,如果目标对象实现了接口,则默认采用 JDK 动态代理,否则采用 CGLIB 动态代理。

JDK 动态代理

假设,代理类 Proxy 在调用被代理类前都需要执行前置方法和后置方法。在静态代理方式,我们需要将 Proxyrequest1request2request3 都改动代码。

1
2
3
4
5
6
7
8
9
10
11
public class Proxy implements Subject{
private RealSubject realSubject; // 省略注入逻辑

public void request1(){
beforeMethod(); // 前置方法
realSubject.request1(); // 调用被代理类方法
afterMethod(); // 后置方法
}
// 省略 request2,request3
}

在 Java 的 java.lang.reflect 包下提供了一个 Proxy 类和一个 InvocationHandler 接口,通过这个类和这个接口可以生成 JDK 动态代理类和动态代理对象。

JDK 动态代理类使用步骤:

  1. 定义一个接口及其实现类;
  2. 自定义 InvocationHandler 并重写 invoke 方法,在 invoke 方法中我们会调用原生方法(被代理类的方法)并自定义一些处理逻辑;
  3. 通过 Proxy.newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h) 方法创建代理对象;

下面是示例代码:

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
class MyInvocationHandler<T> implements InvocationHandler {
T target; // 被代理对象

public MyInvocationHandler(T target) {
this.target = target;
}

/**
proxy :动态生成的代理类
method : 与代理类对象调用的方法相对应
args : 当前 method 方法的参数
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("MyInvocationHandler 执行 " + method.getName() + " 方法");

// 编写调用方法前逻辑
System.out.println("方法执行前,代理类执行自定义逻辑");
// 执行被代理类的方法
Object result = method.invoke(target, args);
// 编写调用方法前逻辑
System.out.println("方法执行后,代理类执行自定义逻辑\n");

return result;
}
}

主函数使用动态代理:

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
public class ProxyTest {
public static void main(String[] args) {
// 被代理对象
Subject subject = new RealSubject();

//创建一个与代理对象相关联的InvocationHandler
InvocationHandler handler = new MyInvocationHandler<>(subject);

Subject proxy = (Subject) java.lang.reflect.Proxy
.newProxyInstance(
Subject.class.getClassLoader(), // 传入ClassLoader,通常就是接口类的`ClassLoader`
new Class[]{Subject.class}, // 传入要实现的接口,即被代理类实现的接口,可能会有多个
handler // 传入处理调用方法的InvocationHandler
);

proxy.request1();
proxy.request2();
proxy.request3();
}
}

/*
输出:
MyInvocationHandler 执行 request1方法
方法执行前,代理类执行自定义逻辑
被代理类执行 request 1
方法执行后,代理类执行自定义逻辑

MyInvocationHandler 执行 request2方法
方法执行前,代理类执行自定义逻辑
被代理类执行 request 2
方法执行后,代理类执行自定义逻辑

MyInvocationHandler 执行 request3方法
方法执行前,代理类执行自定义逻辑
被代理类执行 request 3
方法执行后,代理类执行自定义逻辑
*/

当我们的动态代理对象调用一个方法时,这个方法的调用就会被转发到实现 InvocationHandler 接口类的 invoke 方法来调用。

此处动态代理的优势体现为:可以方便对代理类的函数进行统一处理,不用修改代理类中的方法。因为代理类中的方法,是通过 InvocationHandler 中的 invoke 方法调用的,所以我们只要在 invoke 方法中统一处理,就可以对所有被代理的方法进行相同的操作了。

优化:使用工厂创建代理类

在上面的 main 代码调用中,我们创建实现了 Subject 接口的代理类还能通过 站内文章引入工厂 进一步优化。

1
2
3
4
5
6
7
8
9
class SubjectProxyFactory {
public static Subject getProxy(Object target) {
return (Subject) java.lang.reflect.Proxy.newProxyInstance(
target.getClass().getClassLoader(), // 目标类的类加载器
target.getClass().getInterfaces(), // 代理需要实现的接口,可指定多个
new MyInvocationHandler<>(target) // 代理对象对应的自定义 InvocationHandler
);
}
}

main 代码的调用简化为:

1
2
Subject subject = new RealSubject();
Subject proxy = SubjectProxyFactory.getProxy(subject);

源码浅析(真的很浅)

通读 JDK 动态代理实现源码,我们可以简单理解为 JVM 帮我们自动编写了一个下面的类。这个类时动态生成的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 这个代码是对源码的极大压缩和简化,实际的代理类生成很复杂
public class DynamicProxy implements Subject {
InvocationHandler handler; // 中介类
// 动态代理类的构造函数
public DynamicProxy(InvocationHandler handler) {
this.handler = handler;
}
public void request1() {
this.handler.invoke(
this,
Subject.class.getMethod("request1", new Class[0]),
null
);
}
}

上面 JVM 动态生成的代码中 implements Subject 注定了 JDK 动态代理方式只能代理接口,而不能代理类。

CGLIB 动态代理

CGLIB(Code Generation Library)是一个基于 ASM 的字节码生成库,它允许我们在运行时对字节码进行修改和动态生成。CGLIB 通过继承方式实现代理。

在 CGLIB 动态代理机制中 MethodInterceptor 接口和 Enhancer 类是核心。

CGLIB 动态代理是通过生成一个被代理类的子类来拦截被代理类的方法调用,因此不能代理声明为 final 类型的类和方法,private 方法也无法代理。

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
// 被代理类
class RealSubject {

public void request1() {
System.out.println("被代理类执行 request 1");
}

public void request2() {
System.out.println("被代理类执行 request 2");
}

public void request3() {
System.out.println("被代理类执行 request 3");
}
}

class MyMethodInterceptor implements MethodInterceptor {

/**
* @param o 被代理的对象(需要增强的对象)
* @param method 被拦截的方法(需要增强的方法)
* @param objects 方法入参
* @param methodProxy 用于调用原始方法
*/
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
//调用方法之前,我们可以添加自己的操作
System.out.println("before method " + method.getName());
Object object = methodProxy.invokeSuper(o, objects);
//调用方法之后,我们同样可以添加自己的操作
System.out.println("after method " + method.getName() + "\n");
return object;
}
}

class CglibProxyFactory {
public static Object getProxy(Class<?> clazz) {
// 创建动态代理增强类
Enhancer enhancer = new Enhancer();
// 设置类加载器
enhancer.setClassLoader(clazz.getClassLoader());
// 设置被代理类
enhancer.setSuperclass(clazz);
// 设置方法拦截器
enhancer.setCallback(new MyMethodInterceptor());
// 创建代理类
return enhancer.create();
}
}

/**
* @author uuanqin
*/
public class ProxyTestCGLIB {
public static void main(String[] args) {
// 代理类
RealSubject proxy = (RealSubject) CglibProxyFactory.getProxy(RealSubject.class);

proxy.request1();
proxy.request2();
proxy.request3();
}
}

代理模式的应用

与业务不太相关的附加功能

业务系统中存在一些附加功能,比如:监控、统计、鉴权、限流、事务、幂等、日志。我们可以将将这些附加功能与业务功能解耦,放到代理类中统一处理,让程序员只需要关注业务方面的开发。在 Spring 中,这部分的工作可以交给 AOP 完成。

RPC 框架

详看文章:站内文章简易 RPC 调用框架的实现

缓存

假设我们要开发一个接口请求的缓存功能,对于某些接口请求,如果入参相同,在设定的过期时间内,直接返回缓存结果,而不用重新进行逻辑处理。比如,针对获取用户个人信息的需求,我们可以开发两个接口,一个支持缓存,一个支持实时查询。对于需要实时数据的需求,我们让其调用实时查询接口,对于不需要实时数据的需求,我们让其调用支持缓存的接口。

参考实现方式:在应用启动的时候,我们从配置文件中加载需要支持缓存的接口,以及相应的缓存策略(比如过期时间)等。当请求到来的时候,我们在 AOP 切面中拦截请求,如果请求中带有支持缓存的字段(比如 http://…?..&cached=true),我们便从缓存(内存缓存或者 Redis 缓存等)中获取数据直接返回。

本文 PlantUML 归档

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
interface Subject{
{method} {abstract} request1
{method} {abstract} request2
{method} {abstract} request3
}

class Proxy{
realSubject
{method} request1
{method} request2
{method} request3
}

class RealSubject{
{method} request1
{method} request2
{method} request3
}

class Client{

}

Subject <|.. Proxy
Subject <|.. RealSubject
Proxy o- RealSubject : Uses

Client -> Subject : Uses
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class RealSubject{
{method} request1
{method} request2
{method} request3
}

class Proxy{
realSubject
{method} request1
{method} request2
{method} request3
}

class Client{

}

RealSubject <|-- Proxy

Client -> Proxy : Uses

本文参考


  1. 其实这个故事的主角有多个版本,详看这篇文章:Anecdote Origin: Your Question Is Quite Simple. Hence, I’m Going To Ask My Chauffeur To Respond – Quote Investigator® ↩︎