15 代理模式
15.1 代理模式概述
Proxy Pattern: 给某一个对象提供一个代理或占位符,由代理对象来控制对原对象的访问。
代理对象是客户端和目标对象的之前的桥梁,它接收来自客户端的请求并转发给目标对象,去掉客户端不能看到的内容或添加额外的服务。为了保证客户端使用的透明性,代理对象和原对象需要实现相同的接口。
代理模式结构图如下所示:
15.2 代理模式实现
15.2.1 抽象主题接口
代理对象和原访问对象都需要实现该接口,使得任何使用真实主题对象的地方都可以使用代理对象,客户端也可以针对抽象层编程。
public interface Subject {
public void request();
}
15.2.2 真实主题类
public class RealSubject implements Subject {
public void request() {
// 业务逻辑代码
}
}
15.2.3 代理类
代理类中包含对真实主题的引用,可以把客户端请求转发给真实主题并将请求结果返回给客户端。同时,代理类在将请求转发给真实主题之前和之后还可以执行一些其他操作,增加客户端需要的额外服务。
public class Proxy implements Subject {
private Subject realSubject = new RealSubject();
public void request() {
preRequest();
// 请求转发给真实主题
realSubject.request();
postRequest();
}
public void preRequest() {
// 转发前业务代码
}
public void postRequest() {
// 转发后业务代码
}
}
15.2.4 客户端调用
public class Client {
public static void main(String[] args) {
// 针对抽象层编程, 具体类名 Proxy 可以在配置文件写入
// 该代码等价于 Subject subject = new Proxy();
Subject subject = (Subject) XMLUtil.getBean();
// 通过代理类访问真实主题
subject.request();
}
}
15.3 远程代理
Remote Proxy: 远程代理使得客户端可以访问远程主机上的对象并调用其方法。
远程代理示意图如下所示:代理对象负责接收客户端请求,并通过网络通信访问远程对象,代理对象对于客户端是透明的。
15.3.1 Java RMI
Method Invocation是Java对象间通信最基本的方法,对于同一虚拟机内的对象通信,简单的方法调用即可实现。而对于位于不同主机上的虚拟机中的对象之间通信,则需要一套远程方法调用的机制,帮助我们获得远程主机上对象的引用,并在自己的虚拟机中像使用自己的对象一样使用它。
RMI 允许我们调用远程对象上的方法,可以将 Java 对象作为参数传递,并获得 Java 对象作为返回值。因此 RMI 需要使用对象序列化,在网络上传输对象,必要时还可以使用动态类加载和安全管理器安全地传输 Java 对象。
15.3.2 Stubs and skeletons
Java RMI 主要工作流程如下所示:
Stub: 被称为 ”存根“,保存在客户端,即代理模式中出现的代理类,其负责与远程对象通信
Skeletons: 被称为 ”骨架“,保存在服务端,用于接收 Stub 发送过来的请求,调用服务端对象的方法,并将返回值发送给 Stub
15.3.3 Java RMI Server
Java RMI 服务端主要是构建一个可以通过网络传输的结果类和一个可以被远程访问的目标类,同时需要将该目标类的实例对象注册到 RMI Registry
// 自定义远程接口, 需要继承自 Remote
public interface RemoteObject extends Remote {
public String remoteHello() throws RemoteException;
}
/**
* 这个例子没用构建结果类,直接使用 String 类(可序列化)
* 如果要自定义对象结果类,只需要实现 Serializable 接口即可
*/
public class RemoteObjectImpl extends UnicastRemoteObject implements RemoteObject {
// 空的构造方法,不能使用默认构造,因为需要抛出异常
protected RemoteObjectImpl() throws RemoteException {}
public String remoteHello() throws RemoteException() {
// 业务逻辑代码
// return String 对象
}
}
// 注册
public class RMIServer {
public static void main(String[] args) throws RemoteException, AlreadyBoundException, MalformedURLException {
try {
Registry registry = LocateRegistry.createRegistry(1900);
//实例化远程对象类
RemoteObject remoteObject = new RemoteObjectImpl();
//通过 Naming.bind()方法绑定别名与远程对象
Naming.bind("rmi://127.0.0.1:1900/remoteService", remoteObject);
}
catch(Exception e) {
e.printStackTrace();
}
}
}
15.3.4 Java RMI Client
public class RMIClient {
public static void main(String[] args) throws RemoteException, NotBoundException {
Registry registry = LocateRegistry.getRegistry("127.0.0.1", 1900);
//打印注册中心的远程对象列表
System.out.println(Arrays.toString(registry.list()));
//通过别名获取远程对象代理 stub 并调用远程对象的方法
RemoteObject stub = (RemoteObject) registry.lookup("remoteService");
System.out.println(stub.remoteHello());
}
}
15.4 动态代理
在传统的代理模式中,客户端通过代理对象调用真实主题中的方法。这种情况下,代理类和真实主题类都事先存在,且代理方法也明确指定。每个代理类经过编译之后生成字节码文件,其实现的接口和代理的方法均被固定,这种代理模式被称为静态代理。
静态代理扩展非常不便,无论是需要给不同的主题增加代理还是给同一主题代理不同方法,都需要增加新的代理类,或者修改原代码(不符合开闭原则)。
动态代理(Dynamic Proxy)技术可以让系统在运行时根据需要动态创建代理类,使同一个代理类能够代理多个不同的真实主题类和不同方法。
15.4.1 动态代理实现
-
构建两个不同的真实主题接口
public interface SubjectA { public Boolean proxyMethod1(String s); public String proxyMethod2(String s); } public interface SubjectB { public String proxyMethod1(String s); }
-
构建具体主题类
public class ConcreteSubjectA implements SubjectA{ @Override public Boolean proxyMethod1(String s) { // 业务逻辑代码 return true; } @Override public String proxyMethod2(String s) { String res = s + "Call SubjectA proxy method2 success!"; return res; } } public class ConcreteSubjectB implements SubjectB{ @Override public String proxyMethod1(String s) { String res = s + "Call SubjectB proxy method1 success!"; return res; } }
-
构建请求处理程序,需要实现
InvocationHandler
接口import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; public class DynamicProxyHandler implements InvocationHandler{ // object: 需要动态创建的真实主题对象 private Object object; public DynamicProxyHandler() {} public DynamicProxyHandler(Object object) { this.object = object; } /** * 实现 InvocationHandler 的 invoke() 方法 * @param proxy: 代理对象 * @param method: 代理方法 * @param args: 代理方法参数列表 */ @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { beforeInvoke(); 调用真实主题中定义的方法 Object res = method.invoke(object, args); afterInvoke(); return res; } // 织入一些调用前和调用后的处理逻辑, 可以为客户端提供额外的服务 public void beforeInvoke() { // 真实主题代理方法调用前 // 逻辑代码 } public void afterInvoke() { // 真实主题代理方法调用后 // 逻辑代码 } }
-
客户端调用
import java.lang.reflect.InvocationHandler; import java.lang.reflect.Proxy; public class Client { public static void main(String[] args) { InvocationHandler invocationHandler = null; SubjectA subjectA = new ConcreteSubjectA(); SubjectB subjectB = new ConcreteSubjectB(); // 创建请求转发到的处理程序 invocationHandler = new DynamicProxyHandler(subjectA); SubjectA proxyA = null; /** * 通过反射创建代理类实例对象 * @param loader: 代理类的加载器 * @param interfaces: 代理类所实现的接口列表 * @param handler: 客户端请求转发到的处理程序 */ proxyA = (SubjectA)Proxy.newProxyInstance(SubjectA.class.getClassLoader(), new Class[]{SubjectA.class}, invocationHandler); String res = proxyA.proxyMethod2("Hi "); System.out.println(res); invocationHandler = new DynamicProxyHandler(subjectB); SubjectB proxyB = null; proxyB = (SubjectB)Proxy.newProxyInstance(SubjectB.class.getClassLoader(), new Class[]{SubjectB.class}, invocationHandler); res = proxyB.proxyMethod1("Hi "); System.out.println(res); } }
15.5 代理模式优/缺点
代理模式使用场景:Java RMI (远程代理), Spring AOP(动态代理)
代理模式的优点主要如下:
- 客户端可以针对抽象层编程,增加和更换代理类不需要修改原代码,符合开闭原则
- 远程代理为位于不同地址空间对象的访问提供实现机制,将耗时耗资源的操作移至性能更好的远程主机,提高系统的运行效率
- 动态代理让系统根据需求动态创建代理类,且可以让同一个代理类代理多种不同的方法,相较于静态代理,显著降低了系统中类的个数
代理模式的缺点主要如下:
- 一些代理模式可能会需要复杂的实现过程,如远程代理
- 一些代理模式可能会造成请求的处理速度变慢,如保护代理