2. 重新思考图像代理:
现在需要思考设计模式是否真的能够帮助我们。之前我们费尽心血实现了一个设计模式,而现在又要考虑将它拆掉。现实的开发中会遇到更多这样的情况,实际上这是很自然的。经过他人的评审之后,设计人员可以在发布之前重新构思自己的设计,并修改其中有问题的部分。在实际过程中,设计模式可以帮助我们设计和调试应用程序,另外也方便了他人对我们的设计进行讨论。就Oozinoz公司的ImageIconProxy类而言,模式也起到了自己的作用,虽然不实现Proxy模式会更简单。
ImageIcon类操控Image对象。如果不把绘图请求转发给单独的ImageIcon对象,而是直接操作ImageIcon中的Image对象,这样会更简单。下图显示的LoadingImageIcon位于com.oozinoz.imaging包中;除了构造器,该类仅提供两个方法:load()方法和run()方法。
LoadingImageIcon类的作用是切换Image对象
对该类进行修改后,其load()方法仍然以JFrame对象为参数,用于在指定图像加载完毕之后进行回调。在执行load()方法的时候,它先以LOADING引用的图像对象为参数调用setImage()方法,然后重新绘制图形显示窗口,最后为自己启动一个单独的线程。而run()方法是在单独的线程中执行的,该方法根据构造器中指定的图像文件名创建一个新的ImageIcon对象,然后以该图像对象为参数调用setImage()方法,最后重绘该窗口。
突破题:请填写完整下面LoadingImageIcon类的load()方法和run()方法。
其中load()方法将当前图像设置成"Loading...",而run()方法是在另一个线程中执行,它负责加载指定的图像。
修改后的代码与ImageIcon设计的耦合就不那么紧了;现在的代码主要依赖ImageIcon类的getImage()方法和setImage()方法,而不再依赖请求转发机制。实际上,现在根本不存在转发:LoadingImageIcon类虽然在结构上不是代理,但却起着代理的作用。
依赖于转发的Proxy模式会造成维护方面的负担。例如,随着底层对象的变化,Oozinoz公司将不得不更新代理。为了避免这种开销,我们应该经常考虑Proxy模式的替代方案,但是Proxy模式仍然可能是正确的选择。特别是在我们需要获取消息的对象正在另一台机器上执行的时候,Proxy模式可能不存在替代方案。
3. 远程代理:
如果我们期望调用正在另一台计算机上运行的对象的方法,那么必须找到一种方法来与该远程对象通信,而不能直接调用其方法。我们可以在远程机器上打开一个套接字,并设计一种协议用于向远程对象发送消息。理想情况下,这种方案可以让我们自由地与远程对象通信,就像与本地对象通信一样。在这种方案下使用Proxy模式,可直接调用位于本地的代理对象的方法,该代理对象将调用请求发给远程对象。实际上,著名的公共请求代理架构(CORBA)、ASP.NET以及Java的远程方法调用(RMI)已经实现了这种方案。
在RMI中,代理对象用于将调用请求转发给在另一台计算机上运行的指定对象,客户可以很容易地获得这种代理对象。企业JavaBeans(EJB)规范是业界新兴的一个重要标准,它的基础之一便是RMI;要理解EJB,必须先了解RMI。无论业界标准如何发展,我们可以预见未来的分布计算仍将离不开Proxy模式。RMI为代理模式的实践提供了一个很好的例子。
我们期望能够通过下面的这个RMI应用程序例子,向读者说明其中的Proxy模式如何应用以及该模式的价值所在,而不只是简单介绍一下RMI的使用。RMI和EJB带来了许多新的设计理念;我们不能简单地使所有对象都变成远程的以得到一个合理的系统。但这方面将不作深入讨论,而只是简单探究一下RMI对于Proxy模式来说到底是怎么重要的一个例子。
下面的这个例子中,我们利用RMI让另一台计算机上的Java程序访问一个对象的方法,以此来探究RMI的工作机制。开发的第一步是为指定的远程对象创建一个接口。作为一个实验性的项目,我们为Oozinoz公司创建了一个Rocket接口,该接口独立于Oozinoz公司的现有代码:
package com.oozinoz.remote;
import java.rmi.*;
public interface Rocket extends Remote
{
void boost(double factor) throws RemoteException;
double getApogee() throws RemoteException;
double getPrice() throws RemoteException;
}
Rocket接口扩展了Remote接口。该接口中的所有方法都声明自己会抛出RemoteException异常对象。作为服务的提供者,远程对象不仅实现了远程接口,还要扩展UnicastRemoteObject类,如下图所示:
若要使用RMI,首先要创建一个远程接口,定义需要在计算机之间 交互的消息,
再创建一个远程对象;该远程对象可实现远程接口,并扩展UnicastRemotObject类
RocketImpl对象作为服务提供者类运行在服务器上,客户端可以通过运行在本地的代理对象来访问RocketImpl对象的方法。RocketImpl类的代码比较简单:
RocketImpl的实例运行在一台机器上,我们要让它能够被运行在其他机器上的Java程序访问,就必须在客户端为RocketImpl对象提供一个代理对象。为了实现此目的,客户端需要一个RocketImpl对象的代理。该代理类必须实现Rocket接口,并且提供用于与远程对象通信的附加特性。RMI的最大便利之一就是它能够自动创建这个代理类。为了自动生成代理类,我们必须把RocketImpl.java文件和Rocket.java接口文件存放在RMI注册程序的运行目录之下:
package com.oozinoz.remote;
import java.rmi.*;
public class RegisterRocket
{
public static void main(String[] args)
{
try{
Rocket biggie = new RocketImpl(29.95,820);//可以将biggie声明为RocketImpl类。不过重要的是,只要biggie实现了Rocket接口,它就可以被用户查询到。
Naming.rebind("rmi://localhost:5000/Biggie",biggie);
}catch(Exception e){
e.printStackTrace();
}
}
}
D:\rmi>dir /b com\oozinoz\remote
RegisterRocket.class
RegisterRocket.java
Rocket.class
Rocket.java
RocketImpl.class
RocketImpl.java
ShowRocketClient.class
ShowRocketClient.java
运行JDK提供的RMI编译器,自动生成用于简化远程通信的RocketImpl存根。
D:\rmi>rmic com.oozinoz.remote.RocketImpl
值得注意的是,rimc命令以完整的类名而不是文件名作为参数。早期版本号的JDK将用于客户端的部分和用于服务器端的部分分别存放在不同的文件中。从版本1。2起,RMI编译器将用于客户端和服务器端的部分都存放在一个独立的存根文件中。rimc命令编译了它所需要的所有类,并创建了RocketImpl_Stub类:
D:\rmi>dir /b com\oozinoz\remote
RegisterRocket.class
RegisterRocket.java
Rocket.class
Rocket.java
RocketImpl.class
RocketImpl.java
RocketImpl_Stub.class
ShowRocketClient.class
ShowRocketClient.java
在对象能够被远程访问之前,我们必须用运行在服务器上的RMI注册程序注册该对象。rmiregistry可执行程序是JDK的一部分。在运行注册程序的时候,需指定注册程序所监听的端口:
D:\rmi>rmiregistry 5000
在服务器上运行了注册程序之后,我们便可以开始创建和注册RocketImpl对象:
当编译并运行该代码后,该程序会显示出对象注册成功的确认信息。
Registered biggie
运行RegisterRocket应用程序便可在服务器上创建并运行一个RocketImpl对象biggie。客户端只要能够访问Rocket接口和RocketImpl_Stub类,便可以访问远程运行的biggie对象。如果只有一台机器,我们仍然可以测试这个RMI应用程序,不过要在localhost而不是另外一台机器上运行服务器端代码:
当这个程序运行的时候,它首先使用注册名"Biggie"来查找远程对象。该远程对象代表着远程的一个RocketImpl对象;lookup()方法返回的对象obj是RocketImpl_Stub类的一个实例。RocketImpl_Stub类实现了Rocket接口,因而我们可以将对象obj强制转化为Rocket接口的一个对象。RocketImpl_Stub类继承了RemoteStub类,使其对象能够与服务器通信。
运行ShowRocketClient程序时,屏幕上将显示"Biggie"火箭的最高距离数据:
Apogee is 820.0
对代理对象getApogee()方法的调用将会转发给运行在服务器上的Rocket接口的实现对象。
突破题:下图显示getApogee()方法被转发的过程。最右边的对象使用粗体方框加以标识,表明它与ShowRocketClient程序分别运行在不同的机器上。请填写图中空缺的类名。
基于RMI的分布式应用程序的消息流
RMI的优点在于它使得客户端程序只需与本地代理对象进行交互便可达到与远程对象通信的目的。RMI用户定义了客户端和服务器端共享对象的接口。RMI为客户端和服务器端分别提供一个Rocket接口的实现类;这两个类实现相互协作,从而可完成进程间的无缝通信。服务器端和客户端则不必关心这些细节。
自己总结:这里ShowRocketClient在实际应用中处于客户端,此应用程序访问本地(即客户端)对象RocketImpl_Stub的getApogee()方法,但RocketImpl_Stub将此方法转发给了远程的RocketImpl对象。应该是RocketImpl_Stub类和ShowRocketCient应用程序存放在客户端,RegisterRocket、Rocket及RocketImpl类都位于服务器端。