这里通过一个基于 Borland VisiBroker (4.5.1)
的简单例子,说明基于 CORBA
的基本开发过程,可从网上免费获得软件试用版。
3.1 设计相关的若干问题
首先,简单看一下在系统设计阶段应注意的问题。由于篇幅的限制,此外并不想去考察「软件系统分析设计阶段的普遍问题」,而是重点讨论几个与「基于 CORBA
的分布式系统」相关的问题。
1. 运行平台
设计者必须在设计初期决定,待开发的分布式系统要运行在哪类硬件和软件平台之上。由于不同平台(计算机硬件和操作系统)之间的差异,为一个平台开发的软件系统,通常不能直接运行在另一个平台之上(应注意:尽管 CORBA
提供了异类环境中良好的可互操作性,但这与系统的可移植性是截然不同的两个问题)。一般来说,设计者总是在「软件系统的性能与通用性的矛盾」之间,作一个折衷的选择:要使所开发的软件系统具备良好的通用性,能够方便的在不同平台上迁移,就需要尽可能的避免使用特定平台(比如某个厂商的平台或某个操作系统)相关的机制,而较好的性能在很多情况下,都要借助于特定平台的特性才能获得。
所以,设计者要根据自己特定项目或系统的特点与需求,作出一个折衷的选择。
2. 调用方式
在 CORBA
中,分布式对象提供的服务的调用方式可有三种:
- 同步方式:调用时,调用者会阻塞,直到被调用的服务完成并返回。
- 异步方式:调用者发起调用后不会阻塞,等待服务完成期间,可以执行其它操作。调用者通过轮询方式、或服务者发送的事件检测调用完成,服务完成后调用者检查并处理结果。异步方式通常依靠异步消息来实现。
- 单向方式:调用者只是发出调用请求,并不关心调用什么时候完成(以及完成的结果)。
不同的调用方式适合不同的应用场合。
- 客户程序的请求所引起的服务程序操作,只需要很短时间即可完成,例如:查询某一帐户的当前余额,这时应选用同步通信方式。
- 如果客户程序请求服务程序格式化并打印几个大型文档,服务程序需要较长时间才可完成该请求,这时应选用异步通信方式,从而在服务程序格式化并打印文档期间,客户程序可以做一些其他事情。
- 如果客户程序无需获知请求已完成的确认信息,例如:向系统日志模块登记系统执行了某一操作,则应选用单向通信方式。
3. 资源优化
在分布式环境下,跨网络的通信开销是相当可观的,通常是影响系统整体效率的瓶颈。而在 Stub/Skeleton
机制的支撑下,开发者已经不需要自己编程处理底层通信,分布式对象在开发时,并没有表现出很强的分布特性,这更容易使设计者忽略跨网络的通信对系统的影响。
在一个集中式软件系统中,程序员可在同一进程中随意地连续多次调用一个例程,因为这些调用的系统资源开销微不足道。但在分布式环境下,同样的调用如果发生在跨网络的进程之间,这些调用占用的系统资源是相当可观的。因此在设计阶段,特别是在接口详细设计阶段,应考虑尽量提高网络通信资源的利用率,避免频繁的跨网络(尤其是广域网)通信,而不应只从功能实现的方面去考虑。
4. 其他决策问题
分布式系统通常要比集中式软件考虑更多的安全性、可靠性、事务处理、并发控制等问题。另外,还需要考虑更多的错误处理,例如客户程序发出请求、但服务程序未就绪,甚至找不到服务程序、或无权限访问服务程序时,应如何处理这种情况。
3.2 CORBA
应用程序开发过程
虽然OMG为 CORBA
制订了统一的规范,但规范中也赋予了软件供应商实现 ORB
产品时、自由选择各自不同的实现途径的权利,例如:ORB
可以是一个独立运行的守护进程,也可以嵌入到客户程序和对象实现中,有些 ORB
产品则选择了几种实现方式的组合。所以,不同供应商提供的 ORB
产品,在具体使用方法上可能存在较大差异,这里的 CORBA
部分以Borland公司的 VisiBroker for Java 4.5.1
为例,介绍一个 CORBA
应用程序的具体开发步骤,使用其他 ORB
产品时可参照类似做法。
尽管使用不同 ORB
产品的具体操作差异较大,但程序员开发一个 CORBA
应用程序通常会遵循一定的框架,即首先通过面向对象分析与设计过程,认定应用程序所需的对象,包括对象的属性、行为与约束等特性,然后遵循本节所述的几个开发步骤,完成应用程序的开发、部署与运行,如图3-1所示,图中的箭头表示了任务之间的先后次序。
下面简要解释开发的每一步骤的主要工作。
1. 编写对象接口
对象接口是分布式对象对外提供的服务的规格说明。CORBA
中分布式对象的接口定义,要包括以下内容:
- 提供或使用的服务的名字,即客户端可以调用的方法名;
- 每个方法的参数列表与返回值;
- 方法可能会引发的异常;
- 可选的上下文环境,即调用相关的上下文环境,上下文环境起和参数类似的作用,包含调用相关的若干信息,只不过它并不写死在参数列表中,有更强的灵活性。
可以看出,CORBA
中对象接口中定义的核心内容,和 RMI
例子中用 Java interface
定义的接口类似,但它们有一点明显区别,那就是 CORBA
中对象接口是由 OMG IDL
定义的,而这种 IDL
是独立于程序设计语言的。正是有了这种中性的接口约定,才使得分布式对象和客户端的跨语言得以实现。第四章详细介绍了 IDL
的语法与语义。
2. 编译 IDL
文件
IDL
是一种独立于具体程序设计语言的说明性语言,IDL
编译器的作用是将 IDL
映射到具体程序设计语言,产生客户程序使用的桩代码、以及编写对象实现所需的框架代码。由OMG制订的语言映射规范,允许将 IDL
语言映射到Java、C++、Ada、C、COBOL等多种程序设计语言,这通常是由软件供应商提供的不同编译器分别完成的。
一般厂商实现 CORBA
平台时,都会提供专门的 IDL
编译器来完成 IDL
接口的编译工作。厂商实现 IDL
编译器时,应参照OMG制订的语言的规范,编程人员只要选择使用合适的编译器就可以了,IDL
编译器的工作原理如图3-2所示。
VisiBroker for Java
提供的 idl2java
编译器,将 IDL
映射到Java语言,生成Java语言的客户端桩代码、以及服务端框架代码,桩和框架可分别看做是「服务对象在客户端和服务端的代理」。IDL
文件严格地定义了客户程序与对象实现之间的接口,因而客户端的桩可以与服务端的框架协调工作,即使这两端是用不同的程序设计语言编译,或运行在不同供应商的 ORB
产品之上。
3. 编写客户程序
在 CORBA
中,客户程序的流程较为简单,如图3-3所示,首先初始化 ORB
,然后绑定到要使用的服务对象,然后调用服务对象提供的服务。和 Java RMI
相比,CORBA
客户端程序多了一步初始化 ORB
的操作。
在 CORBA
中,无论是客户程序还是服务程序,都必须在利用 ORB
进行通信之前初始化 ORB
。初始化 ORB
的作用有两个,一个让 ORB
了解有新的成员加入,以便后继为其提供服务;另一个作用就是获取 ORB
伪对象的引用,以备将来调用 ORB
内核提供的操作。
所谓伪对象,专指在
CORBA
基础设施中的一个对象,比如ORB
本身可以看作一个伪对象,伪对象的这个“伪”字主要是针对「在程序中远程访问的CORBA
分布式对象」而言的,因为伪对象是本地的,我们通过伪对象调用CORBA
基础设施提供的操作。
ORB
内核提供了一些「不依赖于任何对象适配器的操作」,这些操作可由客户程序或对象实现调用,包括获取初始引用的操作、动态调用相关的操作、生成类型码的操作、线程和策略相关的操作等,初始化 ORB
即是其中的一个操作。程序员在程序中通过 ORB
伪对象来调用这些操作,也就是说,在程序中通过使用 <orb 伪对象名>.<方法名>
的方式来调用ORB 内核提供的操作。
4. 编写对象实现和服务程序代码
在 CORBA
服务端,开发的主要工作包括编写对象实现与服务程序。IDL
文件只定义了服务对象的规格说明,程序员必须另外编写服务对象的具体实现。
CORBA
规定,所有对象接口定义必须统一用 IDL
书写,但对象实现则有很多选择的余地,例如可使用Java、C++、C、Smalltalk等程序设计语言,并且选择这些语言与客户程序所选用的语言无关,只要 ORB
产品供应商支持 IDL
到这些语言的映射即可。
选用任何一门程序设计语言的程序员,应该熟悉 IDL
到该语言的映射规则,因为通常情况下,IDL
编译器除了生成 Stub
与 Skeleton
之外,还会生成一些开发时需要使用辅助代码。例如 VisiBroker for Java
的 IDL
编译器,将自动生成一些对象适配器的Java类和各种辅助性的Java类,编写对象实现的代码时,必须继承其中的一些类、或使用某些类提供的方法。
与 Java RMI
例子中类似,在 CORBA
服务端,除了编写对象实现代码后,还要编写一个服务程序来将分布式对象准备好。服务程序利用可移植对象适配器 POA
激活伺服对象供客户程序使用。服务程序通常是一个循环执行的进程,不断监听客户程序请求并为之服务。
5. 创建并部署应用程序
编写完代码以后,就可以编译生成目标应用程序了。创建客户程序时,应将「程序员编写的客户程序代码」与「IDL
编译器自动生成的客户程序桩代码」一起编译;创建服务程序时,应将「程序员编写的对象实现代码」与 「IDL
编译器自动生成的服务程序框架代码」一起编译。一些 ORB
产品提供了专门的编译器以简化这一过程,例如 VisiBroker for Java
提供的编译器 vbjc
会自动调用JDK中的Java编译器 javac
,指示 javac
在编译客户程序的同时、编译相关的客户程序桩文件,在编译服务程序的、同时编译相关的服务程序框架文件。
程序员创建的客户程序和服务应用程序,在通过测试并准备投入运行后,进入应用程序的部署 deployment
阶段。分布式系统的布署工作通常远比集中式软件的安装复杂,在该阶段由系统管理员规划,如何在终端用户的桌面系统安装客户程序,或在服务器一类的机器上安装服务程序。由于其复杂性,布署工作经常由单独的角色来承担。
6. 运行应用程序
运行 CORBA
应用程序时,必须首先启动服务程序,然后才可运行客户程序。其他步骤可能与具体 ORB
产品有关,例如 VisiBroker for Java
的 ORB
内核是一个名为 osagent
的独立运行进程(又称智能代理 smart agent
),可以在启动服务程序之后才启动 osagent
,但必须在运行客户程序之前让 osagent
启动完毕。
应注意,上述过程是一个典型的 CORBA
应用程序开发过程,具体实施时,各个步骤会由于不同项目的各自特点而有所区别。例如,利用 CORBA
集成企业原有系统时,就是一种先有对象实现,而后有对象接口规格说明的过程。
3.3 CORBA
开发实例
本节以一个银行帐户管理的简单例子,演示 CORBA
应用程序的典型开发过程,使对「CORBA
应用程序的开发、部署与运行」有一个初步的感性认识。
这是一个简单的银行帐户管理程序,服务端管理大量银行顾客的账户,向远程客户端提供基本的开户、存款、取款、查询余额的功能。
3.3.1 认定分布式对象
按照面向对象的设计理念,我们很容易认定出,系统中应包含图3-4所示的两类对象:
Account
是一个银行帐户的实体模型,它有一个属性balance
表示当前的余额,另有三个行为分别为存款deposit
、取款withdraw
和查询余额getBalance
,每一银行帐户在生存期的任何时刻,都满足帐户余额不小于 0 0 0 这一约束。- 由于例子程序不是仅仅管理某一位顾客的帐户,而是涉及到大量的帐户需要处理,所以还建立了“帐户管理员”这一实体模型,它负责对每一个帐户的开设、撤销和访问等,在实现世界中该实体对应着银行中的储蓄员。帐户管理员
AccountManager
有一个属性accountList
,记录当前已开设的所有帐户,并且有一个行为表示根据帐户标识查找某一帐户,如果该标识的帐户不存在,则创建一个新帐户,我们将该行为命名为open
。这其实只是帐户管理员的最简化模型,在一个实际应用系统中会赋予帐户管理员这一实体更多的职能。
应注意,上述两类对象都是分布式对象,因为对象上的 open, deposit, withdraw
与 getBalance
操作,都是要被客户端远程调用的。
3.3.2 编写分布式对象的接口
按照图3-1所示的开发过程,对于每一个分布式对象,首先要做得就是定义其接口。我们利用OMG的接口定义语言 IDL
,编写对象 Account
和 AccountManager
的规格说明,规格说明存放在一个文本文件中,称之为 IDL
文件。从程序3-1所示 IDL
文件中的对象接口定义可看出,IDL
与Java中的 interface
具有类似的语法。第四章将详细介绍如何使用 IDL 定义模块、接口、数据结构等。
// 程序 3-1 Bank.idl 文件中定义的对象接口
// 银行帐户管理系统的对象接口定义
module Bank {
// 帐户
interface Account {
// 存款
void deposit(in float amount);
// 取款
boolean withdraw(in float amount);
// 查询余额
float getBalance();
};
// 帐户管理员
interface AccountManager {
// 查询指定名字的帐户,查无则新开帐户
Account open(in string name);
};
};
3.3.3 编译 IDL
文件生成桩与框架
完成对象接口的规格说明后,下一步工作是利用 VisiBroker for Java
提供的 idl2java
编译器,根据 IDL
文件生成客户程序的桩代码、以及对象实现的框架代码。客户程序用这些 Java
桩代码调用所有的远程方法,框架代码则与程序员编写的代码一起创建对象实现。
上述 Bank.idl
文件无需作特殊处理,它可用以下命令编译:
prompt> idl2java Bank.idl
由于Java语言规定,每一个文件只能定义一个公有的接口或类,因此 IDL
编译器的输出会生成多个 .java
文件。这些文件存储在一个新建的子目录 Bank
中,Bank
是 IDL
文件中指定的模块名字,也是根据该模块中所有 IDL
接口、自动生成的所有Java类所属的程序包。
IDL
编译器为 IDL
文件中定义的每一个接口自动生成 7 7 7 个 .java
文件,故上述 IDL
文件的编译结果会生成 14 14 14 个 .java
文件,下面以 Account
接口对应的 7 7 7 个文件为例,介绍 IDL
编译器生成的代码,IDL
编译器为接口 Account
生成的文件包括:
AccountOperations.java
Account.java
_AccountStub.java
AccountPOA.java
AccountPOATie.java
AccountHelper.java
AccountHolder.java
在这些文件中,Account.java
和 AccountOperations.java
定义了 IDL
接口 Account
的完整基调(signature
,包括操作的名字和参数表,可用于唯一地表示一个操作的类型)。AccountOperations.java
是 IDL
文件中由 Account
接口定义的所有方法和常量的基调声明,如程序3-2所示。由 IDL
编译器自动生成的对象实现框架代码,将实现该接口(参见程序3-5),该接口还与 AccountPOATie
类一起提供纽带机制(参见程序3-6)。
// 程序 3-2 IDL 编译器生成的 AccountOperations.java 文件内容
package Bank;
public interface AccountOperations {
public void deposit(float amount);
public boolean withdraw(float amount);
public float getBalance();
}
IDL
编译器为每一个 IDL
接口,生成一个最基本的 Java
接口,例如,Account.java
包含了 Account
接口的声明,如程序3-3所示。该接口继承了 AccountOperations
接口。在程序员编写的客户程序中,会使用该接口来调用远程账户对象上的操作。
// 程序 3-3 IDL 编译器生成的 Account.java 文件内容
package Bank;
public interface Account
extends com.inprise.vbroker.CORBA.Object,
Bank.AccountOperations, org.omg.CORBA.portable.IDLEntity {
}
IDL
编译器还为每一个接口创建一个桩类,_AccountStub.java
是 Account
对象在客户端的桩代码,它实现了 Account
接口,如程序3-4所示。应注意,此类中 deposit, withdraw, getBanlance
等操作并没有真正实现账户对象的业务逻辑,只是替客户端完成(对)服务端真正业务逻辑实现的调用。该类负责客户端调用账户对象时的底层通信工作,从客户程序的代码上(程序 3-9)看,客户端通过 Account
接口来调用账户对象上的操作,但实际上客户端调用的是该类上的操作,由该类替客户端完成远程调用。
// 程序 3-4 IDL 编译器生成的_AccountStub.java 文件内容
package Bank;
public class _AccountStub
extends com.inprise.vbroker.CORBA.portable.ObjectImpl implements Account {
final public static java.lang.Class _opsClass = Bank.AccountOperations.class;
private static java.lang.String[] __ids = {
"IDL:Bank/Account:1.0"};
public java.lang.String[] _ids() {
return __ids;
}
public void deposit(float amount) {
while (true) {
if (!_is_local()) {
org.omg.CORBA.portable.OutputStream _output = null;
org.omg.CORBA.portable.InputStream _input = null;
try {
_output = this._request("deposit", true);
_output.write_float((float) amount);
_input = this._invoke(_output);
} catch(org.omg.CORBA.portable.ApplicationException _exception) {
final org.omg.CORBA.portable.InputStream in =
_exception.getInputStream();
java.lang.String _exception_id = _exception.getId();
throw new org.omg.CORBA.UNKNOWN("Unexpected User Exception: " + _exception_id);
} catch(org.omg.CORBA.portable.RemarshalException _exception) {
continue;
} finally {
this._releaseReply(_input);
}
} else {
final org.omg.CORBA.portable.ServantObject _so =
_servant_preinvoke("deposit", _opsClass);
if (_so == null) {
continue;
}
final Bank.AccountOperations _self = (Bank.AccountOperations)_so.servant;
try {
_self.deposit(amount);
} finally {
_servant_postinvoke(_so);
}
}
break;
}
}
public boolean withdraw(float amount) {
while (true) {
if (!_is_local()) {
org.omg.CORBA.portable.OutputStream _output = null;
org.omg.CORBA.portable.InputStream _input = null;
boolean _result;
try {
_output = this._request("withdraw", true);
_output.write_float((float)amount);
_input = this._invoke(_output);
_result = _input.read_boolean();
return _result;
} catch(org.omg.CORBA.portable.ApplicationException _exception) {
final org.omg.CORBA.portable.InputStream in = _exception.getInputStream();
java.lang.String _exception_id = _exception.getId();
throw new org.omg.CORBA.UNKNOWN("Unexpected User Exception: " + _exception_id);
} catch(org.omg.CORBA.portable.RemarshalException _exception) {
continue;
} finally {
this._releaseReply(_input);
}
} else {
final org.omg.CORBA.portable.ServantObject _so = _servant_preinvoke("withdraw", _opsClass);
if (_so == null) {
continue;
}
final Bank.AccountOperations _self = (Bank.AccountOperations)_so.servant