设计模式--代理模式

目录

本文的结构如下:

  • 引言
  • 什么是代理模式
  • 模式的结构
  • 典型代码
  • 代理模式分类
  • 代码示例
  • 代理模式和装饰者模式的区别
  • 静态代理和动态代理的区别
  • 优点和缺点
  • 适用环境
  • 模式应用

一、引言

说到明星,大家都知道一般明星都有经纪人,很多时候要拍个广告,参加个节目,都不是直接联系明星本人,都是先打电话到经纪人这里,然后再由经纪人转告给某个明星,这里面,经纪人起到了一个代理的作用;还有平常说的租房中介,也替房主起了代理的作用......

在软件开发中,也有这种类似的情况。由于某些原因,客户端不想或不能直接访问一个对象,此时可以通过一个称之为“代理”的第三者(就像经纪人和中介)来实现间接访问,这就是代理模式。

二、什么是代理模式

代理模式使用代理对象完成用户请求,屏蔽用户对真实对象的访问。现实世界的代理人被授权执行当事人的一些事宜,无需当事人出面,从第三方的角度看,似乎当事人并不存在,因为他只和代理人通信。而事实上代理人是要有当事人的授权,并且在核心问题上还需要请示当事人。

代理模式的定义:

代理模式(Proxy Pattern):给某一个对象提供一个代理或占位符,并由代理对象来控制对原对象的访问。代理模式是一种对象结构型模式。

在软件设计中,使用代理模式的意图也很多,比如因为安全原因需要屏蔽客户端直接访问真实对象,或者在远程调用中需要使用代理类处理远程方法调用的技术细节 (如 RMI),也可能为了提升系统性能,对真实对象进行封装,从而达到延迟加载的目的。

三、模式的结构

代理模式的UML类图如下:

20171130_proxy01.png

代理模式的结构比较简单,包含如下三个角色:

  • Subject(抽象主题角色):它声明了真实主题和代理主题的共同接口,这样一来在任何使用真实主题的地方都可以使用代理主题,客户端通常需要针对抽象主题角色进行编程。
  • Proxy(代理主题角色):它包含了对真实主题的引用,从而可以在任何时候操作真实主题对象;在代理主题角色中提供一个与真实主题角色相同的接口,以便在任何时候都可以替代真实主题;代理主题角色还可以控制对真实主题的使用,负责在需要的时候创建和删除真实主题对象,并对真实主题对象的使用加以约束。通常,在代理主题角色中,客户端在调用所引用的真实主题操作之前或之后还需要执行其他操作,而不仅仅是单纯调用真实主题对象中的操作。
  • RealSubject(真实主题角色):它定义了代理角色所代表的真实对象,在真实主题角色中实现了真实的业务操作,客户端可以通过代理主题角色间接调用真实主题角色中定义的操作。

四、典型代码

代理模式的结构图比较简单,但是在真实的使用和实现过程中要复杂很多,而且它有很多变化。

抽象主题类声明了真实主题类和代理类的公共方法,它可以是接口、抽象类或具体类,客户端针对抽象主题类编程,一致性地对待真实主题和代理主题,典型的抽象主题类代码如下:

public interface Subject {
    void request();
}

真实主题代码:

public class RealSubject implements Subject {
    public void request() {
        // todo
    }
}

代理类代码:

public class Proxy implements Subject {
    private RealSubject realSubject = new RealSubject();//真实主题的引用

    public void request() {
        postRequest();
        realSubject.request();
        postRequest();
    }

    public void preRequest(){
        //todo
    }

    public void postRequest(){
        //todo
    }
}

五、代理模式分类

在实际开发过程中,代理类的实现比上述代码要复杂很多,代理模式根据其目的和实现方式不同可分为很多种类,其中常用的几种代理模式简要说明如下:

  • 远程代理(Remote Proxy):为一个位于不同的地址空间的对象提供一个本地的代理对象,这个不同的地址空间可以是在同一台主机中,也可是在另一台主机中,远程代理又称为大使(Ambassador)。
  • 虚拟代理(Virtual Proxy):如果需要创建一个资源消耗较大的对象,先创建一个消耗相对较小的对象来表示,真实对象只在需要时才会被真正创建。
  • 保护代理(Protect Proxy):控制对一个对象的访问,可以给不同的用户提供不同级别的使用权限。
  • 缓冲代理(Cache Proxy):为某一个目标操作的结果提供临时的存储空间,以便多个客户端可以共享这些结果。
  • 智能引用代理(Smart Reference Proxy):当一个对象被引用时,提供一些额外的操作,例如将对象被调用的次数记录下来等。

六、代码示例

6.1、远程代理

说到远程代理,就需要先了解RMI(Remote Method Invocation),如果有不知道的,可以查下资料了解下。这里也顺便帖一个链接,关于简单介绍RMI实现原理的。

stub 和 skeleton 的讲解,自己实现一个stub和skeleton程序

简单说,RMI是用Java所特有的分布式计算技术,它允许运行在一个Java虚拟机上的对象调用运行在另一个Java虚拟机上的对象的方法,从而使Java编程人员可以方便地在网络环境中作分布式计算。

利用RMI进行编程就是一个典型的远程代理,这里套用网络上的一张图来大概说明其原理:

20171130_proxy02.png

Stub是“桩”也有称之为“存根”,代表了Server对象。
Skeleton是“骨架”,代表了Client。
Stub明明在客户那边,为什么不是客户的代理而是服务的代理?因为客户是要与服务器交互,现在服务在远程JVM中,无法交互,所以用Stub来代表Server,调用Stub就等同于调用Server(内部通信机制对Client透明,对Client来说,调用Stub和直接调用Server没什么区别,而这正是代理模式的优点之一)。

具体流程是这样的:

  1. Client向Stub发送方法调用请求(Client以为Stub就是Server);
  2. Stub接到请求,通过Socket与服务端的Skeleton通信,把调用请求传递给Skeleton;
  3. Skeleton接到请求,调用本地Server;
  4. Server作出对应动作,把结果返回给调用者Skeleton;
  5. Skeleton接到结果之后通过Socket发送给Stub;
  6. Stub把结果传递给Client。

这里2和5需要通过Socket通信。

PS:这里有必要说一下,在jdk1.2之后,Skeleton就被合并到server中了,但大致的原理还是不变的,至于Stub是怎么出现的,其实是在服务端生成的,并且注册到注册中心的,然后客户端向注册中心请求拿到的。

好了,可以跳过这些原理性的东西,直接使用RMI,网上有很多例子,直接使用应该还是比较简单的。

-----------------------------------------------------------------------------------------------------------------------------------万能的分割线-----------------------------------------------------------------------------------------------------------------------------------

下面直接上例子,假如你在不同地方开了几家超市,想要在家对超市的库存远程监控,可以这样实现:

(1) 先定义一个远程接口,远程接口必须继承Remote接口,声明的方法需要抛出RemoteException,因为客户端通过远程接口调用服务端服务,需要通过网络io,可能会有各种坏事发生,同时也要注意变量和返回值应该是基本类型或者可序列化的:

public interface MarketRemote extends Remote{
    int inventory() throws RemoteException;
    String location() throws RemoteException;
}

(2) 接着在服务端实现远程接口,这里是真正的业务方法,这里需要扩展自UnicastRemoteObject,继承UnicastRemoteObject这个类需要在构造器中抛出RemoteException:

public class MarketRemoteImpl extends UnicastRemoteObject implements MarketRemote {
    private String location;
    private int count;

    public MarketRemoteImpl(String location, int count) throws RemoteException {
        this.count = count;
        this.location = location;
    }

    public int inventory() throws RemoteException {
        return count;
    }

    public String location() throws RemoteException {
        return location;
    }
}

(3) 启动 RMI 注册表:

注册表就像一个电话簿,启动后即可将提供的服务注册到其中,客户可以通过它查询到服务来进行调用。

启动注册表有两种方法,一种是通过命令行rmiregistry来启动,另一种方式是通过LocateRegistry.createRegistry(int port)方法。

(4) 下面是服务器端绑定的代码,即服务器那边将自己的信息注册以便能提供服务:

public class MarketTestDriver {
    public static void main(String[] args) {
        MarketRemote market = null;
        int count = 0;
        if (args.length < 2){
            System.out.println("...");
            System.exit(1);
        }

        try {
            count = Integer.parseInt(args[1]);
            market = new MarketRemoteImpl(args[0], count);

            String name = "rmi." + args[0] + ".market";
            Registry rr = LocateRegistry.createRegistry(1234); //创建1234端口上的对象注册表,如果已经创建了就用getRegistry方法获取
            rr.bind(name, market);//向注册表中注册对象
        } catch (RemoteException e) {
            e.printStackTrace();
        }catch (AlreadyBoundException e) {
            e.printStackTrace();
        }
    }
}

(5) 客户端监视器代码:

public class MarketMonitor{
    MarketRemote marketRemote;

    public MarketMonitor(MarketRemote marketRemote){
        this.marketRemote = marketRemote;
    }

    public void report() throws RemoteException {
        System.out.println("market location: " + marketRemote.location());
        System.out.println("market inventory: " + marketRemote.inventory());
    }
}

(6) 客户端测试代码:

public class MonitorTestDriver {
    public static void main(String[] args) throws RemoteException, NotBoundException {
        String[] locations = { "rmi.localhost.market" };
        MarketMonitor[] monitors  = new MarketMonitor[locations.length];
        // 获取注册表
        Registry registry = LocateRegistry.getRegistry("localhost", 1234);
        for (int i=0; i<locations.length; i++){
            MarketRemote market = (MarketRemote) registry.lookup(locations[i]);
            monitors[i] = new MarketMonitor(market);
        }

        for (MarketMonitor monitor : monitors){
            monitor.report();
        }
    }
}

也许看到这里会疑惑,这里怎么是代理模式了?

还记得前面说过的Stub类吗?它才是真正的代理类,在JDK1.5后,Stub类是由JVM产生的,在运行rr.bind(name, market)时会被注册到服务中。

然后当客户端运行registry.lookup(locations[i])时,如果客户端不存在Stub类,那么客户端就去访问服务端的注册服务,注册服务通过查找注册列表,然后告诉它从从某个位置下载生产的Stub类。

MarketRemote market = (MarketRemote) registry.lookup(locations[i])得到的其实就是Stub,Stub是MarketRemoteImpl对象的代理对象,Stub也实现了MarketRemote接口。

当下载好Stub类后,Stub就开始和服务端的Skeleton 开始通信,marketRemote.location()就是Stub向Skeleton发出请求,Skeleton 接到请求后,去访问真正的服务端的MarketRemoteImpl执行location方法,然后返回值给Skeleton,Skeleton再返回给Stub,Stub再返回给客户端。

6.2、虚拟代理

虚拟代理(Virtual Proxy)也是一种常用的代理模式,对于一些占用系统资源较多或者加载时间较长的对象,可以给这些对象提供一个虚拟代理。在真实对象创建成功之前虚拟代理扮演真实对象的替身,而当真实对象创建之后,虚拟代理将用户的请求转发给真实对象。

通常,在以下两种情况下可以考虑使用虚拟代理:

(1) 由于对象本身的复杂性或者网络等原因导致一个对象需要较长的加载时间,此时可以用一个加载时间相对较短的代理对象来代表真实对象。通常在实现时可以结合多线程技术,一个线程用于显示代理对象,其他线程用于加载真实对象。这种虚拟代理模式可以应用在程序启动的时候,由于创建代理对象在时间和处理复杂度上要少于创建真实对象,因此,在程序启动时,可以用代理对象代替真实对象初始化,大大加速了系统的启动时间。当需要使用真实对象时,再通过代理对象来引用,而此时真实对象可能已经成功加载完毕,可以缩短用户的等待时间。

(2) 当一个对象的加载十分耗费系统资源的时候,也非常适合使用虚拟代理。虚拟代理可以让那些占用大量内存或处理起来非常复杂的对象推迟到使用它们的时候才创建,而在此之前用一个相对来说占用资源较少的代理对象来代表真实对象,再通过代理对象来引用真实对象。为了节省内存,在第一次引用真实对象时再创建对象,并且该对象可被多次重用,在以后每次访问时需要检测所需对象是否已经被创建,因此在访问该对象时需要进行存在性检测,这需要消耗一定的系统时间,但是可以节省内存空间,这是一种用时间换取空间的做法。

我们知道在查询数据前,需要获得数据库连接,软件启动时初始化系统的所有类,此时尝试获得数据库连接,当系统有大量的类似“XML解析等”操作存在时,所有这些初始化操作的叠加会使得系统的启动速度变得非常缓慢。这时可以使用代理模式的代理类封装对数据库查询中的初始化操作,当系统启动时,初始化这个代理类,而非真实的数据库查询类,而代理类什么都没有做,进而加快软件启动时间。

public interface DBQuery {
    void connect();
}

public class DBQueryImpl implements DBQuery {
    public DBQueryImpl(){
        try {
            Thread.sleep(1000);//假设数据库连接等耗时操作
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public void connect() {
        //todo
    }
}

public class DBQueryProxy implements DBQuery {
    private DBQueryImpl real;
    boolean retrieving = false;
    Thread retrievalThread;

    public void connect() {
        if (real !=null){
            real.connect();
        }else {
            if (!retrieving){
                retrieving = true;
                retrievalThread = new Thread(new Runnable() {
                    public void run() {
                        real = new DBQueryImpl();
                    }
                });
                retrievalThread.start();
            }
        }
    }
}

6.3、保护代理

保护代理控制对一个对象的访问,可以给不同的用户提供不同级别的使用权限。

这里假设一个服务者对象,它有一个salary字段,代表薪水,也有一个得分score字段,代表顾客的评分。显然是否薪水是由老板决定的,而评分则需顾客给出。

这要怎么实现呢?我们可以给服务者对象创建两个代理对象,一个BossWaiterProxy,交由Boss访问;一个CustomerWatierProxy,交由Customer访问,这是可以的。但如果换一个服务者呢?也需要创建几个不同的代理,存在重复代码,这时可以用JDK自带的动态代理技术实现。

先有WatierBean:

public interface WatierBean {
    void setSalary(double salary);
    void setScore(int score);
}

public class WaiterBeanImpl implements WaiterBean {
    private double salary;
    private int score;
    private String name = "小黄";

    public double getSalary() {
        return salary;
    }

    public int getScore() {
        return score;
    }

    public void setSalary(double salary) {
        this.salary = salary;
    }

    public void setScore(int score) {
        this.score = score;
    }

    public String getName() {
        return name;
    }
}

public class BossInvocationHandler implements InvocationHandler {
    private WaiterBean waiterBean;

    public BossInvocationHandler(WaiterBean waiterBean){
        this.waiterBean = waiterBean;
    }

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        if (method.getName().equals("setSalary")){
            return method.invoke(waiterBean, args);//boss可以加薪
        }else if (method.getName().equals("setScore")){
            throw new IllegalAccessException();//boss不能评分
        }
        return null;//其他方法一律不理
    }
}

public class CustomerInvocationHandler implements InvocationHandler {
    private WaiterBean waiterBean;

    public CustomerInvocationHandler(WaiterBean waiterBean){
        this.waiterBean = waiterBean;
    }

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        if (method.getName().equals("setScore")){
            return method.invoke(waiterBean, args);//customer可以评分
        }else if (method.getName().equals("setSalary")){
            throw new IllegalAccessException();//customer不可以加薪
        }
        return null;//其他方法一律不理
    }
}

public class WaiterTestDrive {

    public static void main(String[] args) {
        WaiterTestDrive drive = new WaiterTestDrive();
        drive.drive();
    }

    public void drive(){
        WaiterBean waiter = new WaiterBeanImpl();

        WaiterBean bossProxy = getBossProxy(waiter);
        bossProxy.setSalary(2000);
        try{
            bossProxy.setScore(0);
        }catch (Exception e){
            System.out.println("boss不能打分");
        }

        WaiterBean customerProxy = getCustomerProxy(waiter);
        customerProxy.setScore(99999);
        try{
            customerProxy.setSalary(99999999);
        }catch (Exception e){
            System.out.println("customer不能涨薪");
        }
    }

    public WaiterBean getBossProxy(WaiterBean waiter){
        return (WaiterBean) Proxy.newProxyInstance(waiter.getClass().getClassLoader(),
                waiter.getClass().getInterfaces(), new BossInvocationHandler(waiter));
    }

    public WaiterBean getCustomerProxy(WaiterBean waiter){
        return (WaiterBean) Proxy.newProxyInstance(waiter.getClass().getClassLoader(),
                waiter.getClass().getInterfaces(), new CustomerInvocationHandler(waiter));
    }
}

6.4、缓冲代理

缓冲代理(Cache Proxy)也是一种较为常用的代理模式,它为某一个操作的结果提供临时的缓存存储空间,以便在后续使用中能够共享这些结果,从而可以避免某些方法的重复执行,优化系统性能。

代码略。

6.5、智能引用代理

当一个对象被引用时,提供一些额外的操作,例如将对象被调用的次数记录下来等。

public interface Searcher {
    String doSearch(String userId);
}

public class RealSearcher implements Searcher {
    public RealSearcher() {
    }

    public String doSearch(String userId) {
        return "result for " + userId;
    }
}

public class Proxy implements Searcher {
    private RealSearcher searcher;
    private AccessValidator accessValidator;

    public Proxy() {
        searcher = new RealSearcher();
    }

    public String doSearch(String userId) {
        if (checkAccess(userId)) {
            String result = searcher.doSearch(userId);
            logUsage(userId);
            return result;
        } else {
            System.out.println("你没权限哦");
            return "none";
        }
    }

    private boolean checkAccess(String userId) {
        accessValidator = new AccessValidator();
        return accessValidator.vaidateUser(userId);
    }

    private void logUsage(String userId) {
        UsageLogger logger = new UsageLogger();
        logger.setUserId(userId);
        logger.save();
    }
}

public class AccessValidator {
    public boolean vaidateUser(String userId) {
        if (userId.equals("Admin")) {
            System.out.println("user:" + userId + " is OK");
            return true;
        } else {
            return false;
        }
    }
}

public class UsageLogger {
    private String userId;

    public void setUserId(String userId) {
        this.userId = userId;
    }

    public void save() {
        System.out.println("Save user:" + userId + " doSearch");
    }

    public void save(String userId) {
        this.userId = userId;
        save();
    }
}

public class SearchTestDrive {
    public static void main(String[] args) {
        Searcher searcher = new Proxy();
        String[] userIds = {"Admin", "admin"};
        for (String userId : userIds){
            System.out.println(searcher.doSearch(userId));
        }
    }
}

七、代理模式和装饰者模式的区别

看了代理模式的类图,相信会发现代理模式和装饰者模式类图很像。对装饰器模式来说,装饰者(decorator)和被装饰者(decoratee)都实现同一个 接口。对代理模式来说,代理类(proxy class)和真实处理的类(real class)都实现同一个接口。

那它们之间有什么区别呢?

首先代理模式和装饰者模式的意图是不一样的。装饰器模式关注于在一个对象上动态的添加方法,然而代理模式关注于控制对对象的访问。换句话说,用代理模式,代理类(proxy class)可以对它的客户隐藏一个对象的具体信息。

其次,当使用代理模式的时候,常常在一个代理类中创建一个对象的实例。而使用装饰器模式的时候,通常的做法是将原始对象作为一个参数传给装饰者的构造器。

使用代理模式,代理和真实对象之间的的关系通常在编译时就已经确定了,而装饰者能够在运行时递归地被构造。

八、静态代理和动态代理的区别

可以参考这篇文章:

java静态代理与动态代理的区别

九、优点和缺点

9.1、优点

代理模式的共同优点如下:

  • 能够协调调用者和被调用者,在一定程度上降低了系统的耦合度。
  • 客户端可以针对抽象主题角色进行编程,增加和更换代理类无须修改源代码,符合开闭原则,系统具有较好的灵活性和可扩展性。

此外,不同类型的代理模式也具有独特的优点,例如:

  1. 远程代理为位于两个不同地址空间对象的访问提供了一种实现机制,可以将一些消耗资源较多的对象和操作移至性能更好的计算机上,提高系统的整体运行效率。
  2. 虚拟代理通过一个消耗资源较少的对象来代表一个消耗资源较多的对象,可以在一定程度上节省系统的运行开销。
  3. 缓冲代理为某一个操作的结果提供临时的缓存存储空间,以便在后续使用中能够共享这些结果,优化系统性能,缩短执行时间。
  4. 保护代理可以控制对一个对象的访问权限,为不同用户提供不同级别的使用权限。

9.2、缺点

代理模式的主要缺点如下:

  • 由于在客户端和真实主题之间增加了代理对象,因此有些类型的代理模式可能会造成请求的处理速度变慢,例如保护代理。
  • 实现代理模式需要额外的工作,而且有些代理模式的实现过程较为复杂,例如远程代理。
  • 代理对象只服务于一种类型的对象,如果要服务多类型的对象。势必要为每一种对象都进行代理,静态代理在程序规模稍大时就无法胜任了。可以通过动态代理改进。

十、适用环境

代理模式的类型较多,不同类型的代理模式有不同的优缺点,它们应用于不同的场合:

  • 当客户端对象需要访问远程主机中的对象时可以使用远程代理。
  • 当需要用一个消耗资源较少的对象来代表一个消耗资源较多的对象,从而降低系统开销、缩短运行时间时可以使用虚拟代理,例如一个对象需要很长时间才能完成加载时。
  • 当需要为某一个被频繁访问的操作结果提供一个临时存储空间,以供多个客户端共享访问这些结果时可以使用缓冲代理。通过使用缓冲代理,系统无须在客户端每一次访问时都重新执行操作,只需直接从临时缓冲区获取操作结果即可。
  • 当需要控制对一个对象的访问,为不同用户提供不同级别的访问权限时可以使用保护代理。
  • 当需要为一个对象的访问(引用)提供一些额外的操作时可以使用智能引用代理。

十一、模式应用

Spring在JAVA开发中占有举足轻重的地方,其中的AOP是一大功能特色,Spring aop的实现就用到了代理模式。感兴趣的可以去了解一下。