Java 版 (精华区)
发信人: rhine (静&&凉爽&&想念你), 信区: Java
标 题: jini核心技术5
发信站: 哈工大紫丁香 (2000年07月17日11:05:56 星期一), 站内信件
第5章 Jini 起 步
主要内容:
* 创建、测试和部署Jini服务的原则。
* 通过"Hello, World"服务认识查找和发现。
* 使用远程事件检测群体中的变化。
* 实现租借以加强可靠性和"自修复"能力。
* 使用RMI和激活框架。
在本章中,将开始创建一些真正的Jini服务,以及使用服务的客户端应用。我们要接触
的第一对程序是"Hello,World"服务和客户程序,它们是最简单的Jini程序:它们使用
发现并找到查找服务,服务公布一个服务代理,客户取到代理并使用服务。这个服务的
代理十分简单,它在被请求时返回一个"Hello,World"的字符串。在这个例子中,代理
就是服务,因为它可以自己独立地完成任务,但它描述了如何使用Jini进行工作。
在这个起始程序之后,我们将深入了解更多的Jini特性,首先将对客户端进行扩展,使
之使用事件,以便在它以后有服务注册到查找服务时,可通告此客户。
再以后将涉及到更复杂的租借。在刚开始时,服务不续订它的因在查找服务中注册而拥
有的租约,接下来就对功能进行扩展,使之不断续订自己拥有的租约,以便使服务的代
理不会从Jini群体中消失。同样,客户应用程序也要续订其为事件注册而拥有的租约,
在这一部分我们将扩展它的功能使之可管理自己的事件注册租约,以便在群体的状态发
生变化时被通知。
最后,将创建一个更复杂的"Hello, World"服务版本,它使用的服务代理要与后端的服
务器进程进行通信。在这种情况下,代理使用RMI与后端的服务器通信,获取信息进行显
示。尽管例子比较简单,但它代表了构造Jini服务的大多数情况-使用一个"瘦"代理与后
端的"胖"服务器进行通信,后端服务器承担了服务的实现。这个例子也描述了如何使用
RMI激活机制创建只是在需要时才被激活的后端服务。
从本章开始,我们将逐渐把在第3章接触的一些抽象的Jini概念转化为实际的程序,将逐
渐扩展例子的功能,以便在每一步都集中于一个要解释的特征。
5.1 运行Jini服务
在开始编写程序之前,应该先确认所有需要的Jini服务都已经启动。关于如何设置运行
Jini的环境,已在第1章进行了介绍。对于本章的例子,没有必要运行JavaSpaces存储服
务,也没必要运行事务管理器。"Hello, World"所需的服务只是一个查找服务、一个RM
I激活守护进程、一个运行在网络上的HTTP守护进程。RMI激活守护进程(rmid)需要和
查找服务运行在同一个主机上,因为Sun实现的查找服务要使用激活进程。在本书最后的
例子中,将创建自己定制的可激活服务。在这个例子中,还要使rmid与服务运行在同一
台主机上(如果已经使例子和查找服务运行在同一台机器上,就不必另外运行一个单独
的rmid实例了-每台机器只需要运行一个,不管有多少个可激活的服务运行在该机器上)
。
下面是启动前要检查的项目列表:
* 按照第1章中的说明配置PATH和CLASSPATH参数。
* 运行RMI激活守护进程。要在将运行查找服务的机器上运行此进程,而且要在查找服务
之前启动。对于最后一个例子,还需要使激活守护进程和例子程序运行在同一台主机上
。
* 运行Jini查找服务。要保证运行此服务的机器与计划运行例子程序的机器在同一子网
中-原因是Jini的组播发现协议缺省地配置为只寻找该对象"附近"的查找服务 。
* 运行Jini HTTP服务器。这个特别的HTTP服务器实例可为Jini查找服务提供可下载的程
序。此服务器可与查找服务运行于同一机器上,可以把其"根"目录设为Jini分发软件的
"lib"目录,这将保证其他程序可以下载到reggie-dl.jar文件中的类,其中包含了程序
使用查找服务所必需的代码。另外,要编写的所有例子程序,都应该能提供代码给Jini
服务,而且可以彼此提供,因此还需要运行HTTP服务以提供这些例子的代码。在进入例
子程序之后,将详细地说明如何完成这些工作。
有了这些服务的配置,就可以运行下面的所有程序了。不过为使测试和部署Jini时更加
方便,还有一些步骤必须遵循,这些步骤将在下一节介绍。
在介绍到每个例子时,还将看到各例子的特别说明。注意本章的例子都由两部分组成,
Jini服务-发布服务代理的实体和Jini客户-一般只是使用发现和查找去寻找它要用到的
相关服务代理的应用程序。在很多情况下,服务要使用其他的服务,因此Jini程序经常
既是服务,又是客户。不过为简单起见,这里的客户程序只是单独的应用程序。
如果是在同一台机器上运行所有这些程序,包括查找服务、HTTP服务、激活守护进程、
客户和服务器,那么建议你读下一节的内容。如果像开发"单独"的Java应用一样开发Ji
ni应用程序,即在同一台机器上相同的CLASSPATH配置下完成所有的工作,在把这些程序
部署到多台机器上时就可能会遇到问题。下一节的内容介绍了一些调整环境以减少这些
问题的策略。
5.2 按部署情况进行开发
显然,在"真实世界"中,所有的Jini服务和客户都将运行在多机的环境中,这意味着服
务可能会被分布式到网络中任意数量的机器上。服务的下载代码可使用很多的HTTP服务
器,而客户可从任意的机器上连接这些服务。
不过出于方便或其他需要,经常要在同一台机器上开发和测试程序。在这种情况下经常
会出错,原因是在同一台机器上运行所有程序时,我们不能检查到当分布式系统的一部
分运行在其他机器上时可能会存在的问题。
这是什么意思呢?大部分的Jini开发工作都需要运行三个Jini的应用程序:查找服务、
要测试的服务以及使用服务的客户(另外还有其他的程序如HTTP服务器和RMI激活守护进
程等)。如果所有这些程序都运行在同一台机器上并共享资源(相同的CLASSPATH,相同
的HTTP服务器等),那些就会掩盖动态加载库及安全方面潜在的问题,在这样的环境下
开发和测试会使一些问题潜伏在程序之中。
这是只进行"本地"测试存在的显著危害,前期工作的方便最终将会带来更大的麻烦。因
此即使多机环境可能需要做更多的"前导"工作,如启动更多的HTTP服务器、设置安全策
略等,我们也要这么做,好处就是在后来调试和配置时要轻松得多。为此,养成多机环
境的思维习惯是较好的做法,即使在开发和测试时也要"模拟"这样的环境。
这种"模拟"相当简单,即使在单机上进行开发,也可以在运行和测试程序时遵循一些简
单的忠告,保证它可以在多机环境中运行。这些忠告涉及到了多机环境下常见的大部分
问题。
下面是在开发时应该遵循的忠告:
* 为每个向其他程序提供可下载代码的程序单独运行一个HTTP服务器。
* 注意代码基(codebase)问题。
* 设置安全管理器。
* 注意安全策略。
* 注意CLASSPATH的设置。
* 考虑把可下载代码捆绑为一个JAR文件。
接下来我们分别看一下各点的内容。
5.2.1 运行多个HTTP服务器
为每个需要向其他程序提供可下载代码的程序单独运行一个HTTP服务器是一种好的思路
,这种策略可以清楚地把各程序的可下载代码与其他程序的代码分开。如果只运行一个
HTTP服务器,使其根目录指向Java开发树的顶层,可能会更加方便些,因为这样在任何
地方都能访问到开发者编写的所有类。但问题是这种方法使我们无法判断下载代码的相
关性,而且在应用或服务需要移动到另外的机器时变得不可用。
如果能够把一个程序的所有可下载代码单独放在一个HTTP服务器上,然后带有代码基参
数运行,该程序告诉此程序的客户到指定的HTTP服务器上取代码。这样就可以判断不能
在某处得到所有需要的代码的情况,因为此时客户会报告说不能找到所需的特定类。
5.2.2 警惕代码基问题
代码基参数是在服务器(输出可下载代码的程序)上设置,告诉客户(代码的使用者)
到哪里下载所需类的参数。附录A中有关于代码基的更多细节。
但在设置代码基属性时有一些通用的好建议需要考虑,首先,不要使用文件URL,如果一
个服务器向客户传递了文件URL,客户就会试图从本地的文件系统中下载所需的代码。如
果是在同一台机器上同时测试服务和客户,程序可能不会出问题,因为对于客户和服务
来说类的文件在同一位置。但如果在不同的机器上运行客户和服务时,由于文件系统不
同,程序就会出问题。
同样的在代码基不要使用"localhost"作主机名。"localhost"用来指代当前主机,因此
如果服务器把代码基设为了包括"localhost"的URL,则客户会根据此代码基属性试图在
自己的系统中下载代码,而不是去找服务器。同样,当在同一台机器上测试客户和服务
器时,它可以"欺骗性"地运行。而如果在代码基的URL中使用实际的主机名,并为每个服
务单独运行HTTP服务器,则如前所述,代码下载的潜在问题可以提前被发现。
5.2.3 设置安全管理器
任何将使用可下载代码的Java程序都应该通过调用System.setSecurityManager ( )来设
置安全管理器,安全管理器保证了任何远程下载的类(通过由RMI提供的代码基),不会
执行未被允许的操作。
如果没有设置安全管理器,则除了那些在应用的CLASSPATH中可找到的类之外其他的类不
会被下载。因此,如果只是在本地进行测试而未设置安全管理器,程序仍可能会正常工
作,因为那些必需的类可以在CLASSPATH的设置中找到,但到了多机环境中,程序将毫无
疑问地要失败。
5.2.4 注意安全策略
在使用安全管理器运行一个程序时,Java 2安全机制将缺省地使用一个标准的安全策略
。不幸的是,此标准安全策略经常过于严格,使得代码基应用不能运行。因此在大多数
情况下需要指定一个新的策略文件以保证程序的运行。
在本章的例子中,我使用了一个十分随意的安全策略。它允许例子应用可自由访问所有
的资源。当然这种策略只适用于测试"已知"的代码(即所编写的代码),根本不适用于
产品环境。
5.2.5 注意CLASSPATH
读者可能已经注意到,这些开发的建议大部分都是集中于避免对资源进行意料外的共享
-主要是指以不确定或健壮性差的方式共享资源,如文件URL或共享HTTP服务器。两个应
用程序共享代码的另一种方法(这种方法也是我们较熟悉的),是在同一台机器上运行
应用,设置相同的CLASSPATH。而如果客户和服务器不是共享磁盘上的类文件,则不可能
辨别这些程序需要远程访问哪些类。因此为防止出现对类文件不可预测的共享,可以考
虑在运行时根本不要CLASSPATH。相反地,可以传递-cp参数给java字节码解释器。这个
选项允许指定一列可从中下载类文件的目录和JAR文件。即使是在同一个机器上开发客户
和服务程序,也可以把它们分别放在不同的目录,取消CLASSPATH的设置,为java解释器
传递不同的-cp参数以保证在应用程序间不会存在意料外的交互。
另外,开发者要习惯于只提供那些应用程序完成任务必需的类,而不是提供所有的类"以
保证不缺少"。 一开始可以只使用三个Jini的JAR文件作为-cp参数(jini-core.jar,j
ini-ext.jar和sun-util.jar),然后再根据需要加入特殊的应用类文件。编译器可帮助
确定哪些类文件需要安装,它在不能找到所需的类时会报告相应信息。
任何情况下都不需要把带-dl的JAR文件(reggie-dl.jar等)从Jini分发软件中放到自己
的类目录中,它们是从核心Jini服务中动态下载的。如果你把这些类与应用类放在了一
起,那么只能使你使用的这些文件的版本是从Jini中取得的,而不是查找服务期望你使
用的版本。
5.2.6 考虑把可下载代码捆绑为一个JAR文件
要做到隔离程序中任何不想要的相关性,只提供其他程序需要下载的代码,或许最好的
做法是创建一个只包含客户在使用程序时必须下载的类的JAR文件。这也是Jini核心服务
采用的策略。例如查找服务,实现它所需的类都包含在reggie.jar文件中,而需要下载
到客户端的类都在reggie-dl.jar文件中。输出查找服务可下载代码的HTTP服务器将自己
的根目录设置为包含reggie-dl.jar文件的目录,查找服务的代码基属性提供相应HTTP服
务器的URL用以指明客户可在哪里下载reggie-dl.jar中的类。
可以考虑把类分为实现部分和可下载部分,分别为它们创建JAR文件,这样在需要移动服
务,或改变客户下载代码的地址时会带来很多方便。
5.2.7 小结
设计这些提示是为了模拟运行在不同机器上的两个Jini程序间可能存在的隔离,它们不
能共享共同的文件系统和CLASSPATH。安全性必须要考虑,因为程序要到其他机器上访问
资源。另外在真正的部署设置中,不能保证有一个全局的HTTP服务器包含所有的可下载
类文件,相反,应该计划为每个服务建立类文件,并且通过单独的HTTP服务器来访问。
这样设置开发环境有些麻烦,但在开发更加复杂的服务和应用时,这样的设置会带来很
多好处。
现在已了解了开发Jini软件的策略,那么就开始编写一些实际的代码吧!
5.3 第一个Jini程序:Hello, World
先来看一个最简单的Jini程序。这里将创建一个简单的服务和一个使用此服务的客户应
用,然后看一下它们是如何工作的。第一件要做的工作是定义一个接口,如程序清单5-
1所示,此接口定义了服务的功能。服务代理对象将实现此接口,而客户在找到查找服务
后就可以使用它。在本书所有的例子中,都使用了一个特别的包的名称,它可以从Pren
tice-Hall的FTP服务器上以FTP的方式获得,使用这样的名称可以把各章的代码区分开。
程序清单5-1 HelloWorldServiceInterface.java
这个接口定义了一个简单的方法,当调用者请求一个消息时,返回一个字符串。
下面来看一下服务,在这个例子中,"服务"就是代理对象,它可以自己返回一个字符串
而无需调用任何设备或后端进程。
不过我们需要有一个寻找查找服务并公布代理的进程,在下一个例子中,此进程还要处
理代理的租借,以保证查找服务能保持此代理。这个进程称为"包装"进程,它的main (
)例程用于完成与Jini的互操作,以发布代理。
关于这个例子有几点需要注意,这几点也是在本书剩余部分的例子中要遵循的约定。首
先,大部分的Jini代码都位于net.jini包中,这种命名机制遵循了把域名反序作为包名
的约定。Bill Joy领导的Sun的研究室之一Aspen SmallWorks在1997年初注册了"jini.n
et"的域名。
其次,Jini划分了包的名称集,区分了哪些是系统的内核部分,哪些是在内核基础上进
行的开发。因此,net.jini.core.*包包括了内核,组成系统核心的标准接口。这些包在
jini-core.jar文件中。net.jini.*包含了使用内核包建立的库,这些代码在jini-ext.
jar中。最后,在com.sun.jini.*包中Sun提供了一组"帮助者"类,它们可被认为是"非标
准的",需要改动。这些类在sun-util.jar文件中。
在程序清单中,我将遵照明确的约定来引入各Jini类,这样读者就可以清楚区分哪些类
在哪个包中。
程序清单5-2给出的代码不足100行,它表示了如何参与到Jini群体中去。
程序清单5-2 HelloWorldService.java
我们将逐步分析这个例子,首先要做的是引入所需的Jini类以使用发现和查找,这里我
们将使用许多类,它们主要来自于net.jini.core.lookup和net.jini.discovery包。
5.3.1 实现服务代理
在引入类之后,是服务代理类的定义,这里称为HelloWorldServiceProxy。关于这个类
有几点要注意:
* 它实现了java.io.serializable接口,这对于服务代理是需要的。因为类的一个实例
要被"处理"并发送到要用到的查找服务,然后从查找服务送到每个客户。序列化意味着
代理可被保存到字节流中,向下发送一个套接字到远端系统,在末端进行重建。
* 服务代理还实现了一个接口HelloWorldServiceInterface,对于客户它是已知的,原
因是在这个例子中,客户程序将在查找服务中寻找实现了这个接口的服务代理。通常情
况下,应该尽量使用容易理解的接口以使客户可以明白服务到底能做些什么。也就是说
,客户必须知道要请求什么。
* 服务代理有一个公共的、无参数的构造方法,这是所有可被序列化和取消序列化的类
都需要的。
* 在这个例子中,代理被声明为"顶层" 的非公共(public)类,与被用做"包装"应用来
发布它的类在同一个文件中。这里的安排是恰当的,因为客户不需要在编译时访问代理
,所以在这里将其隐藏不会存在问题 。客户在运行时通过序列化和代码下载访问代理,
这里甚至可以把代理声明为嵌套的内部类,而只留一个接口-内部类对创建它们的类的实
例有一个"隐含"的引用。当对象被序列化时,所有内部非暂态、非静态的引用也被序列
化,这将使得包装类与代理捆绑在一起,这当然不是我们所期望的。如果把代理声明为
内部类,一定要保证是静态的(static)内部类以避免此问题。内部类的静态是指类的
嵌套只是为了构造上的方便,而不是为了建立内部与外部类在运行时的联系 。静态内部
类对包含它们的类没有"隐含"的引用,因此包装类也不会被序列化。
警告:忘记序列化
有时候会发生这种情况,或者忘记了把服务代理序列化,或者是漏掉了无参数的构造方
法,这时编译器不能检测到这样的错误,因此当试图在查找服务中注册此代理时,就会
出现运行错误。这时所产生的异常将是java.io.NotSerializableException。ServiceI
tem类包含有传到注册进程的服务代理,它被定义为持有一个Object类型的实例作为服务
的代理,因此在这里可以传递任何类型的对象,但要注意如果对象不是可序列化的,在
编译时不会有任何警告信息。
5.3.2 "包装"应用程序
现在来看一下用于找到查找服务并公布代理的应用程序。这个程序不是用来帮助代理执
行它的服务,它只是用于帮助管理代理的Jini职责。在这个例子中,完成此功能的类是
HelloWorldService,这个类包含的main ( )函数将启动发现过程。
还有一个与Jini发现系统交互的内部类,即Listener,它实现DiscoveryListener接口。
通过把面向发现的代码分成单独的内部类(而不是只使用HelloWorldService包装类来完
成发现),我们可以使设计更加清楚,也可以把参与到发现中所必须的工作与特殊服务
所需的工作区分开。后面将会回到Listener类,仔细分析它是如何工作的。
在声明了支持发现的内部类之后,就到了程序最重要的部分。HelloWorldService的构造
方法完成了四个任务。首先,它创建了一个ServiceItem的实例,该类的实例将在注册过
程中被送到查找服务。
这个构造方法有三个参数。第一个是服务的标识号,此标识号是全局唯一的,即使在不
同的查找服务中,在不同的时间注册服务,每次注册的标识号也都是相同的。服务需要
记住自己的标识号,这要求它必须留部分空间作好记录。这个服务不是特别良好的,因
为每次运行时它都要请求新的服务标识号。
为了简单地构造一个初始的服务标识号,Jini有一个约定,即如果在第一次注册时传递
null作为服务标识号,则查找服务会分配一个全局唯一的标识号,以后的注册就可以使
用这个标识号。很快我们就会看到在这里的例子是如何做的,而在第8章深入分析查找的
时候,将更详细地讨论服务标识号的生命周期以及如何产生标识号;另外还介绍一些管
理记录的方便办法。
第二个参数是服务代理的实例,这个对象将被序列化,然后在查找服务被发现时发送到
查找服务,等待需要它的客户。这里我把代理对象的构造放在了其自己的方法createPr
oxy ( )中,看起来更加清楚,而且也方便将来的子类可以覆盖这个方法。
最后一个参数这里是null,它可以放置一些与服务代理相关的属性,感兴趣的客户可通
过这个属性搜寻服务。例如,如果注册了一个打印机,则可以把打印机的位置和型号作
为属性放在这里,客户程序就可以取到这些信息显示给用户,或者用程序方式找到一个
合适的打印机。这个参数被规定为对象数组的类型,数组中的对象实现Entry接口。在以
后的例子中将看到使用属性的情况,现在暂时不在服务代理中附加属性。
在创建了ServiceItem类,可以在所发现的查找服务中进行注册后,接下来应该设置安全
管理器。注意所有的Java程序都要下载代码(指的是从其他地方而不是在本地目录通过
CLASSPATH完成),必须用安全管理器来保证程序不会执行未被授权的操作。由于服务要
从所有可以找到的查找服务那里下载代理对象,因此也必须在相应的地方设置安全管理
器。
在设置了安全管理器之后,将要创建一个LookupDiscovery的实例,这个类提供了使用J
ini发现协议的机制,使用起来十分简单。由于我们希望当找到一个是公共组成员的查找
服务时能够被告知,因此在传递组名时使用了空字符串("组"只是给Jini群体的一个名
称,按照规定,以空字符串命名的组被认为是公共的群体,所有服务都按缺省加入。在
多数情况下,使用传递空字符串的方法是合适的。否则如果运行一个试验性的服务,可
以用字符串"experimental"作为参数来寻找相应群体,不过同时在网络上运行的查找服
务也应该配置为支持此组名)。
在发现一个新的查找服务时,LookupDiscovery对象会调用DiscoveryListener,嵌套的
Listener类实现了这个接口,因此HelloWorldService的构造方法就添加一个新的Liste
ner实例作为发现的接收器。此后,发现子系统将在找到查找服务时,将调用Discovery
Listener方法上的Listener实例。
注意:发现API是异步感知
显然,如果想实现这样一个Jini服务,此服务在有新的查找服务出现时可被异步通知,
那么就要实现一些接收器接口以便在启动查找服务时,Jini库可回调相应程序。
但如果只是想与已运行的查找服务进行连接,为什么要使用"异步"接口呢(在主控制流
之外进行通知)?能不能使用"同步"API,在从查找服务返回之前一直阻塞?
Jini API对于发现只支持异步的方式。原因在于,几乎在所有情况下,Jini服务都需要
能够处理在其后启动的查找服务,因此使用异步API有一些好处。第一,因为服务必须能
理解查找服务的通知,因此这种方式有利于行为的"正确"。第二,它意味只有一种方式
可获得查找服务的信息,因此服务程序被简化(与同时具有阻塞和非阻塞API的情况比)
。第三,有可能即使知道了查找服务实际上已在网络中运行,但不知道它多长时间才会
响应,因此也就无法知道要阻塞多长时间进行等待,还是用异步API比较合适。
通常,好的API设计有利于创建正确的应用程序,而使应该简单的问题不被复杂化也是J
ini设计的一个重要方面。在后面各章遇到API设计问题时,我会重点解释。
5.3.3 使用发现和查找
现在来看看如何创建支持发现的程序。前面说这里的程序使用了一个称为Listener的内
部类来执行发现,这个类实现了DiscoveryListener接口,接口中有两个方法discovere
d ( )和discarded ( )。在找到了与寻找的查找服务相匹配的组时,前一种方法被调用
;而在原来发现的查找服务不再可用时,调用后一种方法。发生这种情况通常是因为查
找服务停止了响应,因此在LookupDiscovery上调用了discard ( )方法,告诉它已经把
该查找服务从已找到的集合中除去了。在改变了要搜寻的组时,discard ( )也经常被调
用,因为这种改变使得原来找到的查找服务不能再为新的组服务,需要丢弃。这两个方
法都需要传递一个DiscoveryEvent对象,用于描述要被查找或丢弃的查找服务。
提示:理解何时丢弃查找服务
有两种情况需要调用DiscoveryListener中的discarded ( )方法。第一,在要搜寻的组
发生变化,而且LookupDiscovery需要通知你在原来拥有其引用的查找服务中,哪些对当
前的组不再可用;第二,在所管理的组中明确丢弃某个查找服务。第二种情况更常见。
为什么要明确地丢弃一个查找服务呢?如果你正在查找服务上执行一个操作时接到了一
个RemoteException异常,有可能是查找服务被停掉了,或者是不可访问,这时你要在L
ookupDiscovery上调用discard ( )方法以通知该类把查找服务从已发现的服务组中丢弃
。LookupDiscovery将调用discarded ( )的实现以完成状态的清理。
丢弃一个失败了的查找服务,意味着在该查找服务返回网络时还要重新发现它。
对于初级Jini程序员来说,对如何调用discarded ( )方法产生误解是常见的错误。他们
常错误地认为,是查找服务的崩溃,或网络传输的错误,使得discarded ( )方法被调用
,事实并非如此。在查找服务被发现之后,就不再有与它的通信,除非使用某些方法调
用它,因此对于报告错误的查找服务,也要主动将其丢弃。
下面看一下例子中discovered ( )的实现。这里使用了一个hash表来记录所发现的所有
查找服务。当发现协议报告说找到了新的查找服务时,将它从所得到的Discovery Even
t中取出,检查一下hash表确认表中是否还没有此查找服务,若它确实是新加入的,则将
它存在hash表中,调用registerWithLookup ( ),在查找服务中注册。
注意:冗余的查找服务
在例子中,对已找到的查找服务进行记录,以避免出现重复的注册。实际上这并非必须
,同一个查找服务中的第二次注册只是简单地覆盖第一个。
但是对注册情况进行记录有利于管理租借(本章后面将涉及到),因此采用hash表进行
记录主要是考虑到服务的租借行为-一般我们也关心。当然,保证没有冗余的注册也可以
减少一点网络流通量。
在一个查找服务中进行注册的过程十分简单:只要取到与所发现的查找服务相关的Serv
iceRegistrar实例,然后在此实例上调用register ( )方法。正如我们所见到的,这个
调用有两个参数,第一个是先前创建的服务项目(item),第二个是为租约所请求的持
续时间,这个时间是请求查找服务在不能得到我们的消息的情况下仍能持有代理的时间
量。在此例中,我们请求了10分钟的租约。前面我们曾提到,作为租约的授权方,查找
服务可以拒绝请求,或是只提供更短时间的租约。此次调用之后,租约的状态在Servic
eRegistration对象中返回。这个返回值允许我们在其上进行多个有用的操作,我们可以
获得新注册服务的ID-这也是获得第一次运行时被分配的服务ID的方法;还可以获得与注
册相关的Lease对象,修改与服务项目有关的属性。
提示:在一个单独的线程中注册
在这个例子中,registerWithLookup ( )的工作与发现过程在同一个线程中进行,这样
当有DiscoveryEvent发生时就执行注册,然后就占据那个线程直到注册结束。
在大多数情况下,这项工作应该在单独的控制线程中完成,原因是注册过程包括一个远
程调用,可能会用较长的时间才能返回。例如,查找服务可能崩溃,这意味注册操作只
有等到超时才能结束。
这样一来在注册过程进行时,其他的查找服务就不能被发现,查找服务只能在一定时间
段内被发现。一个比较好的做法是使用单独的、短时间运行的线程来处理注册任务。这
里采用的方法只适用于确实希望在进行注册时中断发现任务的情况。不过这里的程序仍
适于描述如何进行发现和注册的基本原理。
在注册了服务代理之后,要检查是不是第一次被注册。如果是,把返回的服务ID写到服
务项目中,这样将来的注册就会使用相同的ID。同时还把注册的结果保存到一个hash表
中,以便在将来需要时取出。
这个程序最大限度地支持服务在它能遇到的查找服务中为不同的组注册,这样如果在一
个网络中运行了多个查找服务,自然就有了冗余性。
注意这个例子只是表示了注册一个服务至少需要的工作,显然还可以做许多相当重要的
改进,这些工作将在以后各部分逐渐进行,最主要的是关于租借的内容。到此为止,这
个程序可以注册一个服务并保持10分钟的时间(或是查找服务分配的注册时间段)。另
外,现在还没有错误检查的工作,例如在register ( )方法失败时会怎么办?注意,注
册过程被定义为如果重复注册一个服务,则它只是覆盖以前的注册。因此,在失败之后
进行重试是安全的,但在此程序中在一次尝试之后就放弃了。以后还将解决很多类似的
问题,但当前最重要的是理解基本的概念。
5.3.4 其他细节
程序的其他部分就很平常了。服务的类实现了Runnable,是为了保证应用不会在执行完
main ( )后就停止工作了。如果在应用中没有活跃的(非守护进程)线程,则在main (
)执行完之后,应用就会终止,可能查找服务根本就来不及向我们报告自己的存在。因
此我们的服务实现了Runnable,其中启动一个简单的线程然后就睡眠。如果是在应用中
做AWT或Swing工作,则没有必要启动自己的线程,因为在创建窗口时Java的窗口系统会
启动这样的线程。不过因为这里不做图形,所以要启动一个自己的线程。注意后端的程
序会永久运行,除非将其杀掉。这里没有提供使这个线程停止工作的方法。
main ( )例程只是创建一个HelloWorldService实例,然后启动后台的进程使应用保持运
行。
下面看一个可使用此服务的简单客户应用程序(程序清单5-3)。
程序清单5-3 HelloWorldClient.java
这个客户程序和服务程序有些相似。它也有一个内部类实现DisvoveryListener,因为它
必须参与到发现协议中;它也启动了一个后台线程以保证主程序不会在找到一个查找服
务后就退出;它也安装了安全管理器,因为它需要从HelloWorldService下载并使用服务
代理。
不同的是,客户程序不是注册一个服务,而是寻找实现了HelloWorldServiceInterface
的服务代理。当discovered ( )被调用时,说明有一个或多个查找服务通过发现协议被
找到。对每个查找服务,我们都将寻找所需要的服务。我们通过ServiceTemplate对象来
完成这项工作,使用此对象是描述要寻找的服务的一种方法。
5.3.5 使用服务模板来寻找服务
每个ServiceTemplate中有三个域,控制着如何去搜寻,下面是ServiceTemplate定义的
一部分。
public class ServiceTemplate {
public ServiceID serviceID;
public Class[ ] serivceType;
public Entry[ ] attributeSetTemplates;
//... other details elided ...
}
ServiceTemplate的代码就像是查询的规范。当向查找服务器传递一个ServiceTemplate
时,查找服务器就搜索其注册的所有服务进行匹配。在下列情况下,模板与服务匹配:
* 模板中的服务ID与注册服务的ID相匹配;或者模板中的服务ID域为空,并且
* 注册的服务是模板serviceType域中各种类型的实例或子类型,或者模板的类型域为空
,并且
* 服务的属性列表中至少包含一个属性可与模板中属性域的各项匹配,或者模板的属性
域为空(关于属性"匹配"的精确定义在第7章讨论)。
通过使用ServiceTemplate,可以搜寻一定的服务,这些服务或者与已知的类或接口相匹
配,或者具有一组所需属性,或者有指定的服务ID(在ID知道的情况下)。因为匹配过
程考虑了Java类型的关系,因此也可以搜索是已知类型子类型的服务。例如,若类B扩展
了类A,而A在模板中,则搜索的结果会返回在查找服务中注册的所有类型A或类型B的对
象。把模板中的一个域设为空,就意味着它是通配值。所有域都设为空的模板将匹配所
有的服务。
提示:寻找查找服务的代理
查找服务的代理对象就是其自身,它和其注册的服务存储在一起,因此当匹配所有的服
务时,对lookup ( )的调用把查找服务本身的引用与其他在查找服务中注册的服务一起
返回。不过许多用户希望lookup ( )只返回除查找服务外的其他服务。
当然,也可以把ServiceRegistrar实现为在搜寻代理时只返回查找服务的代理。
在上面的例子中,创建了一个基于服务类型进行搜索的模板,该模板由一列只包含所需
接口类的Class初始化,把服务ID和属性域留为空,意思是我们的查找将匹配所有实现了
HelloWorldServiceInterface的服务,不管它的服务ID和相关联的属性如何。
5.3.6 查找一个服务
我们通过在代表查找服务的ServiceRegistrar对象上调用lookup ( )方法来执行事实上
的查找,这个方法与查找服务交互,执行搜寻任务。如果找到了匹配的服务,查找服务
就把该服务的代理对象传送到客户端。这个方法要么是返回服务的代理,取消序列化后
准备使用,要么是在无匹配的情况下返回null。
注意,在一个给定的Jini群体中完全有可能有多个服务实现了我们需要的接口,而且对
于我们提出的任意查询都可能存在多个匹配,但这里使用的lookup ( )调用方式只返回
一个匹配,一般是所找到的第一个匹配的服务。还有另一种实现lookup ( )方法的方式
,即另外用一个参数指出希望返回的最大匹配数。
不过在通常情况下,对给定的查询只返回第一个匹配结果是比较好的做法。如果在查询
时只给出了要查找的接口,则明显地是告诉查找服务接口是我们关心的唯一问题,这对
所有的HelloWorldServiceInterface都是一样的效果。同样地,如果查询条件是"三楼上
任何工作正常的彩色打印机",则任一个能满足条件的打印机都是好的选择。
如果需要十分特殊的服务,使用ServiceTemplate中可用的匹配工具不容易一下子匹配出
来,则可以使用普通的查询取得一组匹配结果,然后再精选(有可能是呈现给用户)得
到找出所需的特殊服务。不过像浏览器这样的客户一般希望能得到可以找到的所有服务
。
对我们的应用来说,得到一个匹配就足够了。得到匹配结果之后,我们就可以将其转化
为已知的类型并使用它。服务代理十分简单,而实现的细节完全隐藏起来的事实说明,
只要知道其接口,客户就可以与任意复杂的服务进行互操作。
5.3.7 编译并运行例子程序
上面的代码可以敲入,也可以下载,然后就可以编译并运行程序了。在本章的例子中,
将遵照前面所讨论的繁琐的建议,把客户和服务"模拟"运行在不同的主机上。不管读者
是否想尽快跳过这一部分,建议按那些步骤进行。你可以把所有的类都放到开发目录中
并共享一个CLASSPATH。但如果是这样,必须在进一步工作之前仔细检查程序的各个过程
。下面这个工具条列出了在本书中将使用的约定。
工具条:本书使用的约定
本书中所有的代码都要遵循前面所列举的原则,这只是为更好地在不同机器上运行客户
和服务。有些原则对于你来说可能是额外的工作,毕竟你想全力投入到编程中。
但刚起步的Jini程序员遇到的最多的问题就是要处理安全和代码下载的问题,如果不遵
守那些原则,这些问题就容易被隐藏。
在本书所有关于创建和测试代码例子的指南中,对于把代码放于何处以及如何运行服务
等问题,我都遵循了一组约定。
大部分的例子都包含两部分-服务和使用服务的客户。为了保证服务和客户不会共享类文
件,把二者的类文件分开放在不同的目录中。
同样,在大部分例子中,服务和客户都可能需要向Jini群体中的其他实体输出可下载代
码,例如对于服务,需要把代理对象输出到Jini查找服务,以供客户使用,客户也经常
需要为它的远程事件接收器输出可下载代码。在所有的例子中,都为每个实体要输出的
可下载代码创建和使用了分开的目录,因此客户的可下载部分与客户代码的其他部分在
不同的目录中,服务程序也一样。
为模拟每个可下载代码包在不同机器上的效果,将使用不同的HTTP服务器分别输出服务
器输出目录和客户输出目录中的代码,每个服务器的"根"目录都设为指向相应包含可下
载代码的目录。这些HTTP服务器可运行在同一台机器上,只是使用不同的端口号。在本
书中,使用不同的端口号分别运行服务和客户的HTTP服务器,端口8085用于服务,端口
8086用于客户。客户和服务器的代码基属性将分别指向输出其代码的HTTP服务器实例。
表5-1给出了使用目录的约定,包括Windows系统和Solaris系统。
表5-1 类文件目录的约定
目 录 内 容 Windows中的位置 Solaris中的位置
实现服务所需的类文件 C:\service /files/service
服务的可供其他实体下载的类文件 C:\service-dl /files/service-dl
实现客户所需的类文件 C:\client /files/client
客户的可供其他实体下载的类文件 C:\client-dl /files/client-dl
在编译客户时,编译器类路径的设置不仅要指向三个Jini的JAR文件,而且要指向客户实
现的目录,就像上表中指出的一样。只有这样,编译器才能找到并使用为客户创建的类
文件。同样在编译服务时,也设置类路径使之指向三个Jini JAR文件和服务的实现代码
所在的目录。
这样安排的一个好处就是便于在这些不同的目标中创建JAR文件(另外便于分别维护客户
和服务使用的类文件)。如果把自己的程序按这种方式划分,就可以在每个目录中将客
户或服务的类文件分开创建为实现和供下载的JAR文件,就像Jini的参考实现中那样(如
reggie.jar和reggie-dl.jar)。
可以参考本章前面的5.2节"按部署情况进行开发",了解这些约定的理论基础。
让我们先做好建立并运行例子程序的准备。第一,去除CLASSPATH的设置,保证没有外部
的类文件通过这种方式进入服务或客户,我们将通过命令行来设置寻找类的路径。第二
,保证例子的源码都放在一个熟悉的地方,建议在Windows中使用C:\files\corejini目
录,在Solaris或其他UNIX系统中使用/files/corejini目录。
1. 编译
接下来编译程序。我们希望把客户和服务的类文件分开,因为通常情况下,客户和服务
在编译时不共享代码,除了一些服务代理要实现的共享接口外彼此不必有对方的信息。
还需要把用来实现程序的代码与要下载到其他程序的代码区分开。在本例中,应用程序
的可下载代码只有代理对象。当然客户和服务都需要能下载查找服务的代理,不过在启
动查找服务时,应该已经在网络中配置了输出reggie-dl.jar文件的HTTP服务器。
为将代码分离开,将设置javac编译器使之把类文件输出到单独的目录中。事实上,我们
将为客户方的实现创建client目录,为服务方的实现创建service目录,为服务输出的可
下载代码创建service-dl目录(这是客户将使用的可下载代码,注意这里使用的目录命
名约定)。在Windows下,可在C:\中创建这些目录,在Solaris系统中,可在/files下创
建,然后用-d参数通知编译器把创建的类文件放在相应目录。
在Windows系统中:首先创建所需的目录,然后进入包含待编译源码的目录。
mkdir C:\client
mkdir C:\service
mkdir C:\service-dl
cd C:\files\corejini\chapter5
接下来编译服务需要的源码,服务基于HelloWorldService.java和HelloWorldService
Interface.java两个文件,-d选项用于说明把结果类文件放到C:\service。
javac -classpath C:\ jini1_0\lib\jini-core.jar;
C:\jini1_0\lib\jini-ext.jar;
C:\jini1_0\lib\sun-util.jar;
C:\service
-d C:\service
C:\files\corejini\chapter5\HelloWorldServiceInterface.java
C:\files\corejini\chapter5\HelloWorldService.java
下面编译客户需要的源码,客户同样依赖于HelloWorldServiceInterface.java,另外还
有HelloWorldClient.java,我们将结果文件放在C:\client中。
java-classpath C
--
海纳百川,
有容乃大,
壁立千尺,
无欲则刚。
※ 来源:·哈工大紫丁香 bbs.hit.edu.cn·[FROM: dip.hit.edu.cn]
Powered by KBS BBS 2.0 (http://dev.kcn.cn)
页面执行时间:202.046毫秒