proxy

第十四章 代理模式(Proxy Pattern)

1 概要

代理模式(Proxy Pattern)是一种结构性的模式(Structural Design Pattern)。在代理模式中,会创建一个代理对象用来"代表真实使用的对象"。这个代理对象可以控制这个"真实"对象何时被创建、何时被访问、如何被访问等问题。例如,这个代理对象能够控制直到真实对象被访问时才被创建。

从功能上看,代理对象并不会重新实现一次“真实”对象的功能;反而,代理对象会直接或者间接的将请求“转发”给真实对象。只不过在“转发”之前或者之后,代理对象会处理一些“额外”的逻辑。

代理模式被广泛的应用于程序设计之中。一般来说,有以下几种场景可以考虑使用代理模式。

  1. 鉴权(Authentication),即保护真实的对象只能被授权用户访问。那么,在代理对象中可以对用户的请求进行权限审核。只有当用户被授予了访问权限时,才能访问真实的对象。这个鉴权的过程可以放在代理对象中。
  2. 日志记录(Logging)。当需要记录访问真实对象的所有操作时,可在代理对象中记录所有的请求,再由代理对象调用真实的对象去完成请求。
  3. 智能代理(Smart Proxy)。假如只有当满足一定条件的情况下才能访问真实对象的话,可在代理对象中完成条件检测。

2 代理模式的结构

在代理模式中,有四个参与方。

  1. Subject接口提供了调用真实逻辑的接口。
  2. RealSubject类实现了Subject接口,并在实现的方法中,完成了逻辑处理。
  3. Proxy类。Proxy类实现了Subject接口,并且Proxy类包含了一个指向Subject接口的成员变量。在实现的方法中,Proxy类控制何时调用Subject对象。
  4. Client类。Client类表示的是使用Proxy类的客户。

图一 代理模式结构

图一 代理模式结构。

3 代理模式示例

我们使用一个简单的用例来展示代理模式的用法。这个用例实现了一个简单的查询状态的功能。这个用例定义了一个接口QueryStatusItf接口,该接口声明了一个成员方法getStatus()。该方法接收一个参数,即调用者的用户名。该用例在QueryStatusImpl中实现了这个接口,并返回状态"Normal"。

interface QueryStatusItf {
    String getStatus(String username);
}

public class QueryStatusImpl implements QueryStatusItf {
    @Override
	public String getStatus(String username) {
	    return "Normal";
	}
}

但是,现在该用例增加了一项需求:只有管理员用户才能使用这个接口查询状态。如果是其他用户访问,则返回null。所以,为了实现这项新的需求,并且将这项新需求与已有的实现分离开,我们可以使用代理模式,由一个新的代理类QueryStatusProxy实现这项新需求。

在QueryStatusProxy类中,它实现了QueryStatusItf接口。所以,调用者能使用QueryStatusItf类型的变量指向QueryStatusProxy对象。QueryStatusProxy类中定义了一个成员变量,用于保存并指向真实的QueryStatus对象。在getStatus()方法中,它首先比较了用户名是否为"admin",用于检验只有管理员用户才能使用,完成状态查询。

public class QueryStatusProxy implements QueryStatusItf {
    private QueryStatusItf realObj = null;
	
	public QueryStatusProxy(QueryStatusItf obj) {
	    this.realObj = obj;
	}
	
    @Override
	public String getStatus(String username) {
	    if ("admin".equals(username))
            return this.realObj.getStatus(username);
		return null;
	}
}

4 应用举例

4.1 静态代理(Static Proxy) VS. 动态代理(Dynamic Proxy)

在上述的例子中,我们明确地实现了QueryStatusProxy代理类。QueryStatusProxy类的源代码是静态不变的;它会经过编译器,生成运行于Java虚拟机上的二进制代码。我们称这种实现方式为静态代理(Static Proxy)

静态代理方法的优势是能够明确的实现代理类的逻辑。针对上述场景,我们使用QueryStatusProxy来代理QueryStatus类。但是,如果我们有10个或者更多的、类似于QueryStatus类时,我们需要相同数量的代理类,这增加了开发人员的开发工作量和维护工作量。所以,为了解决这个问题,Java标准库提供了动态代理(Dynamic Proxy)功能。

与静态代理不同的是,动态代理类是在Java程序运行时动态创建的。动态代理类并不存在于编译阶段。

我们使用下面的一个简单例子来讲解动态代理的工作原理和使用方法。首先,我们创建一个InvocationHandler对象,这个对象用于处理"额外"的逻辑和转发方法调用给"真实"的对象。我们会在下一步详细介绍。

然后,我们使用Proxy类的静态方法newProxyInstance()生成一个代理对象。这里需要注意的是,newProxyInstance()方法接受三个参数。第一个参数是ClassLoader(类加载器)对象;第二个参数是Class<?>对象的数组;第三个参数是前一步创建的handler对象。这个动态代理对象是使用前两个参数创建的。更具体的说,这个创建过程分为两个步骤。第一个步骤是动态创建一个新的类,这个类实现了第二个参数传递的所有接口。在本例中,这个动态的类实现了QueryStatusItf接口。第二个步骤是动态创建这个新类的对象,并从newProxyInstance()方法返回给调用者。所以,开发人员可以将其强制类型转换为QueryStatusItf接口对象。我们可以发现,这个新创建的动态类并不存在于源代码中,它是运行时动态创建的。

另一个需要注意的是,当在动态代理对象上调用给定接口时,这个动态对象能够"截取"方法调用,并将其方法调用转发给第三个参数对象handler。因此,InvocationHandler::invoke()方法会被调用。

import java.lang.reflect.Proxy;
import java.lang.reflect.InvocationHandler;
...

InvocationHandler handler = new InvocationHandlerImpl();
QueryStatusItf queryStatus = (QueryStatusItf)Proxy.newProxyInstance(
	QueryStatusItf.class.getClassLoader(), 
	new Class<?>[] {QueryStatusItf.class}, 
	handler);

InvocationHandler接口只有一个成员方法invoke()。invoke()方法有三个参数。第一个参数是动态生成的代理对象;第二个参数Method表示的是当前被调用的方法对象,第三个参数是方法调用的传入参数。

如果我们使用动态代理来实现QueryStatusProxy静态代理类的话,我们可以在下面的InvocationHandlerImpl类中添加代理逻辑。因为QueryStatusItf接口只有一个成员方法,所以,我们略去了参数检查。当代理接口具有多个函数或者动态代理类实现了多个接口时,开发人员需要使用Java反射机制对参数method和args检查当前调用的方法是哪个方法以及检查传入参数的类型和个数。

import java.lang.reflect.InvocationHandler;

public class InvocationHandlerImpl implements InvocationHandler {
	private Object objectOfQueryStatusImpl = null;

	public InvocationHandlerImpl(Object objectOfQueryStatusImpl) {
        this.objectOfQueryStatusImpl = objectOfQueryStatusImpl;
	}

	public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
		if ("admin".equals(args[0]))
		    return method.invoke(this.objectOfQueryStatusImpl, args);
        return null;
	}
}

因此,我们可以使用以下代码创建并调用动态代理接口。

import java.lang.reflect.Proxy;
import java.lang.reflect.InvocationHandler;

public class DynamicProxyExample {
	public static void main(String[] args) {
		QueryStatusImpl queryStatus = new QueryStatusImpl();

        InvocationHandler handler = new InvocationHandlerImpl(queryStatus);

        QueryStatusItf dynamicProxy = (QueryStatusItf)Proxy.newProxyInstance(
	        QueryStatusItf.class.getClassLoader(), 
	        new Class<?>[] {QueryStatusItf.class}, 
	        handler);
		
        dynamicProxy.getStatus("admin");
	}
}

4.2 Java RMI (Remote Method Invocation)

Java RMI (Remote Method Invocation),或称为Java远程方法调用,非常适合使用代理模式来实现。Java RMI提供了一种协议/机制/框架,允许开发人员直接使用远程服务器上的对象或者方法,就像使用本地对象或者方法一样。其实现原理是,开发人员在本地定义一个远程方法接口,称为Stub。当开发人员调用本地接口时,实际上程序调用的是一个代理对象。该代理对象会将调用的请求发送到远程服务器上,进行"真实"的调用。当调用完毕之后,再将结果返回给本地的调用者。在这个过程中,与远程服务器的交流全部隐藏在代理对象中,调用者无需知道其中的实现细节。

5 小结

本章介绍了代理模式的结构和使用方法。它和其他的结构性模式较为相似,因为它们都是在原有逻辑的基础上加了一层外包类(Wrapping Class)。然而,代理模式与其他模式不同之处在于,代理类与真实类都继承了相同的接口。所以,代理类的接口是与真实类的接口兼容的。当在代码中将调用真实类的地方更换为调用代理类,不会影响调用者的处理逻辑。

上一章
下一章

注册用户登陆后可留言

Copyright  2019 Little Waterdrop, LLC. All Rights Reserved.