天天看点

Design Pattern学习笔记之代理模式(the Proxy Pattern)Design Pattern学习笔记之代理模式(the Proxy Pattern)

Design Pattern学习笔记之代理模式(the Proxy Pattern)

1.    引子--Whois?

好警察还是坏警察?你是个好警察,你为每一个服务请求者提供友好、优质的服务,但是这样的话,你忙不过来,每个人都找你,特别需要你服务的人反倒获取不到服务。怎么办?一般的情况下,我们还需要一个“坏”警察,由他来过滤掉一部分意义不大的服务请求,换句话说,这个“坏”警察只是控制对“好”警察的访问,真正提供服务的是好警察。

在上面这个杜撰出来的例子中我们引入了代理模式的精髓,即控制访问,实际应用中代理模式有很多控制访问的方式,比如远程代理或者虚拟代理,在接下来的内容中,我们会从远程代理入手,详细了解代理模式的含义,并介绍其他代理模式。

2.    问题来了—远程监控自动售货机

事情还得从上一节的口香糖自动售货机说起,这天,口香糖公司老板打电话过来,想实现对公司旗下所有自动售货机的监控,监控内容主要包括库存数量和当前状态。

大家还记得在状态机模式中说到自动售货机GumballMachine,该类提供了getCount和getState方法,使用这两个方法就能轻松满足需求。很快,我们写出了如下代码:

public class GumballMonitor {

    GumballMachine machine;

    public GumballMonitor(GumballMachine machine) {

       this.machine = machine;

    }

    public void report() {

       System.out.println("GumballMachine: " + machine.getLocation());

       System.out.println("Currentinventory: " + machine.getCount() + " gumballs");

       System.out.println("Currentstate: " + machine.getState());

    }

}

问题看起来解决的很快,但是当提交给客户时,口香糖公司的老板一点都不满意,因为他实际想要的是一个远程控制的监控功能,现在全市所有的自动售货机都已联网,他希望坐在办公室实现对所有自动售货机的监控(提前做需求调研多么重要!)。

3.    头脑风暴—代理模式的引入

我们的核心研发人员进行了讨论,A君翻了翻经典的设计模式,建议使用传说中的代理模式来解决目前遇到的问题。代理模式就是使用代理来访问真正的服务提供方,在远程控制自动售货机的场景中,我们可以在本地创建代理,让监控类像访问本地方法一样调用自动售货机的代理;再由代理通过网络访问远程的自动售货机类,来获取实际的库存数量和当前状态;这样做的好处:第一可以保持监控类的逻辑不变(像调用本地的自动售货机类一样调用代理);第二可以隐藏复杂的网络访问的细节。

应用代理模式的调用流程:

Design Pattern学习笔记之代理模式(the Proxy Pattern)Design Pattern学习笔记之代理模式(the Proxy Pattern)

4.    基础知识—java的RMI

Java的远程方法调用(RMI)就是典型的远程代理模式应用,RMI的实现需要两个帮助类clientHelper和serviceHelper,由他们来完成复杂的打包、解包等网络传递的工作,并提供了远程方法注册和查找的机制,方便客户端查找服务器端的方法定义。

在RMI的语境中,将客户端的帮助类称为stub,将服务器端的帮助类称为skeleton,当然应用远程代理模式时,客户端虽然能像使用本地方法一样调用远程服务,但是由于通过网络提供服务的脆弱和不稳定性,客户端需要时刻注意应对异常情况的发生(而本地方法不存在类似问题)。

应用RMI的一般步骤:

1.      创建一个远程接口,stub和远程实际提供服务的类都要实现这个接口。

2.      创建服务的具体实现类(自动售货机类)

3.      使用jdk自带工具rmic生成stub和skeleton

4.      启动rmi的注册工具rmiregistry,远程方法只有注册后,客户端类才能查询到

5.      启动具体远程服务的实现类,启动后,该服务类自动实现在rmiregistry的注册

6.      至此,准备工作完成,客户端可查找远程方法,调用远程方法。

5.    实践篇—应用RMI的例子

我们使用简单的例子来说明使用RMI机制进行远程调用的一个例子。

1.       先声明远程方法的简单接口:

public interface MyRemote extends Remote{

        public String sayHello() throws RemoteException;

}

接口要从rmi的远程接口扩展而来,接口的方法要抛出rmi的异常,接口方法的参数和返回值都要支持序列化。

2.       提供远程接口的具体实现并注册到rmi:

public class MyRemoteImpl extends UnicastRemoteObject implements MyRemote{

        protectedMyRemoteImpl() throws RemoteException {

           super();

           // TODO Auto-generated constructor stub

        }

        publicString sayHello(){

           return "Server says, 'Hey'";      

        }

}

具体远程调用接口的实现类必须从rmi的UnicastRemoteObject扩展而来;由于父类的构造方法会抛出异常,因此需要创建空的构造方法来抛出异常。

实现这个远程接口还没有完,服务端的实现类需要注册到rmi中,客户端才能查询到,我们看一下注册方法:

    MyRemote myRemote = newMyRemoteImpl();

Naming.rebind(“RemoteHello”, myRemote);

3.       生成stub和skeleton

对具体远程接口实现类调用rmic,自动生成stub和skeleton。 Rmic MyRemoteImpl

4.       启动rmi注册

启动注册的路径要能找到具体的服务类,命令窗口下,直接输入:rmiregistry

5.       启动具体远程接口实现类

启动远程接口实现类,java MyRemoteImpl,完成远程接口在rmi的注册。

6.       客户端使用rmi调用远程接口

MyRemotemyRemote = (MyRemote)Naming.lookup(“rmi://127.0.0.1/RemoteHello”);

myRemote.sayHello();

使用Naming.lookup来完成远程接口的查找,参数包括ip地址和服务注册时使用的服务名称:Naming.rebind(“RemoteHello”, myRemote)

6.    番外篇—序列化相关

1、序列化是干什么的?

简单说就是为了保存在内存中的各种对象的状态(也就是实例变量,不是方法),并且可以把保存的对象状态再读出来。虽然你可以用你自己的各种各样的方法来保存object states,但是Java给你提供一种应该比你自己好的保存对象状态的机制,那就是序列化。

2、什么情况下需要序列化  

    a)当你想把的内存中的对象状态保存到一个文件中或者数据库中时候;

    b)当你想用套接字在网络上传送对象的时候;

c)当你想通过RMI传输对象的时候;

3、相关注意事项

    a)序列化时,只对对象的状态进行保存,而不管对象的方法;

    b)当一个父类实现序列化,子类自动实现序列化,不需要显式实现Serializable接口;

    c)当一个对象的实例变量引用其他对象,序列化该对象时也把引用对象进行序列化;

    d)并非所有的对象都可以序列化,,至于为什么不可以,有很多原因了,比如:

       1.安全方面的原因,比如一个对象拥有private,public等field,对于一个要传输的对象,比如写到文件,或者进行rmi传输  等等,在序列化进行传输的过程中,这个对象的private等域是不受保护的。

      2. 资源分配方面的原因,比如socket,thread类,如果可以序列化,进行传输或者保存,也无法对他们进行重新的资源分配,而且,也是没有必要这样实现。

7.    实践篇—远程监控自动售货机的实现

根据前一章了解到的RMI知识,我们应用RMI来改进原有的自动售货机监控程序,让它能满足远程监控的需求。

1.      提供一个统一的接口。我们来为自动售货机定义统计的接口,以满足远程监控的需求。

public interface GumballMachineRemote extends Remote {

public int getCount() throws RemoteException;

public String getLocation()throws RemoteException;

public State getState() throws RemoteException;

}

         注意getState的返回值State要支持序列化,我们来改进下,让它从Serializable接口扩展:

         public interface State extends Serializable {

        public void insertQuarter();

        public void ejectQuarter();

        public void turnCrank();

        public void dispense();

}

还有一个问题,就是大家可能还记得,State类持有一个自动售货机GumballMachine类,这个类也需要序列化。当然,我们这里采用一种更简单的处理方式,由于在远程调用时对于State而言,我们只取状态名,不需要使用GumballMachine,因此可以用transient关键字修饰变量,告诉编译器不对这个对象进行序列化。下面是代码片段:

public class SoldState implements State {

    transient GumballMachine gumballMachine;

2.      现在到了创建具体服务提供类的时候了,只需要在原来的自动售货机类上进行修改即可,以下是代码片段:

public class GumballMachine

       extends UnicastRemoteObject implements GumballMachineRemote

{

State soldOutState;

State noQuarterState;

State hasQuarterState;

State soldState;

State winnerState;

State state = soldOutState;

int count = 0;

String location;

public GumballMachine(String location, int numberGumballs) throws RemoteException {

    soldOutState = new SoldOutState(this);

    noQuarterState = new NoQuarterState(this);

    hasQuarterState = new HasQuarterState(this);

    soldState = new SoldState(this);

    winnerState = new WinnerState(this);

    this.count = numberGumballs;

        if (numberGumballs > 0) {

        state = noQuarterState;

    }

    this.location = location;

}

public void insertQuarter() {

    state.insertQuarter();

}

启动rmiregistry,我们创建一个测试驱动类,实现远程自动售货机的注册。

public class GumballMachineTestDrive {

    public static void main(String[] args) {

       GumballMachineRemote gumballMachine = null;

       int count;

       if (args.length < 2) {

           System.out.println("GumballMachine<name> <inventory>");

           System.exit(1);

       }

       try {

           count = Integer.parseInt(args[1]);

           gumballMachine =

              new GumballMachine(args[0], count);

           Naming.rebind("//" + args[0] + "/gumballmachine", gumballMachine);

       } catch (Exception e) {

           e.printStackTrace();

       }

    }

}

3.      应用工具生成stub和skeleton。

4.      客户端监控程序,实现远程调用,只需做小的改动,以下是关键的代码片段:

try {

       GumballMachineRemote machine =

        (GumballMachineRemote)Naming.lookup(location[i]);

       monitor[i] = new GumballMonitor(machine);

        System.out.println(monitor[i]);

         } catch (Exception e) {

             e.printStackTrace();

      }

8.    理论篇—代理模式

The ProxyPattern provides a surrogate or placeholder for another object to controlaccess to it. 代理模式提供另一个对象的代表,由这个代表对象控制对另一个对象的访问。

控制对另外一个对象的访问怎么理解?在远程监控自动售货机的示例中,监控对象自身不知道怎么调用远程的服务提供方,代理类掌握这些知识(知道网络传输和远程调用相关的其他细节),所以我们说远程控制示例中,stub是远程具体服务的提供方,它控制着对远程自动售货机的访问。当然,代理模式还有很多变种,有各种各样控制访问的方式:虚拟代理用于控制对昂贵资源的访问,保护代理用于控制需要鉴权的访问。

代理模式的类图:

Design Pattern学习笔记之代理模式(the Proxy Pattern)Design Pattern学习笔记之代理模式(the Proxy Pattern)

9.    更多代理—虚拟代理

虚拟代理用于推迟创建资源昂贵的对象,使用虚拟代理来替代该对象,真正服务的对象创建后,虚拟代理退化到只提供请求转发的功能。

新的需求来了:我们现在要创建一个展示唱片封面的应用,用户选择了唱片名字后,应用从在线服务中下载对应的封面图像并展示。从在线服务中下载图像时,下载速度受到在线服务的忙闲程度以及网络带宽等因素的影响,因此,我们希望在下载的过程中,先展示提示信息,等下载完成后,再来展示唱片封面。

          下面,我们会应用虚拟代理来实现这个功能。

使用swing的支持来完成界面开发,swing的Icon接口,刚好可以充当代理模式中的统一接口的角色。类图如下:

Design Pattern学习笔记之代理模式(the Proxy Pattern)Design Pattern学习笔记之代理模式(the Proxy Pattern)

图像代理类的实现:

class ImageProxy implements Icon {

    ImageIcon imageIcon;

    URL imageURL;

    Thread retrievalThread;

    boolean retrieving = false;

    public ImageProxy(URL url) { imageURL = url; }

    public int getIconWidth() {

       if (imageIcon != null) {

            return imageIcon.getIconWidth();

        } else {

           return 800;

       }

    }

    public int getIconHeight() {

       if (imageIcon != null) {

            return imageIcon.getIconHeight();

        } else {

           return 600;

       }

    }

    public void paintIcon(final Component c, Graphics g, int x,  int y) {

       if (imageIcon != null) {

           imageIcon.paintIcon(c, g, x, y);

       } else {

           g.drawString("LoadingCD cover, please wait...", x+300, y+190);

           if (!retrieving) {

              retrieving = true;

              retrievalThread = new Thread(new Runnable() {

                  public void run() {

                     try {

                         imageIcon = new ImageIcon(imageURL, "CDCover");

                         c.repaint();

                     } catch (Exception e) {

                         e.printStackTrace();

                     }

                  }

              });

              retrievalThread.start();

           }

       }

    }

}

关键的部分是paintIcon,没有从在线服务下载到封面图像之前,展示提示信息,并启动新的线程,创建新的ImageIcon对象从在线服务中下载图像,下载完图像后,重绘图标。

使用另外一个线程来下载图像的好处是避免整个前台界面类挂起等待下载。

等到完成下载后,图标代理把所有的方法调用都转发给下载后的图像图标。

还有什么问题?貌似每次都要重新下载,完全可以使用之前下载好的。

还有什么问题?貌似代理类更适合用状态机模式来消除条件判断。

10.             不辨不明—没有傻问题

Q:远程代理与虚拟代理是如此不同,他们真的都是代理模式?

A:确实都是。现实中有很多代理模式的变种,它们的共同点是都截取了客户端对一个对象的方法调用,这种非直接的方式提供了各种控制的可能性:远程访问、虚拟访问以及鉴权访问等各种控制方式。

Q:在唱片封面的示例中,ImageProxy是否更像装饰模式?你看它封装对真正Image的调用,提供相同的接口,使用组合技术将方法调用委托给真正的Image。

A:在有些场景下,装饰模式和代理模式很像,但是它们的主要目的不同:代理模式用于控制对对象的访问,而装饰模式用于为对象增加新的行为和特性。你可能会说,ImageProxy确实为Image增加了展示提示信息的特性啊。当然可以这样说,但是更关键的是ImageProxy控制着对Image的访问,为什么这么说?因为ImageProxy实现了客户端和Image的松散耦合,使得客户端无需等到Image创建后才能展示(可先展示提示信息),从这个角度看ImageProxy控制对Image的访问:在真正的Image创建之前展示提示信息;创建之后展示Image。

Q:应用代理模式时,客户端怎么创建代理?

A:可使用工厂方法模式来创建。

Q:代理模式和适配器模式也很像,不是么?

A:两种模式都在客户端和具体服务类之间插入其他类来达到特定目的,有相似性,最大的不同在于代理模式不改变对象的接口,适配器模式就是为了改变对象的接口而存在。保护代理模式根据不同权限为客户端开放被代理对象的特定方法,看起来像是接口发生变化了一样,跟适配器模式更像。

11.             更多代理—保护代理

我们接下来会介绍代理模式的另外一种变种:保护代理,通过鉴权来保证不同身份的客户端访问服务提供对象的合适方法,在实现过程中,我们会使用JDK的动态代理技术—在运行期间动态创建代理。先来了解JDK的动态代理,看一下类图:

Design Pattern学习笔记之代理模式(the Proxy Pattern)Design Pattern学习笔记之代理模式(the Proxy Pattern)

JDK实现的动态代理跟前面介绍过的两种代理模式都不同,它的代理由两个类实现:Proxy和InvocationHandler1。Proxy类是由JDK根据接口自动生成的,但是它不知道客户端调用方法时应该做什么,由InvocationHandler1来实现具体的方法调用。

需求来了:我们设计一个供人们约会的应用,人们可以在系统中注册身份信息,其他人可以查看你的信息,并能根据个人喜好为你的“热度”打分。我们来看看人员信息类的接口:

public interface PersonBean {

    String getName();

    String getGender();

    String getInterests();

    int getHotOrNotRating();

    void setName(String name);

    void setGender(String gender);

    void setInterests(String interests);

    void setHotOrNotRating(int rating);

}

再来看看实现:

public class PersonBeanImpl implements PersonBean {

    String name;

    String gender;

    String interests;

    int rating;

    int ratingCount = 0;

    public String getName() {

       return name; 

    }

    public String getGender() {

       return gender;

    }

    public String getInterests() {

       return interests;

    }

    public int getHotOrNotRating() {

       if (ratingCount == 0) return 0;

       return (rating/ratingCount);

    }

    public void setName(String name) {

       this.name = name;

    }

    public void setGender(String gender) {

       this.gender = gender;

    }

    public void setInterests(String interests) {

       this.interests = interests;

    }

    public void setHotOrNotRating(int rating) {

       this.rating += rating;  

       ratingCount++;

    }

}

可以看到,主要是一些get、set方法,可以用来查询和设置人员信息,唯一复杂点的热度值,是根据人们打分的次数和总分值而计算的一个平均值。

         有什么问题呢?

         上面这个人员信息的类对所有人都开放所有的方法,结果有个“聪明”的杰克自己把自己的热度设置的很高,赢得了很多跟美女约会的机会;而“本分”的本杰明被杰克恶作剧地把自己的岁数增加了一倍,又不懂得修改“热度”,结果实际的高富帅没有一次约会。

         对问题就在这里,我们不能对所有的人开放所有的权限,看起来应该只能让别人给你打热度分,而只有你自己能修改个人的其他信息。来看看具体实现。

         我们计划设计两个proxy:本人proxy,用于控制本人对自己信息的修改;他人proxy,用于控制设置其他人的热度值,查看其他人的信息。使用JDK的动态代理机制,proxy自动生成,我们需要书写两个不同的handler用于实现自己和其他人对人员信息所做的操作。

         Handler接口只有一个invoke方法,客户端对proxy调用的任何方法,都最终体现到对handler的invoke方法调用。下面是操作自己信息的handler实现:

public class OwnerInvocationHandler implements InvocationHandler {

    PersonBean person;

    public OwnerInvocationHandler(PersonBean person) {

       this.person = person;

    }

    public Object invoke(Object proxy, Method method, Object[]args)

           throws IllegalAccessException {

       try {

           if (method.getName().startsWith("get")) {

              return method.invoke(person, args);

           }else if (method.getName().equals("setHotOrNotRating")) {

              throw new IllegalAccessException();

           } else if (method.getName().startsWith("set")) {

              return method.invoke(person, args);

           }

        } catch (InvocationTargetException e) {

            e.printStackTrace();

        }

       return null;

    }

}

实现proxy:

PersonBean getOwnerProxy(PersonBean person) {       

        return (PersonBean) Proxy.newProxyInstance(

              person.getClass().getClassLoader(),

              person.getClass().getInterfaces(),

                new OwnerInvocationHandler(person));

    }

12.             不辨不明—没有傻问题

Q:动态代理具体指的是什么?

A:在本例中指的是proxy类是运行期才创建的。

Q:handler这个代理看起来很奇怪,它并没有实现具体服务类的任何接口。

A:handler并不是代理类,它负责对proxy代理类转发的方法调用进行处理;proxy才是代理类,由静态方法Proxy.newProxyInstance方法动态创建。

Q:在应用RMI时讲到用skeleton,从jdk1.2开始已经不需要了。

A:确实这样。从jdk1.2开始,使用反射机制由stub直接访问具体服务方,在本节提到skeleton只是为了方便描述。

Q:据说从jdk1.5开始,连stub都不需要了?

A:是这样,jdk1.5中的RMI使用动态代理技术在运行期间自动创建stub,不需要事先创建好。

13.             Review

新模式:

The ProxyPattern provides a surrogate or placeholder for another object to controlaccess to it. 代理模式提供另一个对象的代表,由这个代表对象控制对另一个对象的访问。

模式回顾:

a)        代理模式使用另外一个类的替代品来控制对该类的访问,控制访问的方式有很多种。

b)        远程代理控制客户端和远端具体服务类之间的交互。

c)        虚拟代理控制对实例化时要消耗大量资源的对象访问。

d)        保护代理基于不同的调用者提供不同的访问。

e)        代理模式和装饰模式类似,但目标不同,装饰模式主要为了增加新的特性和行为,代理模式主要为了控制对象的访问。

f)         Jdk内置了一种动态代理的实现方式,由proxy转发客户端请求,handler实现对具体实现类的调用。

OO准则:

a. 封装变化,encapsulate what varies

b. 组合优于继承, favorcomposition over inheritance

c. 面向接口编程,program to interfaces, not implementation

d. 致力于实现交互对象之间的松散耦合, strive for loosely coupled designs between objects that interact

e. 类应该对于扩展开发,对于修改封闭, classes should be open for extension but closed for modification

f. 依赖于抽象类或者接口而不是具体类。Depend on abstraction. Do not depend on concrete classes.

g. 只跟关系最密切的对象打交道。Principle of Least Knowledge – talk only to your immediate friend.

h. 别调用我,由我在需要的时候调用你。Don’t call us, we’ll call you.

i. 单一职责原则,一个类只应该因为一个理由而变化。    Single Responsibility – A class should have only one reason tochange.

继续阅读