美文网首页
设计模式之代理(Proxy)模式

设计模式之代理(Proxy)模式

作者: 纸中圆 | 来源:发表于2019-03-13 19:04 被阅读0次

什么是代理模式?

  代理模式(Proxy Pattern)是程序设计中的一种设计模式。所谓的代理者是指一个类别可以作为其它东西的接口。代理者可以作任何东西的接口:网络连接、存储器中的大对象、文件或其它昂贵或无法复制的资源。

  当一个复杂对象的多份副本须存在时,代理模式可以结合享元模式以减少存储器用量。典型作法是创建一个复杂对象及多个代理者,每个代理者会引用到原本的复杂对象。而作用在代理者的运算会转送到原本对象。一旦所有的代理者都不存在时,复杂对象会被移除。

  可以这样理解代理模式:使用代理模式创建代表对象,让代表对象控制某对象的访问,被代理的对象可以是需要安全控制的对象、远程的对象和创建开销大的对象

  代理主要分为三种:保护代理、远程代理、虚拟代理。其中:

  • 保护代理基于权限控制对资源的访问
  • 远程代理控制访问远程对象。
  • 虚拟代理控制访问创建开销大的资源

保护代理

  Java在java.lang.reflect包中有自己的代理支持,利用这个包你可以在运行时动态地创建一个代理类,实现一个或多个接口,并将方法的调用转发到你所指定的类。因为实际的代理类实在运行时创建的,我们称这个Java技术为:动态代理
  我们要利用Java的动态代理创建我们的下一个代理实现(保护代理)。但在这之前,先看一下类图,了解一下动态代理是怎么一回事,它和代理模式的传统定义有一点出入。

  因为Java已经为你创建了Proxy类,所以你需要有办法来告诉Proxy类你要做什么。你不能像以前一样把代码放在Proxy类中,因为Proxy类不是你直接实现的。既然这样的代码不能放在Proxy类中,那么要放在哪里?放在InvocationHandler接口的实现类中。InvocationHandler是一个接口,里面只有一个invoke()方法,InvocationHandler接口实现类的工作是响应代理的任何调用。下面是InvocationHandler接口代码:

public interface InvocationHandler {
   public Object invoke(Object proxy, Method method, Object[] args)
        throws Throwable;
}

你可以把InvocationHandler接口实现类想成是代理收到方法调用后,请求做实际工作的对象。

接下来,看看如何使用动态代理......

例子:过年相亲。。。

  一到大过年的时候,俊男美女李狗蛋张翠花都往村里跑,七大姑八大姨纷纷开始当起了媒人。。。假如你就是这些媒人中的一个,你负责帮狗蛋他们实现相亲服务系统。你有一个好点子,就是在服务系统中加入评分机制(这里只是一个简单实现,你可以理解为身高财富颜值的综合分数,也不要纠结它是十分制还是百分制),你希望这套系统能帮助你的顾客找到可能的相亲对象。

你的服务系统涉及到一个Person接口,允许设置或取得一个人的信息:

public interface Person {
    String getName();
    String getSex();
    String getInterests();
    int getScore();

    void setName(String name);
    void setSex(String sex);
    void setInterests(String interests);
    void setScore(int score);
}

在让PersonImpl类实现该接口:

public class PersonImpl implements Person {
    private String name;
    private String sex;
    private String interests;
    //评分
    private int score;
    //评分人数
    private int scoreCount = 0;

    @Override
    public String getName() {
        return name;
    }

    @Override
    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String getSex() {
        return sex;
    }

    @Override
    public void setSex(String sex) {
        this.sex = sex;
    }

    @Override
    public String getInterests() {
        return interests;
    }

    @Override
    public void setInterests(String interests) {
        this.interests = interests;
    }

    @Override
    public int getScore() {
        if (scoreCount == 0) return 0;
        return (score / scoreCount);
    }

    @Override
    public void setScore(int score) {
        this.score += score;
        scoreCount++;
    }
}

  李狗蛋用过这个系统之后就说:“我单身狗当了二十多年,一直以为是我的原因,后来我发现原来有人篡改过我的颜值信息,我很帅的好嘛!我还发现居然有人给自己评高分来拉高自己的score!我觉得系统不应该允许用户篡改别人的颜值信息,也不应该允许用户给自己打分数。”

我李狗蛋不服

  虽然我们怀疑李狗蛋找不到女朋友可能是其他的原因,但是说的没错,系统不应该允许用户篡改别人的数据。可是根据我们定义的Person,任何客户都可以调用任何方法。

  这是一个我们可以使用保护代理的绝佳例子。什么是保护代理?这是一种根据访问权限决定客户可否访问对象的代理。比方说,你是老板,手下很多员工,保护代理允许普通员工调用对象上的某些方法,经理还可以多调用一些其他的方法,而人力资源处的雇员可以调用对象上的所有方法。

  在我们的服务中,我们希望用户可以设置自己的信息,同时又防止他人更改这些信息。评分机制则相反,你不能更改自己的评分,但是他人可以设置你的评分。我们在Person中已经有许多getter方法了,每个方法的返回信息都是公开的,任何用户都可以调用他们。

  因此,我们有一些问题要修正:用户不可以设置自己的评分,也不可以改变其他用户的个人信息。要修正这些问题,你必须创建两个代理:一个访问你自己的Person对象,另一个访问另一个用户的Person对象。这样,代理就可以控制在每一种情况下允许哪一种请求。

  创建这种代理,我们必须使用Java API的动态代理。Java会为我们创建两个代理,我们只需要提供handler来处理代理转来的方法。

步骤一:创建两个实现了InvocationHandler接口的类

  我们知道需要写两个实现了InvocationHandler(d调用处理器)接口的类,其中一个给拥有者使用,另一个给非拥有者使用。究竟什么是InvocationHandler呢?你可以这么想:当代理的方法被调用时,代理就会把这个调用转发给InvocationHandler的实现类,该类的invoke()方法一定会被调用。InvocationHandler的接口代码如下:

public interface InvocationHandler {
   public Object invoke(Object proxy, Method method, Object[] args)
        throws Throwable;
}

  这里只有一个名为invoke()的方法,不管代理被调用的是何种方法,处理器被调用的一定是invoke()方法。让我们看看这是如何工作的。


现在我们开始创建客户本人调用处理器和他人调用处理器(这不是代理类!!!)。

  当proxy调用invoke时,要如何做?通常,会先检查该方法是否来自proxy,并基于该方法的名称和变量做决定。现在我们来实现OwnInvocationHandler类:

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

//客户本人的调用处理器
public class OwnInvocationHandler implements InvocationHandler {

    private Person person;

    public OwnInvocationHandler(Person person) {
        //将person传入构造器,并保持它的引用
        this.person = person;
    }

    //每次proxy方法被调用,都会调用该方法
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws IllegalAccessException {

        try {
            //如果方法名首个单词为get,可以执行该方法,客户当然可以查看自己的任何信息
            if (method.getName().startsWith("get")) {
                return method.invoke(person, args);
            } //否则如果方法名为setScore,则抛出一个异常,代表客户不能给自己打分
            else if (method.getName().equals("setScore")) {
                throw new IllegalAccessException();
            } //如果方法名首个单词为set,可以执行该方法。客户可以执行除了给自己打分外其他所有的设置自身的方法
            else if (method.getName().startsWith("set")) {
                return method.invoke(person, args);
            }
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
        //如果调用其他的方法,一律返回null
        return null;
    }
}

  对于他人代理类,跟上面的代码异曲同工,只需要把setScore()方法和get的方法对他人开放,其他set方法抛异常即可,这里不做演示,读者可以自己实现。

步骤二:利用代理(Proxy)类并实例化代理对象

  现在,只剩下创建动态Proxy类,并实例化Proxy对象了。让我们开始编写一个以Person为参数,并知道如何为Person对象创建客户本人对象代理的方法。也就是说,我们要创建一个代理,将它的方法调用转发再给OwnInvocationHandler接口实现类。代码如下:

import java.lang.reflect.Proxy;

public class OwnProxy {
    private Person person;

    public OwnProxy(Person person) {
        this.person = person;
    }

    //获得客户本人的代理
    public  Person getOwnProxy(){
        return (Person)Proxy.newProxyInstance(
                person.getClass().getClassLoader(),//获取类加载器
                person.getClass().getInterfaces(),//获取类的接口
                new OwnInvocationHandler(person));//获取调用处理器
    }
}

OwnProxy类中只有一个方法,那就是获得客户本人的代理对象。其中,Proxy.newProxyInstance()的方法参数:第一个参数需要Person对象的类载入器,因为代理类是在运行期才被创建出来的,我们需要将它加入JVM内存创建该对象;第二个参数需要Person对象的接口,因为耦合性等一系列复杂原因被设计成这样;第三个就是调用处理集啦,因为代理类会将它的方法调用转发给OwnInvocationHandler来处理,嗯,这个我们强调好几次了。

  可能有很多人把InvocationHandler当做了代理,但它根本就不是proxy,它只是一个帮助proxy的类,proxy会把调用转发给它处理。Proxy类本身是利用静态的Proxy.newProxyInstance()方法在运行时动态创建的,它是Java API自带的,使得Java程序员根据这种规则去创建代理类。

  此处笔者将InvocationHandler接口实现类和代理类分开来写是为了让读者明白上面反复提到的那一点:InvocationHandler接口实现类不是代理类!你理解了这个概念就好了,很重要。现在,为了我们代码的简洁,把它们两个合成一个类:

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
//该类可以获取客户本人的代理,也实现了InvocationHandler接口,所以能同时帮助代理对象处理其请求方法
public class OwnProxyInvocationHandler implements InvocationHandler {

    //要被代理的对象,当然也可以写Object对象,它是超类,所以在构造器里可以传入任何子类
    private Person person;

    public OwnProxyInvocationHandler(Person person) {
        this.person = person;
    }

    //获得客户本人的代理对象
    public Person getOwnProxy() {
        return (Person) Proxy.newProxyInstance(
                person.getClass().getClassLoader(),//获取类加载器
                person.getClass().getInterfaces(),//获取类的接口
                this);//获取调用处理器,该类实现了InvocationHandler接口,调用自身即可
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws IllegalAccessException {
        try {
            //如果方法名首个单词为get,可以执行该方法,客户当然可以查看自己的任何信息
            if (method.getName().startsWith("get")) {
               return method.invoke(person,args);
            } //否则如果方法名为setScore,则抛出一个异常,代表客户不能给自己打分
            else if (method.getName().equals("setScore")) {
                throw new IllegalAccessException();
            } //如果方法名首个单词为set,可以执行该方法。客户可以执行除了给自己打分外其他所有的设置自身的方法
            else if (method.getName().startsWith("set")) {
                return method.invoke(person, args);
            }
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
        //如果调用其他的方法,一律返回null
        return null;
    }
}

  该类不仅可以创建代理对象,还实现了InvocationHandler接口,具体解释请见注释。好了,就差最后一步了,我们创建测试类并实例化对象来测试吧!

步骤三:测试

public class Test {
    public static void main(String[] args) {
        //1.创建被代理对象
        Person person = new PersonImpl();
        //2.将被代理对象作为参数传入
        OwnProxyInvocationHandler ownProxy = new OwnProxyInvocationHandler(person);
        //3.获取代理对象
        Person proxy = ownProxy.getOwnProxy();
        //4.代理对象设置方法(调用时都会转发给invoke方法处理在返回结果)
        proxy.setName("李狗蛋");//客户本人代理对象可以设置自己的名字
        proxy.setScore(8);//客户本人代理对象不可以给自己打分,执行到此时会抛出异常
        proxy.setSex("男");
    }
}

  我们的客户本人的代理只能设置自己的姓名,性别等信息,但是如果想设置自己的分数,就会抛出一个异常,如下图所示:



  当然,真实设计的系统肯定不是抛出一个异常,而是别的效果。比如用户查看自己信息界面的时候,有很多个修改按钮可以点击,但修改分数按钮是灰色的。这里只是为了讲清楚代理的例子罢了,明白意思即可。

远程代理

待更新

虚拟代理

  虚拟代理作为创建开销大的对象代表。虚拟代理经常知道我们真正需要一个对象的时候才创建它。当对象在创建前和创建中是,由虚拟代理来扮演对象的替身。对象创建后,代理就会将请求委托给对象。


虚拟代理

  就像客户-秘书-老板的关系一样,客户提出请求,秘书可以处理这些请求(老板不在),有时候老板很忙,没那么多时间,当老板对象(被创建了)有空了,秘书就会把请求交给老板来处理。
代码待更新

参考资料

《HeadFirst设计模式》

相关文章

网友评论

      本文标题:设计模式之代理(Proxy)模式

      本文链接:https://www.haomeiwen.com/subject/xxphpqtx.html