代理模式(Proxy Pattern)

来源:百度文库 编辑:神马文学网 时间:2024/04/29 10:57:02
本文是翻译《Software Architecture Design Patterns in Java》一书,我将陆续为大家翻译其他章节,希望大家支持指正!
第23章:代理模式(Proxy Pattern)
描述:
让我们思考一下下面的代码:
//Client
class Customer{    public void someMethod(){      //Create the Service Provider Instance      FileUtil futilObj=new FileUtil();      //Access the Service      futilObj.writeToFile(“Some Data”);    }  }
作为它实现的一部分,Customer类创建了一个FileUtil类的一个实例并且直接访问它的服务。换句话说,对于客户对象,访问FileUtil对象的方式是很直接的。它的实现可能是客户对象访问服务提供者对象最为普通的方式了。相比较,有些时候客户对象可能不直接访问服务提供者(也就是指目标对象),这种情况是由于下面的原因导致的:
(1)    目标对象的位置??目标对象可能存在于同一台或者不同机器的不同地址空间。
(2)    目标对象的存在形式??目标对象可能直到他被请求服务的时候还不存在,或者对象被压缩。
(3)    特殊的行为??目标对象可以根据客户对象的访问权限接受或拒绝服务。在多线程环境,一些服务提供者对象需要特殊的考虑。
在这些情况下,代理模式(Proxy Pattern)建议不要使有特殊需求的客户对象直接访问目标对象,而是使用一个单独的(分离的)对象(也就是指代理对象)为不同的客户提供通常的、直接的访问目标对象的方式。
代理对象提供和目标对象一样的接口。代理对象负责与目标对象交互的细节,代表客户对象与目标对象交互。所以客户对象不再需要处理访问目标对象的服务时的特殊需求。客户对象通过它的接口调用代理对象,代理对象直接把这些调用依次地传递给目标对象。客户对象不需要知道代理的原对象(目标对象)。代理对象隐藏了与客户对象进行交互的对象细节,如:对象是否是远程的、是否初始化、是否需要特殊的权限等。换句话说,代理对象作为客户和不可访问的远程对象或推迟初始化对象之间的透明桥梁。
代理对象因使用的场景不同,代理的种类也不同。让我们来快速的浏览一下一些代理和它们的目标。
注意:表23.1列出了不同种类的代理对象,在一章中,仅讨论远程代理,其他的一些代理会在本书后面的模式中讨论。

Table 23.1: List of Different Proxy Types
代理类型     目的
远程代理    提供对在不同地址空间的远程对象的访问
缓存代理
/服务代理    为了提供能够保存目标操作经常用到的结果,代理对象以存储方式保存这些结果。当客户对象请求同一个操作时,代理不需要直接访问目标对象,而是从存贮介质返回操作结果。
防火墙代理    使用防火墙代理主要是为了保护目标对象以防止有害客户的访问。同时也可以防止客户访问有害的目标对象。
保护代理    提供了不同客户访问不同层次的目标对象的功能。
在创建代理时,定义了一个权限的集合。虽后,这些权限用来限制访问代理的特定部分,如果没有执行方法的权限,客户对象不允许访问特定的方法。
同步代理    提供了允许不同的客户对象安全的同步访问目标对象的功能。
计数代理    在执行目标对象的方法前,提供了一些审计机制。
代理模式和其他模式
从讨论不同的代理对象中可以看出:代理对象有两个主要的特征:
(1)    它介于客户对象和目标对象之间。
(2)    它接受客户对象的调用,然后转发调用给目标对象。
在这种情形下,看上去和本书中前面讨论的其他模式有些相似。让我们讨论一下代理模式和一些与它相似的模式之间的相同点和不同点。
代理模式和装饰模式

代理模式:
(1)    客户对象不能直接访问目标对象
(2)    代理对象提供了对目标对象的访问控制(在保护代理中)
(3)    代理对象不能再增加其他的功能。
装饰模式:
(1)    如果需要,客户对象不能直接访问目标对象。
(2)    装饰对象不能控制对目标对象的访问。
(3)    装饰对象可以增加额外的功能。
代理模式和外观模式:
代理模式:
(1)    代理对象代表一个单一对象。
(2)    客户对象不能直接访问目标对象。
(3)    代理对象提供了对于单一目标对象的访问控制。
外观模式:
(1)    外观对象代表了对象的一个子系统。
(2)    如果必要,客户对象可以直接访问子系统中的对象。
(3)    一个外观对象提供了一个对子系统组件的简单的、高层次的接口.
代理模式和责任链模式:
代理模式:
(1)    代理对象代表了一个单一的对象。
(2)    克辉请求首先被代理对象所接受,但是不直接被代理对象处理。
(3)    客户请求总是被传递给目标对象。
(4)    假设客户与服务器正常工作,可以保证请求会得到响应,
责任链模式:
(1)    责任链包括很多对象。
(2)    接受客户请求的对象首先处理请求。
(3)    近当现在的接收者不能处理请求时,客户请求才被传递给下一个对象。
(4)    不能保证请求会得到响应。也就是请求已经到达责任链尾,担仍然没有被处理。
在Java中,远程方法调用(RMI)充分的利用了远程代理模式,让我们快速的浏览一下远程方法调用(RMI)的概念和远程方法调用(RMI)通信过程应用的组件。
RMI:快速浏览
RMI使客户对象像访问本地对象一样访问远程对象并调用其方法成为可能。(如图23.1)

Figure 23.1: Client’s View of Its Communication with a Remote Object Using RMI
下面是为实现RMI功能而一起协作的不同组件。
(1)    远程接口(Remote Interface)??一个远程对象必须实现一个远程接口(这个接口扩展java.rmi.Remote)。远程接口声明可以被客户访问的远程对象的方法。换句话说,远程接口可以看成远程对象对客户的视图。
需求(要求):
1)    扩展java.rmi.Remote
2)    在远程接口中定义的所有方法必须声明抛出java.rmi.RemoteException异常。
(2)    远程对象(Remote Object)??远程对象负责实现在远程接口中定义的方法。
需求(要求):
1)    必须提供远程接口的实现。
2)    必须扩展java.rmi.server.UnicastRemoteObject类。
3)    必须有一个没有参数的构造函数。
4)    必须与一个服务器相关联。通过调用零参数的构造函数,服务器创建远程对象的一个实例。
(3)    RMI注册表(RMI Registry)??RMI注册表提供了保持不同远程对象的地址空间。
1)    远程对象需要存储在一个客户可以通过命名引用(Name reference)来访问它的RMI注册表中。
2)    一个给定的命名引用仅可以存储一个对象。
(4)    客户(Client)??客户是一个试图访问远程对象的应用程序。
1)    必须可以感知被远程对象实现的接口。
2)    通过命名引用(Name reference)在RMI注册表中可以查到远程对象。一旦查到远程对象的引用,调用这个引用上的方法。
(5)RMIC (Java RMI 桩编译器)Java RMI stub compiler??一旦远程对象编译成功,RMIC(Java RMI 桩编译器)可以生成远程对象的桩类文件(stub)和框架类文件(skeleton)。桩类文件(stub)和框架类文件(skeleton)从编译的远程对象类中产生。这些桩类文件(stub)和框架类文件(skeleton) 使客户对象以无缝的方式访问远程对象成为可能。
下面这部分描述客户对象和远程对象如何通信。
RMI通信机制:
一般地,客户对象不能按通常方式直接访问远程对象。为了使客户对象像访问本地对象一样访问远程对象的服务,RMIC(Java RMI 桩编译器)生成的远程对象的桩文件(stub)和远程接口需要拷贝到客户机器上。
桩文件(stub)负责扮演着远程对象的(远程)代理的角色,负责把方法的调用传递给真实的远程对象实现所在的远程服务器上。任何时候,客户对象引用远程对象,这个引用实际上是远程对象的本地桩文件。也就是,当客户调用远程对象上的方法时,调用首先被本地桩实例所接受,桩再将这个调用传递到远程服务器上。在服务器端,RMIC产生的远程对象的框架文件(skeleton)接受这个调用。
框架文件(skeleton)在服务器端,不需要拷贝到客户机器上。框架文件(skeleton)负责将这些调用转发到真正的远程对象的实现上。一旦远程对象执行了方法,方法返回的结果将按照反方向返回给客户。
图23.2说明了RMI通信的过程

Figure 23.2: The Actual RMI Communication Process
了解更多的关于Java RMI的知识,推荐阅读RMI tutorial
RMI和远程代理模式:
从RMI通信的讨论中,可以看到桩类文件扮演着远程对象的远程代理的角色。它使得客户访问远程对象就像访问本地对象一样成为可能。因此,一些使用了RMI技术的应用就已经暗含着代理模式的实现。
例子:
在讨论外观模式时,我们建立了一个简单的客户数据管理应用来验证和保存输入的客户数据。我们的设计由分别代表不同客户数据的三个类组成。
在应用外观模式以前,客户AccountManager可以直接与子系统的三个用来验证、保存客户数据的类交互。应用外观模式,我们定义了一个CustomFacade外观对象代表客户与三个子系统类交互(如图23.3)。

Figure 23.3: Customer Data Management Application for the Local Mode of Operation?Class Association
在这个应用中,子系统组件和外观对象对于客户对象AccountManager都是本地的。现在,让我们建立这个应用的不同版本,这个版本已远程的方式运行。在远程方式下,这个应用通过运用JAVA RMI技术,访问远程对象。
在使应用运行在远程操作模式下的设计中,我们要把子系统组件(Account、Address和CreditCard)和外观(CustomerFacade)移到远程服务器上。这样会带来以下好处:
1)    在服务器上的对象可以被不同的客户应用所共享。客户不再需要维护这些类的本地版本,因此,成为轻型客户端(light-weighted)。
2)    可以对变化、性能和监控进行统一的集中控制。

Figure 23.4: Customer Data Management Application for the Remote Mode of Operation?Class Association
让我们开始运用RMI技术设计远程操作模式下的客户数据管理应用。
第一步,先定义远程接口CustomerIntr:这个借口要满足:
1)    声明外观实现的方法。
2)    所有的方法声明抛出RemoteException异常。
3)    扩展java.rmi.Remote接口。
public interface CustomerIntr extends java.rmi.Remote {    void setAddress(String inAddress) throws RemoteException;    void setCity(String inCity) throws RemoteException;    void setState(String inState) throws RemoteException;    void setFName(String inFName) throws RemoteException;    void setLName(String inLName) throws RemoteException;    void setCardType(String inCardType) throws RemoteException;    void setCardNumber(String inCardNumber)      throws RemoteException;    void setCardExpDate(String inCardExpDate)      throws RemoteException;    boolean saveCustomerData() throws RemoteException;  }
让我们重新定义CustomerFacade外观类,因为它要实现CustomerIntr远程接口。不同的客户对象通过CustomerIntr接口在具体类CustomerFacade上的实现与子系统对象进行交互。图23.5展示了CustomerFacade和它实现的远程接口CustomerIntr之间的结构和关联。
Listing 23.1: CustomerFacade Class?Revised
public class CustomerFacade extends UnicastRemoteObject    implements CustomerIntr {    private String address;    private String city;    private String state;    private String cardType;    private String cardNumber;    private String cardExpDate;    private String fname;    private String lname;    public CustomerFacade() throws RemoteException {      super();      System.out.println("Server object created");    }    public static void main(String[] args) throws Exception {      String port = "1099";      String host = "localhost";      //Check for hostname argument      if (args.length == 1) {        host = args[0];      }      if (args.length == 2) {        port = args[1];      }      if (System.getSecurityManager() == null) {        System.setSecurityManager(new RMISecurityManager());      }      //Create an instance of the server      CustomerFacade facade = new CustomerFacade();      //Bind it with the RMI Registry      Naming.bind("//" + host + ":" + port + "/CustomerFacade”,                  facade);      System.out.println("Service Bound…");    }    public void setAddress(String inAddress)      throws RemoteException {      address = inAddress;    }    public void setCity(String inCity)      throws RemoteException{ city = inCity;    }    public void setState(String inState)      throws RemoteException{ state = inState;    }    public void setFName(String inFName)      throws RemoteException{ fname = inFName;    }    public void setLName(String inLName)      throws RemoteException{ lname = inLName;    }    public void setCardType(String inCardType)      throws RemoteException {      cardType = inCardType;    }    public void setCardNumber(String inCardNumber)      throws RemoteException {      cardNumber = inCardNumber;    }    public void setCardExpDate(String inCardExpDate)      throws RemoteException {      cardExpDate = inCardExpDate;    }    public boolean saveCustomerData() throws RemoteException{      Address objAddress;      Account objAccount;      CreditCard objCreditCard;      /*       client is transparent from the following       set of subsystem related operations.      */      boolean validData = true;      String errorMessage = "";      objAccount = new Account(fname, lname);      if (objAccount.isValid() == false) {        validData = false;        errorMessage = "Invalid FirstName/LastName";      }      objAddress = new Address(address, city, state);      if (objAddress.isValid() == false) {        validData = false;        errorMessage = "Invalid Address/City/State";      }      objCreditCard = new CreditCard(cardType, cardNumber,                      cardExpDate);      if (objCreditCard.isValid() == false) {        validData = false;        errorMessage = "Invalid CreditCard Info";      }      if (!validData) {        System.out.println(errorMessage);        return false;      }      if (objAddress.save() && objAccount.save() &&          objCreditCard.save()) {        return true;      }  else {        return false;      }    }  }

Figure 23.5: Façade Design?Remote Mode of Operation
因为子系统组件对于CustomerFacade类是本地的,子系统组件初始化、方法调用的方式上没有任何变化,子系统组件对于CustomerFacade类仍然是本地对象。当执行的时候,CustomerFacade自己创建一个实例并把引用名称(reference name)保存在RMI注册表中。客户对象通过引用名称能取得远程对象的一个拷贝。
因为客户不需要直接访问任何的子系统组件。所以在远程操作模式下的设计中,不需要对子系统的任何组件进行任何的修改。
让我们重新设计客户类AccountManager:
Listing 23.2: AccountManager Class?Revised
…          …  public void actionPerformed(ActionEvent e) {          …          …    if (e.getActionCommand().equals(          AccountManager.VALIDATE_SAVE)) {      //get input values      String firstName = objAccountManager.getFirstName();      String lastName = objAccountManager.getLastName();      String address = objAccountManager.getAddress();           …           …      try {        //Call registry for AddOperation        facade = (CustomerIntr) Naming.lookup ("rmi://" +                 objAccountManager.getRMIHost() + ":" +                 objAccountManager.getRMIPort() +                 "/CustomerFacade");        facade.setFName(firstName);        facade.setLName(lastName);        facade.setAddress(address);           …           …        //Client is not required to access subsystem components.        boolean result = facade.saveCustomerData();        if (result) {          validateCheckResult =            " Valid Customer Data: Data Saved Successfully ";        }  else {          validateCheckResult =            " Invalid Customer Data: Data Could Not Be Saved ";        }        } catch (Exception ex) {         System.out.println(           "Error: Please check to ensure the " +           "remote server is running" +           ex.getMessage());        }        objAccountManager.setResultDisplay(          validateCheckResult);        }    }
和本地运行模式相似,AccountManager显示了必要的接受输入客户数据的用户界面,(如图23.6)当用户输入数据点击Validate&Save按钮时,它会在RMI注册表中通过引用名称取得远程对象的引用。

Figure 23.6: The User Interface?Remote Mode of Operation
一旦从RMI注册表中取得了远程对象的引用,客户就像调用本地操作一样调用远程对象上的操作。图23.7解释了这个行为。

Figure 23.7: AccountManager View of Its Communication with the Remote CustomerFacade
注意:在运行程序以前,编译CustomerFacade类产生桩类文件必须拷贝到客户类AccountManager所在的位置。编译CustomerFacade类以后,使用RMIC编译CustomerFacade类文件,产生桩类文件(stub)和框架类文件(skeleton)。编译和部署应用程序不同部分的具体指导在后面的“附加说明”部分。
实际上,当客户调用远程对象CustomerFacade上的类似saveCustomerData方法时,在客户本地的CustomerFacade_stub对象首先接受这个调用,然后CustomerFacade_stub把这个调用传递给等待处理的服务器。
在服务器端的CustomerFacade_skel负责接收通过低层次的网络通信传递的方法的调用。然后,它分发这个调用给服务器上的真实的CustomerFacade对象。例如:saveCustomerData方法,CustomerFacade对象创建必要的子系统的对象,调用这些对象上的验证和保存客户数据的方法。处理的结果以相反的方式带回给客户端。图23.8说明了真实的通信机制。

Figure 23.8: The Actual Flow of Communication
从上面可以看到,CustomerFacade_stub类能使客户对象像调用本地对象一样调用用通常的方式不能调用的远程对象的方法。在这里桩的作用就是远程代理。
附加说明

编译和部署的说明
1)    在proxy/Server目录下,编译所有的Java文件
2)    在proxy/Server目录下,执行下面的命令:
Rmic CustomerFacade
这个命令调用RMI桩编译器,分别创建桩类文件(stub) CustomerFacade_stub.class和框架类文件(skeleton) CustomerFacade_skel.class。
3)    从proxy/Server目录拷贝下面的文件到proxy/Client目录:
CustomerIntr.class
CustomerFacade_Stub.class
4)    在proxy/Client目录下,编译所有的Java文件
5)    开启rmiregistry
start rmiregistry  (Windows)
rmiregistry & (Solaris)
 ?这是RMI注册表监听的端口,默认为1099
例如:
start rmiregistry
6) 执行下面的命令:
java -Djava.security.policy= CustomerFacade
 
例如:
java -Djava.security.policy=java.policy CustomerFacade
localhost 1099
 ? 这是权限文件的名字。这个文件中,需要指定潜在的运行系统在文件系统的位置。
注意: The java.policy文件在服务器目录下是有效的。
 ?这是注册的远程对象正在运行的DNS(Domain Name System)域命名系统的名字后者主机的IP地址。如果是在同一台机器上,使用“localhost”。
 ? 注册的远程对象监听指定的RemoteRegistryHost端口,默认为1099。
6)    下面的信息会在命令行中显示:
Server object created
Service bound…
7)    在proxy/Client目录下,执行下面的命令运行客户程序。
java -Djava.security.policy= AccountManager
 
例如:
java -Djava.security.policy=java.policy AccountManager
localhost 1099
 ?这是权限文件的名字。这个文件中,需要指定潜在的运行系统在文件系统的位置。
注意: The java.policy文件在客户目录下是有效的。
 ?这是注册的远程对象正在运行的DNS(Domain Name System)域命名系统的名字后者主机的IP地址。如果是在同一台机器上,使用“localhost”。
 ? 注册的远程对象监听指定的RemoteRegistryHost端口,默认为1099。
附件为本文的代码和原文:
对于代码的说明:为了把上面介绍的复杂过程简单化,自己定义了两个cmd文件。下载原吗以后,
1)先修改server目录下的java.policy文件,
grant {         permission java.net.SocketPermission "*:1024-65535",           "connect,accept,resolve";         permission java.net.SocketPermission "*:1-1023",           "connect,resolve";         permission java.io.FilePermission "[b]C:\\eclipse\\workspace\\AU2142\\27\\server\\*[/b]", "read,write"; };
修改C:\\eclipse\\workspace\\AU2142\\27\\server\\*为你的下载目录,一定要对应。
2)再运行server目录下的RunServer.cmd;
3)再运行client目录下的RunClient.cmd;
注意:如果client和server不是在一台机器上,要修改
RunClient.cmd和RunServer.cmd;大家一看着两个文件,就知道怎么修改了,地球人都知道了!哈哈!