Java和COM最引人注目的相似之处就是COM接口与Java的“interface”关键字的关系。这是接近完美的一种相符,因为:
接下来,让我们对COM开发的一些关键问题进行讨论。编写Java/COM客户和服务器时,这些问题是首先需要弄清楚的。
COM是一种二进制规范,致力于实施可相互操作的对象。例如,COM认为一个对象的二进制布局必须能够调用另一个COM对象里的服务。由于是对二进制布局的一种描述,所以只要某种语言能生成这样的一种布局,就可通过它实现COM对象。通常,程序员不必关注象这样的一些低级细节,因为编译器可自动生成正确的布局。例如,假设您的程序是用C++写的,那么大多数编译器都能生成符合COM规范的一张虚拟函数表格。对那些不生成可执行代码的语言,比如VB和Java,在运行期则会自动挂接到COM。
COM库也提供了几个基本的函数,比如用于创建对象或查找系统中一个已注册COM类的函数。
一个组件对象模型的基本目标包括:
第一点正是面向对象程序设计要解决的问题:我们有一个客户对象,它能向一个服务器对象发出请求。在这种情况下,“客户”和“服务器”这两个术语是在常规意义上使用的,并非指一些特定的硬件配置。对于任何面向对象的语言,第一个目标都是很容易达到的——只要您的代码是一个完整的代码块,同时实现了服务器对象代码以及客户对象代码。若改变了客户和服务器对象相互间的沟通形式,只需简单地重新编译和链接一遍即可。重新启动应用程序时,它就会自动采用组件的最新版本。
但假若应用程序由一些未在自己控制之下的组件对象构成,情况就会变得迥然有异——我们不能控制它们的源码,而且它们的更新可能完全独立于我们的应用程序进行。例如,当我们在自己的程序里使用由其他厂商开发的ActiveX控件时,就会面临这一情况。控件会安装到我们的系统里,我们的程序能够(在运行期)定位服务器代码,激活对象,同它建立链接,然后使用它。以后,我们可安装控件的新版本,我们的应用程序应该仍然能够运行;即使在最糟的情况下,它也应礼貌地报告一条出错消息,比如“控件未找到”等等;一般不会莫名其妙地挂起或死机。
在这些情况下,我们的组件是在独立的可执行代码文件里实现的:DLL或EXE。若服务器对象在一个独立的可执行代码文件里实现,就需要由操作系统提供的一个标准方法,从而激活这些对象。当然,我们并不想在自己的代码里使用DLL或EXE的物理名称及位置,因为这些参数可能经常发生变化。此时,我们想使用的是由操作系统维护的一些标识符。另外,我们的应用程序需要对服务器展示出来的服务进行的一个描述。下面这两个小节将分别讨论这两个问题。
Unique IDentifier,全局唯一标识符),可由特殊的工具生成。此外,这些数字可以保证在“任何空间和时间”里独一无二,没有重复。在空间,是由于数字生成器会读取网卡的ID号码;在时间,是由于同时会用到系统的日期和时间。可用GUID标识COM类(此时叫作CLSID)或者COM接口(IID)。尽管名字不同,但基本概念与二进制结构都是相同的。GUID亦可在其他环境中使用,这里不再赘述。
GUID以及相关的信息都保存在Windows注册表中,或者说保存在“注册数据库”(RegistrationDatabase)中。这是一种分级式的数据库,内建于操作系统中,容纳了与系统软硬件配置有关的大量信息。对于COM,注册表会跟踪系统内安装的组件,比如它们的CLSID、实现它们的可执行文件的名字及位置以及其他大量细节。其中一个比较重要的细节是组件的ProgID;ProgID在概念上类似于GUID,因为它们都标识着一个COM组件。区别在于GUID是一个二进制的、通过算法生成的值。而ProgID则是由程序员定义的字串值。ProgID是随同一个CLSID分配的。
我们说一个COM组件已在系统内注册,最起码的一个条件就是它的CLSID和它的执行文件已存在于注册表中(ProgID通常也已就位)。在后面的例子里,我们主要任务就是注册与使用COM组件。
注册表的一项重要特点就是它作为客户和服务器对象之间的一个去耦层使用。利用注册表内保存的一些信息,客户会激活服务器;其中一项信息是服务器执行模块的物理位置。若这个位置发生了变动,注册表内的信息就会相应地更新。但这个更新过程对于客户来说是“透明”或者看不见的。后者只需直接使用ProgID或CLSID即可。换句话说,注册表使服务器代码的位置透明成为了可能。随着DCOM(分布式COM)的引入,在本地机器上运行的一个服务器甚至可移到网络中的一台远程机器,整个过程甚至不会引起客户对它的丝毫注意(大多数情况下如此)。
由服务器展示出来的COM函数会返回一个值,采用预先定义好的HRESULT类型。HRESULT代表一个包含了三个字段的整数。这样便可使用多个失败和成功代码,同时还可以使用其他信息。由于COM函数返回的是一个HRESULT,所以不能用返回值从函数调用里取回原始数据。若必须返回数据,可传递指向一个内存区域的指针,函数将在那个区域里填充数据。我们把这称为“外部参数”。作为Java/COM程序员,我们不必过于关注这个问题,因为虚拟机会帮助我们自动照管一切。这个问题将在后续的小节里讲述。
如果想写一个Java/COM客户,必须经历一系列不同的步骤。Java/COM“客户”是一些特殊的Java代码,它们想激活和使用系统内注册的一个COM服务器。同样地,虚拟机会与COM服务器沟通,并将它提供的服务作为Java类内的各种方法展示(揭示)出来。另一个Microsoft工具是jactivex,它能读取一个类型库,并生成相应的Java源文件,在其中包含特殊的编译器引导命令。生成的源文件属于我们在指定类型库之后命名的一个包的一部分。下一步是在自己的COM客户Java源文件中导入那个包。
接下来让我们讨论两个例子。
本节将介绍ActiveX控件、Automation服务器或者其他任何符合COM规范的服务器的开发过程。下面这个例子实现了一个简单的Automation服务器,它能执行整数加法。我们用setAddend()方法设置addend的值。每次调用sum()方法的时候,addend就会添加到当前result里。我们用getResult()获得result值,并用clear()重新设置值。用于实现这一行为的Java类是非常简单的:
public class Adder { private int addend; private int result; public void setAddend(int a) { addend = a; } public int getAddend() { return addend; } public int getResult() { return result; } public void sum() { result += addend; } public void clear() { result = 0; addend = 0; } }
为了将这个Java类作为一个COM对象使用,我们将Javareg工具应用于编译好的Adder.class文件。这个工具提供了一系列选项;在这种情况下,我们指定Java类文件名("Adder"),想为这个服务器在注册表里置入的ProgID("JavaAdder.Adder.1"),以及想为即将生成的类型库指定的名字("JavaAdder.tlb")。由于尚未给出CLSID,所以Javareg会自动生成一个。若我们再次对同样的服务器调用Javareg,就会直接使用现成的CLSID。
javareg /register /class:Adder /progid:JavaAdder.Adder.1 /typelib:JavaAdder.tlb
Javareg也会将新服务器注册到Windows注册表。此时,我们必须记住将Adder.class复制到Windows\Java rustlib目录。考虑到安全方面的原因(特别是涉及程序片调用COM服务的问题),只有在COM服务器已安装到trustlib目录的前提下,这些服务器才会被激活。
现在,我们已在自己的系统中安装了一个新的Automation服务器。为进行测试,我们需要一个Automation控制器,而Automation控制器就是Visual Basic(VB)。在下面,大家会看到几行VB代码。按照VB的格式,我设置了一个文本框,用它从用户那里接收要相加的值。并用一个标签显示结果,用两个下推按钮分别调用sum()和clear()方法。最开始,我们声明了一个名为Adder的对象变量。在Form_Load子例程中(在窗体首次显示时载入),会调用Adder自动服务器的一个新实例,并对窗体的文本字段进行初始化。一旦用户按下“Sum”或者“Clear”按钮,就会调用服务器中对应的方法。Dim Adder As Object Private Sub Form_Load() Set Adder = CreateObject("JavaAdder.Adder.1") Addend.Text = Adder.getAddend Result.Caption = Adder.getResult End Sub Private Sub SumBtn_Click() Adder.setAddend (Addend.Text) Adder.Sum Result.Caption = Adder.getResult End Sub Private Sub ClearBtn_Click() Adder.Clear Addend.Text = Adder.getAddend Result.Caption = Adder.getResult End Sub
注意,这段代码根本不知道服务器是用Java实现的。
运行这个程序并调用了CreateObject()函数以后,就会在Windows注册表里搜索指定的ProgID。在与ProgID有关的信息中,最重要的是Java类文件的名字。作为一个响应,会启动Java虚拟机,而且在JVM内部调用Java对象的实例。从那个时候开始,JVM就会自动接管客户和服务器代码之间的交流。
jactivex /javatlb JavaAdder.tlb
Jactivex完成以后,我们再来看看自己的windows/java/trustlib目录。此时可在其中看到一个新的子目录,名为javaadder。这个目录包含了用于新包的源文件。这是在Java里与类型库的功能差不多的一个库。这些文件需要使用Microsoft编译器的专用引导命令:@com。jactivex生成多个文件的原因是COM使用多个实体来描述一个COM服务器(另一个原因是我没有对MIDL文件和Java/COM工具的使用进行细致的调整)。
名为Adder.java的文件等价于MIDL文件中的一个coclass引导命令:它是对一个COM类的声明。其他文件则是由服务器揭示出来的COM接口的Java等价物。这些接口(比如Adder_DispatchDefault.java)都属于“遣送”(Dispatch)接口,属于Automation控制器与Automation服务器之间的沟通机制的一部分。Java/COM集成特性也支持双接口的实现与使用。但是,IDispatch和双接口的问题已超出了本附录的范围。
在下面,大家可看到对应的客户代码。第一行只是导入由jactivex生成的包。然后创建并使用COM Automation服务器的一个实例,就象它是一个原始的Java类那样。请注意行内的类型模型,其中“例示”了COM对象(即生成并调用它的一个实例)。这与COM对象模型是一致的。在COM中,程序员永远不会得到对整个对象的一个引用。相反,他们只能拥有对类内实现的一个或多个接口的引用。“例示”Adder类的一个Java对象以后,就相当于指示COM激活服务器,并创建这个COM对象的一个实例。但我们随后必须指定自己想使用哪个接口,在由服务器实现的接口中挑选一个。这正是类型模型完成的工作。这儿使用的是“默认遣送”接口,它是Automation控制器用于同一个Automation服务器通信的标准接口。欲了解这方面的细节,请参考由Ibid编著的《Inside COM》。请注意激活服务器并选择一个COM接口是多么容易!import javaadder.*; public class JavaClient { public static void main(String [] args) { Adder_DispatchDefault iAdder = (Adder_DispatchDefault) new Adder(); iAdder.setAddend(3); iAdder.sum(); iAdder.sum(); iAdder.sum(); System.out.println(iAdder.getResult()); } }
现在,我们可以编译它,并开始运行程序。
Automation(安全数组自动)类型——能与ActiveX控件在一个较深的层次打交道,并可控制COM异常。
由于篇幅有限,这里不可能涉及所有这些主题。但我想着重强调一下COM异常的问题。根据规范,几乎所有COM函数都会返回一个HRESULT值,它告诉我们函数调用是否成功,以及失败的原因。但若观察服务器和客户代码中的Java方法签名,就会发现没有HRESULT。相反,我们用函数返回值从一些函数那里取回数据。“虚拟机”(VM)会将Java风格的函数调用转换成COM风格的函数调用,甚至包括返回参数。但假若我们在服务器里调用的一个函数在COM这一级失败,又会在虚拟机里出现什么事情呢?在这种情况下,JVM会认为HRESULT值标志着一次失败,并会产生类com.ms.com.ComFailException的一个固有Java异常。这样一来,我们就可用Java异常控制机制来管理COM错误,而不是检查函数的返回值。
如欲深入了解这个包内包含的类,请参考微软公司的产品文档。
JVM会帮助我们考虑到所有的细节。一个ActiveX控件仅仅是一个COM服务器,它展示了预先定义好的、请求的接口。Bean只是一个特殊的Java类,它遵循特定的编程风格。但在写作本书的时候,这一集成仍然不能算作完美。例如,虚拟机不能将JavaBeans事件映射成为COM事件模型。若希望从ActiveX容器内部的一个Bean里对事件加以控制,Bean必须通过低级技术拦截象鼠标行动这类的系统事件,不能采用标准的JavaBeans委托事件模型。
抛开这个问题不管,ActiveX/Beans集成仍然是非常有趣的。由于牵涉的概念与工具与上面讨论的完全相同,所以请参阅您的Microsoft文档,了解进一步的细节。
固有方法为我们带来了安全问题的一些考虑。若您的Java代码发出对一个固有方法的调用,就相当于将控制权传递到了虚拟机“体系”的外面。固有方法拥有对操作系统的完全访问权限!当然,如果由自己编写固有方法,这正是我们所希望的。但这对程序片来说却是不可接受的——至少不能默许这样做。我们不想看到从因特网远程服务器下载回来的一个程序片自由自在地操作文件系统以及机器的其他敏感区域,除非特别允许它这样做。为了用J/Direct,RNI和COM集成防止此类情况的发生,只有受到信任(委托)的Java代码才有权发出对固有方法的调用。根据程序片的具体使用,必须满足不同的条件才可放行。例如,使用J/Direct的一个程序片必须拥有数字化签名,指出自己受到完全信任。在写作本书的时候,并不是所有这些安全机制都已实现(对于Microsoft SDK for Java,beta 2版本)。所以当新版本出现以后,请务必留意它的文档说明。