现在朋友圈有好多做香港代购的微商,大部分网民无法自己去香港购买想要的商品,于是委托这些微商,告诉他们想要的商品,让他们帮我们购买。我们只需要付钱给他们,他们就会去香港购买,然后把商品寄给我们。这就是一种代理模式。
1 代理模式概述
引入一个新的代理对象,在客户端对象和目标对象之间起到中介的作用,去掉客户不能看到的内容和服务或者增添客户想要的额外服务。
图 代理模式结构图
- Subject:抽象主题角色。声明了真实主题和代理主题的共同接口,使得在任何使用真实主题的地方都可以使用代理主题,客户端通常需要针对抽象主题进行编程。
- Proxy:代理主题角色。包含了对真实主题的引入,从而可以在任何时候操作真实主题对象。通常,在代理主题角色中,客户端在调用所引用的真实主题操作之前或之后还需要执行其他操作,而不仅是单纯调用真实主题对象中的操作。
- RealSubject:真实主题角色。实现了真实的业务操作,客户端可以通过代理主题角色间接调用真实主题角色中定义的操作。
public interface Subject {
void buy(String goodsName);
}
public class RealSubject implements Subject{
@Override
public void buy(String goodsName) {
System.out.println("购买:" + goodsName);
}
}
public class ProxyBuy implements Subject {
private final RealSubject realSubject = new RealSubject();
private void goHongKong() {
System.out.println("前往香港");
}
@Override
public void buy(String goodsName) {
goHongKong();
realSubject.buy(goodsName);
sendGoods();
}
private void sendGoods() {
System.out.println("把商品寄送給客户");
}
}
public class Client {
public static void main(String[] args) {
Subject subject = new ProxyBuy();
subject.buy("奶粉");
System.out.println("----------");
subject.buy("iphone");
// 运行结果:
// 前往香港
// 购买:奶粉
// 把商品寄送給客户
// ----------
// 前往香港
// 购买:iphone
// 把商品寄送給客户
}
}
在实际开发中,代理类的实现比上述代码要复杂得到。代理模式根据其目的和实现方式的不同可分为很多类。
远程代理 | 为一个位于不同的地址空间的对象提供一个本地的代理对象。 |
虚拟代理 | 如果需要创建一个资源消耗较大的对象,先创建一个消耗相对较小的对象来表示,真实对象只在需要时才会被真正创建。 |
保护代理 | 控制对一个对象的访问,可以给不同的用户提供不同级别的使用权限。 |
缓存代理 | 为某一个目标操作的结果提供临时的存储空间,以便多个客户端可以共享这些结果。 |
智能引用代理 | 当一个对象被引用时,提供一些额外的操作。例如将对象被调用的次数记录下来。 |
表 常用的5种代理模式
1.1 虚拟代理
对于一些占用系统资源较多或者加载时间较长的对象,可以给对象提供一个虚拟代理。在真实对象创建成功之前虚拟代理扮演真实对象的替身,而当真实对象创建之后,虚拟代理将用户的请求转发给真实对象。
1.1.1 适用场景
1) 由于对象本身的复杂性或者网络等原因导致一个对象需要较长的加载时间,此时可以用一个加载时间较短的代理对象来代表真实对象。在程序启动时,可以用代理对象代替真实对象初始化,大大加速系统的启动时间。
2) 当一个对象的加载是否耗费系统资源时。虚拟代理可以让那些占用大量内存或者处理起来非常复杂的对象推迟到使用它们的时候才场景,而在之前用一个相对来说占用资源较少的代理对象来代表真实对象,再通过代理对象来引用真实对象。
需求:在面试的时候,一般都最少有两轮面试,一面由项目经理负责,二面由技术总监面。而最终的录取结果是由技术总监决定的。(技术总监工资更高,如果每个面试者都由总监面,那么将会花费总监很多时间,进而给公司带来更多的花费)。
分析:技术总监属于占用资源较多的对象,不能频繁的调用。而产品经理占用的资源相对较小。在这个需求中,虚拟代理为产品经理。先一轮面试,把筛选后的名单给技术总监。
图 需求实现结构图
public interface Interviewer {
boolean audition(String name, boolean isLastOne);
}
public class TechnicalDirector implements Interviewer{
@Override
public boolean audition(String name, boolean isLastOne) {
Random random = new Random();
return random.nextInt() % 3 == 0;
}
}
public class ProjectManager implements Interviewer{
private final List<String> passList = new ArrayList<>();
@Override
public boolean audition(String name, boolean isLastOne) {
Random random = new Random();
boolean pass = random.nextInt() % 5 == 0;
if (pass) passList.add(name);
if (isLastOne) cassTechnicalDirector();
return pass;
}
/**
* 一轮面试完成后,再叫技术总监来面试
*/
private void cassTechnicalDirector(){
TechnicalDirector technicalDirector = new TechnicalDirector();
Iterator<String> iterator = passList.iterator();
List<String> tempList = new ArrayList<>();
while (iterator.hasNext()) {
String next = iterator.next();
if (technicalDirector.audition(next, !iterator.hasNext())) {
tempList.add(next);
}
}
if (tempList.size() == 0) {
System.out.println("没人通过面试");
} else {
System.out.println("以下人员通过面试:");
System.out.println(tempList);
}
}
}
public class Client {
public static void main(String[] args) {
Interviewer interviewer = new ProjectManager();
for (int i = 0; i < 5000; i++) {
interviewer.audition("路人" + i, i == 19);
}
// 运行结果:
// 以下人员通过面试:
// [路人5, 路人14, 路人16]
}
}
1.2 Java动态代理
在传统的代理模式中,客户端通过Proxy类调用RealSubject类的request()方法,同时可以在代理类中封装其他方法。而代理类和真实主题类都应该是事先已经存在的。如果需要为不同的真实主题类提供代理类,或者代理一个真实主题类中不同的方法,都需要增加新的代理类,这将导致系统的类格式急剧增加。
动态代理可以让系统能够根据实际需要来动态创建代理类,让同一个代理类能够代理多个不同的真实主题类,而且可以代理不同的方法。
从JDK1.3开始,Java提供了对动态代理的支持。
1.2.1 InvocationHandler接口
是代理处理程序类的实现接口。作为代理实例的调用处理者的公共父类,该接口只声明了一个方法:
invoke(proxy,method,args): 用于处理对代理实例的方法调用并返回相应结果。第1个参数表示代理类的实例,第2个参数表示需要代理的方法,第3个表示代理方法的参数数组。
1.2.2 Proxy类
图 Proxy类的部分方法
最常用的方法是:
- newProxyInstance(classLoader,interfaces,invocationHandler), 返回一个动态创建的代理实例,第1个参数表示代理类的类加载器,第2个参数表示代理类所实现的接口列表(与真实主题类的接口列表一致),第3个参数表示所指派的调用处理程序类。
- getProxyClass(classLoader,class[]),返回一个Class类型的代理类,第1个参数是代理类的类加载器,第2个参数表示代理类所实现的接口列表。
1.2.3 动态代理实战
创建动态代理的步骤如下:
1)和静态代理相同,首先定义一个抽象主机角色,来定义代理类和真实主题类的真实接口。
public interface BuyTicket {
boolean buyTicket(String start, String end);
}
2)创建被代理的类
public class Customer implements BuyTicket{
@Override
public boolean buyTicket(String start, String end) {
System.out.println("购买" + start + "到" + end + "的火车票");
Random random = new Random();
return random.nextInt() % 2 == 0;
}
}
3)创建InvocationHandler类
public class BuyTicketInvocationHandler implements InvocationHandler {
private final BuyTicket buyTicket;
public BuyTicketInvocationHandler(BuyTicket buyTicket) {
this.buyTicket = buyTicket;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
int num = 0; // 最多尝试次数
boolean res = false;
while (++num < 10 && !res) {
System.out.println("黄牛党第" + num + "次操作");
res = (boolean)method.invoke(buyTicket, args);
}
return res;
}
}
4)创建动态代理对象
public class Client {
public static void main(String[] args) {
BuyTicket buyTicket = new Customer();
BuyTicketInvocationHandler invocationHandler = new BuyTicketInvocationHandler(buyTicket);
BuyTicket buyTicket1 = (BuyTicket) Proxy.newProxyInstance(BuyTicket.class.getClassLoader(), new Class<?>[]{BuyTicket.class}, invocationHandler);
boolean res = buyTicket1.buyTicket("深圳", "赣州");
if (res) System.out.println("购票成功");
else System.out.println("购票失败");
// 运行结果:
// 黄牛党第1次操作
// 购买深圳到赣州的火车票
// 黄牛党第2次操作
// 购买深圳到赣州的火车票
// 购票成功
}
}
2 远程代理
使得客户端可以访问远程主机(或另一个虚拟机)上的对象。远程主机可能具有更好的计算性能与处理速度,可以快速响应并处理客户端请求(或者是客户端不能直接访问远程主机中的业务对象)。
远程代理可以将网络的细节隐藏起来,使得客户端不必考虑网络的存在。客户端完全可以认为被代理的远程对象是局域的而不是远程的,而远程代理对象承担了大部分的网络通信工作,并负责对远程业务方法的调用。
图 远程代理示意图
客户端无需关心实现具体业务的是谁,只需要按照服务接口所定义的方式直接与本地主机的代理对象交互即可。
2.1 RMI
Remote Method Invocation,远程方法调用。Java通过这个机制实现远程代理,实现一个Java虚拟机中的对象调用另一个Java虚拟机中对象的方法。
客户端通过一个桩(Stub)(相当于代理类)对象与远程主机上的业务对象进行通信。而远程主机端有个Skeleton(骨架)对象来负责与Stub对象通信。RMI基本步骤如下:
图 RMI基本步骤
- 客户端发起请求,将请求转交至RMI客户端的Stub类。
- Stub类将请求的接口、方法、参数等信息进行序列化。
- 将序列化后的流使用Socket传输到服务端。
- 服务端将接收到的流转发至相应的Skeleton类。
- Skeleton类将信息反序列化后调用实际业务处理类。
- 业务处理类处理完毕后将结果返回给Skeleton。
- Skeleton将结果序列化,再通过Socket将流传送到客户端Stub。
- Stub将接收到的流进行反序列化,将反序列化后得到的结果返回给客户端调用者。
2.2 RMI 实战
实现RMI的调用分为两大步:1)发布RMI服务;2)调用RMI服务。
2.2.1 发布RMI服务
发布一个RMI服务只需要做三件事:
- 定义一个RMI接口。
- 编写RMI接口的实现类。
- 通过JNDI发布RMI服务。
// 必须继承java.rmi.Remote,此外每个接口必须声明抛出一个java.rmi.RemoteException 异常
public interface BuyService extends Remote {
String buyGoods(String goodsName) throws RemoteException;
}
// 必须继承java.rmi.server.UnicastRemoteObject, 而且必须提供一个构造器,并且构造器
// 必须抛出java.rmi.server.UnicastRemoteObject 异常
public class BuyServiceImpl extends UnicastRemoteObject implements BuyService{
protected BuyServiceImpl() throws RemoteException {
}
@Override
public String buyGoods(String goodsName) throws RemoteException {
return "远程购买:" + goodsName;
}
}
public class JNDIPublisher {
public static void main(String[] args) throws RemoteException, MalformedURLException {
BuyService buyService = new BuyServiceImpl();
int port = 8089; //rmi服务端口
String url = "rmi://localhost:" + port + "/BuyService"; //服务类的寻址符
LocateRegistry.createRegistry(port);
Naming.rebind(url,buyService);
}
}
2.2.2 调用RMI服务
在另外一个项目(主机)中调用上面已发布的RMI服务。在调用服务时只有做两件事:
1)查找服务类的接口。
Object obj = Naming.lookup(url); // url 为上面定义的服务类的寻址符。
2)调用这个接口的方法。
调用接口方法有两个方法,1)通过反射;2)通过导入服务端的接口类(一定要导入,重写无效)。
public class RMICaller {
public static void main(String[] args) {
String url = "rmi://localhost:8089/BuyService";
try {
Object lookup = Naming.lookup(url);
// 通过反射来调用方法
Method method = lookup.getClass().getMethod("buyGoods",String.class);
System.out.println(method.invoke(lookup, "奶粉"));;
// 运行结果:
// 远程购买:奶粉
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
3 代码模式优缺点
优点:
- 能协调调用者和被调用者,降低系统耦合度,符合迪米特法则。
- 客户端针对抽象主题角色进行编程,增加和更换代理类无须修改源代码,复合开闭原则。
- 远程代理为位于不同地址空间对象的访问提供了一种实现机制,可以将一些消耗资源较多的对象和操作移至性能更好的计算机上,提供系统的整体运行效率。
- 虚拟代理通过一个消耗资源较少的对象来代表一个消耗资源较多的对象,可以在一定程度上节省系统的运行开销。
- 保护代理可以控制对一个对象的访问权限,为不同用户提供不同级别的使用权限。
缺点:
- 增加了代理对象,可能会造成请求的处理速度变慢。
- 实现代理需要额外的工作,有些代理模式的实现非常复杂。
4 适用场景
- 客户端对象需要访问远程主机中的对象。
- 需要用一个消耗资源较少的对象来代表一个消耗资源较多的对象。例如一个对象需要很长时间才能完成加载时。
- 需要控制对一个对象的访问。
- 需要为某个被频繁访问的操作结果提供一个临时存储空间,以供对各客户端共享访问这些结果时。可以通过缓存代理。
- 需要为一个对象的访问提供一些额外的操作时。