关于代理设计模式

文章目录

做了个资料整合的工作,给《设计模式:可复用面向对象软件的基础》相关正文部分提供了更多示例等信息,并使用Java演示了部分代理技术。

代理设计模式属于结构型设计模式。结构型模式涉及到如何组合类和对象以获得更大的结构。

代理模式主要目的是为其他对象提供一个代理或占位符,以控制对这个对象的访问。在代理模式中,代理对象可以介入其所代表的真实对象的活动,这种介入可以是透明的,也可以改变或增强真实对象的行为。

按用途分类

代理是在需要用比较通用和复杂的对象指针代替简单的指针的时候的。

远程代理(Remote Proxy)

可以让客户端通过一个本地对象来操作一个远程对象,就像它们在同一个地址空间一样。远程代理可以隐藏远程对象的位置和通信细节,从而简化了客户端的编程。

以下是一些例子:

  1. RPC。

  2. 书上的例子:“NEXTSTEP使用NXProxy类实现了这一目的”,这里补充以下信息:

    NEXTSTEP是一种面向对象的操作系统,它使用了Objective-C语言和NeXTSTEP框架。NeXTSTEP框架提供了一些类和协议来支持远程代理的实现,其中最重要的是NXProxy类和NXRemoteObject协议。

    NXProxy类是一个抽象类,它实现了NXRemoteObject协议,该协议定义了远程对象的基本行为。NXProxy类的子类可以根据不同的通信机制来实现具体的远程代理,例如使用分布式对象(DO)或远程消息传递(RMP)。

    分布式对象是一种基于端口的通信机制,它允许在同一台或不同台计算机上运行的进程之间传递对象。分布式对象使用NSConnection类来建立连接,并使用NSDistantObject类来创建远程代理。NSDistantObject类是NXProxy类的一个子类,它可以将客户端对本地代理对象的方法调用转换为对远程对象的方法调用,并将返回值转换回本地对象。

    远程消息传递是一种基于消息的通信机制,它允许在不同地址空间的对象之间发送和接收消息。远程消息传递使用NXMessagePort类来建立连接,并使用NXMessageProxy类来创建远程代理。NXMessageProxy类也是NXProxy类的一个子类,它可以将客户端对本地代理对象的消息发送转换为对远程对象的消息发送,并将返回值转换回本地对象。

虚代理(Virtual Proxy)

当真实对象很大或很耗资源时,我们需要用一个轻量级的代理对象来延迟真实对象的创建或加载,从而提高性能或节省内存。

  • 一个书上的例子:考虑一个可以在文档中嵌入图形对象的文档编辑器。有些图形对象(如大型光栅图像)的创建开销很大。但是打开文档必须很迅速,因此我们在打开文档时应避免一次性创建所有开销很大的对象。而且并非所有这些对象在文档中都同时可见,也没有必要同时创建这些对象。在文档中就可以用图像Proxy来代替哪个真正的图像,并且在需要时负责创建图像。

保护代理(Protection Proxy)

当真实对象有不同的访问权限时,我们需要用一个代理对象来控制对真实对象的访问,从而实现安全性或鉴权。

  • 一个例子是:

    假设有一个文件服务器,它提供了一个File类来表示文件对象,File类有以下方法:

    • read(): 读取文件内容
    • write(content): 写入文件内容
    • delete(): 删除文件

    这些方法都需要不同的访问权限,比如只有文件的所有者才能写入或删除文件,而其他用户只能读取文件。为了实现这个功能,我们可以使用一个FileProxy类来作为File类的代理,FileProxy类有以下属性和方法:

    • file: 真实的文件对象
    • user: 当前的用户对象
    • read(): 调用file的read()方法,并返回结果
    • write(content): 检查user是否是file的所有者,如果是,则调用file的write(content)方法,如果不是,则抛出异常
    • delete(): 检查user是否是file的所有者,如果是,则调用file的delete()方法,如果不是,则抛出异常

    这样,客户端就可以通过FileProxy类来操作File类,而不需要关心访问权限的细节。

缓存代理(Cache Proxy)

当真实对象的操作很耗时或很频繁时,我们需要用一个代理对象来缓存真实对象的结果,从而减少重复计算或网络传输。

智能指引(Smart Reference)

取代了简单的指针,它在访问对象时执行一些附加操作(附加的内务处理 Housekeeping task)。

  • 这个可以用在:
    • 引用计数:当真实对象被多个客户端共享时,我们需要用一个智能指引对象来记录真实对象的引用次数,从而在没有任何客户端引用时自动释放真实对象的内存。
    • 锁定:当真实对象被多个客户端并发访问时,我们需要用一个智能指引对象来对真实对象进行加锁和解锁,从而保证数据的一致性和安全性。

按实现方式分类

代理设计模式通常涉及这几个组成部分:

  • Suject(抽象主题),可以是抽象类,也可以是接口;
  • RealSubject(真实主题),implements Subject,具体执行业务逻辑;
  • Proxy(代理),implements Subject 同时保存对RealSubject的引用,可以访问和控制RealSubject,并可以负责其创建和删除。

静态代理

静态代理中就可以很好体现上面几个组成部分:

 1public interface Subject {
 2    void doSomething();
 3}
 4
 5public class RealSubject implements Subject{
 6 
 7    @Override
 8    public void doSomething() {
 9        //TODO 具体执行逻辑
10    }
11}
12
13public class Proxy implements Subject{
14    //要代理的RealSubject
15    private Subject realSubject;
16 
17    public Proxy(Subject realSubject){
18        this.realSubject = realSubject;
19    }
20    @Override
21    public void doSomething() {
22        this.before();
23        realSubject.doSomething();
24        this.after();
25    }
26
27    private void before(){
28        // TODO
29    }
30 
31    private void after(){
32        // TODO
33    }
34}

动态代理

使用代理可以在运行时创建实现了一组给定接口的新类。只有在编译时期无法确定需要哪个接口时使用代理。

主要有两种实现方法:JDK动态代理,Java提供的原生支持;字节码生成,通常依赖于第三方库,如CGLIB或ByteBuddy,它们通过直接操作字节码来创建代理类,从而提供更强大、更高效的代理功能。

JDK动态代理

先以JDK动态代理为例。

比如,假设我们有一个接口ILawsuit,它定义了打官司的一些方法,如提交申请、举证、辩护等。

1public interface ILawsuit {
2    void fileComplaint();
3    void presentEvidence();
4    void defend();
5}

我们有一个类XProgrammer,它实现了ILawsuit接口,表示X程序员要打官司。

 1public class XProgrammer implements ILawsuit{
 2    public void fileComplaint() {
 3        System.out.println("XProgrammer" + "提交了申请.");
 4    }
 5
 6    public void presentEvidence() {
 7        System.out.println("XProgrammer" + "完成了举证.");
 8    }
 9
10    public void defend() {
11        System.out.println("XProgrammer" + "进行辩护.");
12
13    }
14}

我们想让律师Lawyer作为X程序员的代理,帮助他打官司。

如果使用静态代理,我们需要创建一个代理类Lawyer,它也要实现ILawsuit接口,并且持有一个XProgrammer的引用,在每个方法中调用XProgrammer的对应方法,并且可以添加一些自己的操作。

这样做的缺点是代码冗余:

  • 如果ILawsuit接口增加了一个方法,那么Lawyer也要增加这个方法,并且调用XProgrammer的方法。
  • 如果我们有多个被代理对象,比如YDesigner、ZTeacher等,都实现了ILawsuit接口,那么我们就要为这每个角色都给一个专门的律师来打官司了。

使用动态代理就可以避免上面的弊端。

我们只需要创建一个类LaywerProxy,它实现了InvocationHandler接口,并且持有一个被代理对象的引用,在invoke方法中调用被代理对象的对应方法,并且可以添加一些自己的操作。

 1public class LawyerProxy implements InvocationHandler {
 2    private ILawsuit realSubject;
 3    public LawyerProxy(ILawsuit realSubject) {
 4        this.realSubject = realSubject;
 5    }
 6    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
 7        System.out.println("律师操作了一下");
 8        Object result = method.invoke(realSubject, args);
 9        System.out.println("律师又操作了一下");
10        return result;
11    }
12}

然后我们可以使用Proxy类的newProxyInstance方法来动态生成一个代理对象,这个代理对象会实现被代理对象所实现的所有接口,并且把所有方法的调用都转发给LawyerProxy的invoke方法。

 1public class Test {
 2    public static void main(String[] args) {
 3        ILawsuit someone = new XProgrammer();
 4        LawyerProxy lawyer = new LawyerProxy(someone);
 5        ILawsuit proxySubject = (ILawsuit) Proxy.newProxyInstance(
 6                ILawsuit.class.getClassLoader(),
 7                new Class<?>[]{ILawsuit.class},
 8                lawyer
 9        );
10        proxySubject.fileComplaint();
11    }
12}

这样做的优点是代码简洁,如果ILawsuit接口增加了一个方法,那么LawyerProxy不需要修改任何代码,而且我们可以用同一个LawyerProxy来为多个被代理对象生成代理对象。

cglib动态代理

这里以cglib为例。cglib是一个基于ASM(一个 Java 字节码操控框架)的字节码生成库,它可以在运行时动态地创建和修改Java类。cglib提供了一个Enhancer类,它可以用来生成目标类的子类,并实现方法的拦截和回调。

Enhancer类有两种方式来设置回调对象,一种是使用setCallback(Callback callback)方法,它可以为所有的方法设置同一个回调对象。另一种是使用setCallbacks(Callback[] callbacks)方法,它可以为不同的方法设置不同的回调对象,但是需要配合setCallbackFilter(CallbackFilter filter)方法来指定回调对象的索引。

 1public class TargetObject {
 2    public String method1(String paramName) {
 3        return paramName;
 4    }
 5
 6    public int method2(int count) {
 7        return count;
 8    }
 9}
10
11// 回调对象1
12public class Callback1 implements MethodInterceptor {
13    @Override
14    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
15        System.out.println("Before method1");
16        Object result = proxy.invokeSuper(obj, args);
17        System.out.println("After method1");
18        return result;
19    }
20}
21
22// 回调对象2
23public class Callback2 implements MethodInterceptor {
24    @Override
25    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
26        System.out.println("Before method2");
27        Object result = proxy.invokeSuper(obj, args);
28        System.out.println("After method2");
29        return result;
30    }
31}
32
33// 回调过滤器
34public class CallbackFilterImpl implements CallbackFilter {
35    @Override
36    public int accept(Method method) {
37        if ("method1".equals(method.getName())) {
38            return 0; // 返回回调对象1的索引
39        } else {
40            return 1; // 返回回调对象2的索引
41        }
42    }
43}
44
45// 测试类
46public class TestCglib {
47    public static void main(String[] args) {
48        Enhancer enhancer = new Enhancer();
49        enhancer.setSuperclass(TargetObject.class);
50        
51        // 使用setCallback方法设置单一回调对象
52        enhancer.setCallback(new Callback1());
53        // 创建代理对象
54        TargetObject proxy1 = (TargetObject) enhancer.create();
55        // 调用代理对象的方法
56        System.out.println(proxy1.method1("hello"));
57        System.out.println(proxy1.method2(100));
58        
59        // 使用setCallbacks和setCallbackFilter方法设置多个回调对象
60        enhancer.setCallbacks(new MethodInterceptor[]{new Callback1(), new Callback2()});
61        enhancer.setCallbackFilter(new CallbackFilterImpl());
62        // 创建代理对象
63        TargetObject proxy2 = (TargetObject) enhancer.create();
64        // 调用代理对象的方法
65        System.out.println(proxy2.method1("world"));
66        System.out.println(proxy2.method2(200));
67    }

RPC中使用Java动态代理技术

实际使用时不需要自己实现,有很多RPC框架可供选择。

RPC中使用Java动态代理技术的步骤一般如下:

  • 定义一个公共的接口,表示要调用的远程方法,比如HelloService接口,它有一个sayHello方法。
  • 在服务器端,实现该接口,并注册到RPC框架中,比如HelloServiceImpl类,它实现了HelloService接口,并提供了sayHello方法的具体逻辑。
  • 在客户端,使用Proxy.newProxyInstance方法,传入一个InvocationHandler实现类,生成一个HelloService接口的代理对象,比如HelloServiceProxy类,它实现了InvocationHandler接口,并重写了invoke方法。
  • 在invoke方法中,根据传入的方法名、参数等信息,构造一个RPC请求对象,并发送给服务器端,然后等待服务器端的响应,并将响应结果返回给客户端。
  • 在客户端,通过代理对象调用sayHello方法,就像调用本地方法一样,但实际上是通过网络通信与服务器端交互。

总结

最近面试被问到这方面问题,其实都是知道的,但是面试的时候没有清晰答出来,自己的基础还是薄弱了,遂做了一个资料整合的工作。其实这块还有很多需要学的,比如书上还有用c++、Smalltalk实现代理之类的,看着还蛮有趣的,暂时对这两门语言不熟,就没管了。aop相关的概念也没有整理进来,那些也都好理解。

参考资料

  • 《Java核心技术卷1》代理部分
  • 《设计模式:可复用面向对象软件的基础》4.7
comments powered by Disqus