级别: 初级 刘必欣 (liu_bi_xin@sina.com)
| XML error: Please enter a value for the author element's jobtitle attribute, or the company-name element, or both. |
2002 年 10 月 01 日 本文的第一部分基于ApacheSOAP2.2 实现了一个简化的CORBAProvider。但在许多实际应用场景中,复杂类型的调用参数或返回值是无法回避的,因此本文的第二部分将着重解决对复杂IDL类型参数的支持问题,这是系统是否具有实用价值的关键之一。这里所介绍的使用CompoundType统一表示各种复合类型对于减少应用开发者的负担有重要意义。
本文的第一部分介绍了使用ApacheSOAP2.2桥接CORBA对象的一种方法。其主要的技术思路是:利用ApacheSOAP可插拔的Provider接口,实现CORBAProvider;采用名字服务定位对象,使用部署时刻指定的静态Stub做为客户端与CORBA服务端进行通信。
上一篇文章所关注的重点是CORBAProvider的工作原理和实现机制,因而将其所支持的数据类型简化为IDL基本类型。但是在许多实际应用场景中,复杂类型的调用参数或返回值是无法回避的,这要求CORBAProvider能够处理诸如array、struct、union、sequence、valuetype等典型的复合IDL类型。因此,本篇将重点讨论这一问题。
本文假设您对ApacheSOAP的源码有一定了解,特别是编解码部分。否则请参见ApacheSOAP文档及"参考资源"4、5。
现有方案分析
SOAP协议宣称支持两类复合类型,一种复合类型以名字标识成员(如结构),另一种复合类型以序号标识成员(如数组)。如果按上述两类区分IDL中的复合类型,那么struct,union,valuetype等属于前者,sequence和array属于后者。这里我们重点讨论前者,因为sequence和array都映射为java数组,而ApacheSOAP2.2已经能够支持数组类型的编解码。
事实上,struct,union和valuetype具有某些共同特征,它们都是若干数据成员的封装,虽然struct,union本身并不具有对象含义,但在IDL2java映射中它们都映射为某个特定的java类,具有对象含义的valuetype同样映射为java类。因此,使用java类来封装这些复合类型是一个自然的想法。这样对于SOAP客户而言,它只需要了解java类的具体形式,却并不区分struct,union或valuetype等IDL类型,也无需知道所传递的对象将为后端CORBA系统传递一个复合IDL类型数据。这种透明性对SOAP客户是必要的。
ApacheSOAP2.2为对象类型参数的传递提供了解决方案。使用ApacheSOAP2.2我们可以这样处理复合IDL类型参数(或返回值)。
- 由应用开发者为每个IDL复合类型书写一个相应的纯java类(注:此类并非该IDL类型的java映射类),并手工实现该java类的序列化器和反序列化器。客户端和服务端程序必须都包含这个java类的代码,并分别在客户端和部署服务时增加相应的类型映射。具体方法可以参见ApacheSOAP文档。
- 另一种方案仍然要求客户方和服务方为每个IDL复合类型实现每一个纯java类,但利用ApacheSOAP实现的BeanSerializer来编解码这些复合IDL类型对应的java类。因此要求该java类必须满足javabean规范,即每个属性都必须有相应的get/set方法;并在部署时声明使用BeanSerializer来序列化和反序列化该java类。
虽然上述的方案二改进了方案一,免除了用户书写定制的序列化器/反序列化器的麻烦,但它们都存在以下缺点:
- 增加应用开发者的负担。不论是服务的开发者(service provider),还是客户应用的开发者(service requestor),都需要为每一个复合类型书写相应的java类,这是一个烦琐、重复性的工作;
- 客户方和服务方java类及其序列化器、反序列化器需严格一致,否则不能正确编解码参数;
- 书写定制的序列化器、反序列化器需要使用ApacheSOAP内部编解码API,因而将编程的复杂性留给了应用开发者;
- 是一种应用级的解决方案,不适用于为CORBA对象的发布提供平台级支持。
为此,我们希望以一种通用模式支持复合类型,使得每一个复合类型都可以采用该通用模式表示、使用通用过程进行编解码(如图1所示),从而向应用开发者呈现一种简单、自然的开发方式。下面详细介绍实现策略。
图1 以通用模式支持复合类型
复合类型的统一表示
首先,让我们考虑是否可以使用某种统一的形式来表示struct、union、valuetype等复合类型。这样做的依据是:struct,union,valuetype虽然具有不同的语法和语义,但它们都封装了一组数据成员,在OMG公布的Corba Web Services Initial Joint Submission(orbos/2001-06-07)中,其IDL2XML Schema为它们定义了类似的线级形式。使用统一形式表示复合类型的好处是:利于使用统一的编解码过程来实现复合类型的序列化和反序列化。
CompoundType类正是这一思想的产物,它用于统一表示各种复合类型。CompoundType类被设计为包含一个QName对象和一个Parameter向量(如图2所示)。熟悉XML Schema的读者一眼就能看出,CompoundType类的结构与XML 所定义的 Complextype标签内的结构是一致的:QName用于表示该复合类型的名字(名字空间+局部名),Paramter向量中的每一个元素表示该复合类型的一个数据成员。
图2 CompoundType类
如何实现CompoundType类的5个方法是显而易见的,详见"参考资源"中的示例源码。
下面的例子说明如何用CompoundType对象表示一个IDL复合数据类型实例。以结构Customer为例,它的IDL定义如下所示。此外模块ct中还定义了接口Test,其echo方法将使用一个Customer结构实例作为输入参数:
module ct{
struct Customer{
string name;
short id;
};
interface Test{
void echo(in Customer c);
};
};
|
代码示例1创建一个CompoundType对象来表示上述Customer实例,并将name赋为Jinkam,id赋为1001。可以看到,使用CompoundType对象表示复合类型实例时只需首先设置类型(即调用setQName方法,这里"urn:CompoundType-demo"是应用开发者所定义的类型的名字空间,"Customer"是类型的局部名),然后依次加入各个数据成员即可。对于union或valuetype等其它复合类型,除数据成员外可能还需一些描述信息(如union的descriminator序号,valuetype的repositoryID,等),它们也只需作为一个元素加入Paramter向量中即可。
代码示例1
1. CompoundType ctype = new CompoundType();
2. ctype.setQName (new QName("urn:CompoundType-demo" , "Customer")) ;
3. ctype.addElement (new Parameter("name", String.class, "Jinkam", null)) ;
4. ctype.addElement (new Parameter("id", Short.class, Short.valueOf("1001") , null)) ;
|
复合类型的编解码
接下来考虑如何为CompoundType实现统一的编/解码过程,即实现CompoundType的序列化和反序列化类。CompoundType的序列化类CompoundTypeSerializer和反序列化类CompoundTypeDeserializer分别实现Serializer接口和Deserializer接口(参见ApacheSOAP文档),分别负责将CompoundType对象编码成XML文本,或将XML文档表示的复合类型实际解码为CompoundType对象。
由于CompoundType对象由代表类型的QName对象和若干Parameter对象组成的向量构成,因此,序列化类首先编码CompoundType对象的类型,然后循环地处理每个Parameter成员。幸运的是,ApacheSOAP已经为Parameter类实现了完整的序列化类和反序列化类,因此这里只需直接调用即可。这是我们选择使用Parameter对象表示复合类型的数据成员的原因之一。反序列化类的工作过程类似。
需要注意的是,CompoundType的(反)序列化类所处的位置与其它类型的(反)序列化类有些不同。在ApacheSOAP的编解码机制中,处理参数编解码的根(反)序列化器是ParameterSerializer类,它根据encodingStyle 和类 Parameter类型(对于序列化)或者 QName (对于反序列化)将控制逻辑转到它的下层(反)序列化类,从而形成树状的调用路径(如图3中实线所示)。但CompoundType(反)序列化类的引入破坏了这种严格的树状结构,因为编解码逻辑在转入CompoundType(反)序列化类后会首先上溯至ParameterSerializer类(如图3中虚线所示),然后进行再次散转。因此,编码解码CompoundType时,SOAPMappingRegistry将被递归调用。
图3 序列化类的调用关系
代码示例2说明CompoundTypeSerializer的工作流程。其中,第1行向目标流写出一个新标签,即CompoundType参数的名字,context表示当前正在处理的参数的Class对象;第2-10行则向目标流输出该参数的类型,即该XML标签的type属性;第14-20行循环地处理Parameter向量中的每一个元素,如上所述,它将每个Paramter对象当作一个独立的参数进行编码,xjmr是一个SOAPMappingRegistry对象,它的marshal()方法体中的编码逻辑将跟据Parameter所封装的参数类型的不同而转入不同的序列化类。完整的代码请参见"参考资源"。
代码示例2
1. sink.write('<' + context.toString());
2. CompoundType cs = (CompoundType)src;
3. QName elementType=cs.getQName() ;
4. String xsiNSPrefix =
5. nsStack.getPrefixFromURI( Constants.NS_URI_CURRENT_SCHEMA_XSI, sink);
6. String elementTypeNSPrefix =
7. nsStack.getPrefixFromURI( elementType.getNamespaceURI(), sink);
8. sink.write(' ' + xsiNSPrefix + ':' + Constants.ATTR_TYPE + "=\"" +
9. elementTypeNSPrefix + ':' +
10. elementType.getLocalPart() + '\"');
11. ……
12. sink.write('>');
13. sink.write(StringUtils.lineSeparator);
14. for (int i = 0; i< cs.getSize(); i++)
15. {
16. Parameter param = cs.getElement(i) ;
17. xjmr.marshall(inScopeEncStyle,Parameter.class,param,param.getName() ,
18. sink,nsStack,ctx) ;
19. sink.write(StringUtils.lineSeparator);
20. }
21. sink.write("</"+ context+">"); |
代码示例3说明CompoundTypeDeserializer的解码流程。其中第4行代码为新创建的CompoundType对象设置类型,elementType是一个QName实例;第6-14行代码解析XML标签的每一个子元素,并循环递归地调用SOAPMappingRegistry的unmarshal方法解码每一个Parameter对象,然后将其加入CompoundType对象的Parameter向量中。完整的代码请见"参考资源"。
代码示例3
1. Element root = (Element)src;
2. ……
3. CompoundType cs = new CompoundType();
4. cs.setQName (elementType);
5. Element tempE = DOMUtils.getFirstChildElement(root);
6. while (tempE != null){
7. ……
8. Bean paramBean = xjmr.unmarshall (actualParamEncStyle,
9. RPCConstants.Q_ELEM_PARAMETER, tempE, ctx);
10. Parameter param = (Parameter) paramBean.value;
11. …..
12. cs.addElement(param);
13. tempE=DOMUtils.getNextSiblingElement(tempE);
14. }
15. return new Bean(CompoundType.class,cs);
|
添加类型映射
作为对ApacheSOAP类型映射的扩展,CompoundType与其相应的序列化类、反序列化类的映射关系需要被显式地告之SOAPMappingRegistry,即需要向SOAPMappingRegistry注册,否则ApacheSOAP将无法知道该怎样编解码CompoundType对象。
ApacheSOAP的文档中详细说明了如何在客户方和服务方添加扩展类型映射,此处不赘述,仅给出简明示例。假设客户方调用Test接口的echo方法,代码示例4继代码示例1创建CompoundType对象后为它设置类型映射。
代码示例4
1. SOAPMappingRegistry smr = new SOAPMappingRegistry();
2. CompoundTypeSerializer Ser = new CompoundTypeSerializer();
3. Vector params = new Vector();
4. params.addElement (new Parameter ("c",CompoundType.class,ctype,null)) ;
5. call.setParams (params);
6. smr.mapTypes(Constants.NS_URI_SOAP_ENC, ctype.getQName() ,
7. CompoundType.class, Ser, null);
8. call.setSOAPMappingRegistry(smr);
|
服务方在服务的部署时刻加入类型映射。例如,此处服务方类型映射应作如下部署:
| Namespace URI | urn:CompoundType-demo | | Local Part | Customer | | JavaType | corbaprovider.CompoundType | | Java to XML Serializer | corbaprovider.CompoundTypeSerializer | | XML to Java Deserializer | corbaprovider.CompoundTypeDeserializer |
至此,我们已经完成了CompoundType类的开发,任意一个复合类型都可以使用CompoundType来表示和传输。应用开发者不再需要为每一种复合类型书写一个特定的java类来封装它,甚至为这个类书写序列化和反序列过程。所有的工作仅仅是三个类,并且实现它们并不比为某一个特定的复合类型书写java类和定制(反)序列化类复杂多少。上述机制更适合对ApacheSOAP作平台级的扩展,如果您有一个CORBA系统中的大量对象需要发布为Web Service,这将是一个这是一劳永逸的解决方案。
该欣赏一下成果了。下面所截取的SOAP报文显示了SOAP客户端访问Test接口的echo方法时所传递一个Customer参数的编码情况。
<?xml version ='1.0' encoding ='UTF-8'?>
<SOAP-ENV:Envelope xmlns:SOAP-ENV ="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:xsi =http://www.w3.org/1999/XMLSchema-instance
xmlns:xsd ="http://www.w3.org/1999/XMLSchema">
<SOAP-ENV:Body>
<ns1:echo xmlns:ns1="urn:CompoundType-demo" SOAP-ENV:encodingStyle ="http://schemas.xmlsoap.org/soap/encoding/">
<c xsi:type ="ns1:Customer">
<lt;name xsi:type ="xsd:string"> Jinkam
<lt;/name>
<lt;id xsi:type ="xsd:short">1001</id>
<lt;/c>
</ns1:echo>
<lt;/SOAP-ENV:Body>
</SOAP-ENV:Envelope> |
复合类型的参数重建
仅仅将复合类型参数封装在CompoundType对象中传递到SOAP服务器是不够的,因为CompoundType对象不能直接作为参数激发CORBA请求。回忆本文的第一部分,我们利用了运行时刻加载的stub来激发CORBA调用,这是一种静态构造请求的方式。stub要求使用IDL的语言映射类型参数来激活方法,而不能是其它数据类型或普通的java类。例如,Test接口的echo方法接收一个Customer参数,那么在_TestStub.echo()方法的参数类型需为IDL结构Customer的Java映射类,该类是实现IDLEntity接口的。因此,在激发CORBA请求前,我们还需在SOAP服务器端重建可以用于CORBA调用的IDL类型参数实例,即将CompoundType对象转换为复合类型的Java映射类对象。
不幸的是,在静态stub方式下重建参数实例并不是轻而易举的事。原因是:除了使用Java语言的反射机制外,没有别的途径在运行时刻获取复合参数的类型信息;虽然可以实例化IDL参数对象,但是不同IDL类型的映射规则不同,导致无法(或不容易)找到一个统一的方法来为IDL参数对象设置状态(如,结构直接暴露它的成员变量,但联合和值类型则需要通过accessor来设置状态)。因此,作为一个折衷的方案,目前可采用定制过程来实现参数重建。
由于参数重建随接口、方法的不同而不同,因此CorbaProvider需将这一工作委托给与应用相关的代码来完成,应用开发者知道怎样初始化参数实例、怎样利用CompoundType中的信息来设置参数状态。为此,我们为每个服务设计一个Proxy类,它封装了与该服务相关的参数重建逻辑,也封装了stub激活请求的过程。Proxy的位置处于CorbaProvider与后端CORBA对象之间,它的存在使得CorbaProvider不必关心与特定服务相关的参数类型等细节,Proxy接收CorbaProvider传递的CompoundType对象,依据其中封装的参数信息创建特定的IDL映射类对象并设置状态,最后使用该映射类对象激发CORBA请求。事实上不难看出,这个服务特定的Proxy类的原理与功能都类似于CORBA的skeleton。图4显示了CorbaProvider的三层结构及各层之间的数据流动。
图4 CorbaProvider三层结构
在本文第一部分中,CorbaProvider直接激发请求,这时Proxy也是隐式存在的,因为此时Proxy无需做参数重建的工作,它直接使用CorbaProvider传递的Parameter对象激发请求即可。因此,重新设计后的CorbaProvider体系结构同样适用于简单类型。
Proxy具有如下的类结构:
CorbaProxy接口定义了所有Proxy类需实现的invoke()方法。BaseCorbaProxy类是一个抽象类,它封装了对所有Proxy而言相同的逻辑,例如,这里把激发stub的方法调用的代码封装在它的invokeMethodOnStub()方法里(这部分代码在上篇文章中曾位于CorbaProvider.invoke()方法内)。服务特定的Proxy则继承BaseCorbaProxy,并实现invoke()方法。
代码示例5展示了Test接口的CtProxy的invoke()方法。它的功能是:跟据请求的方法名,将控制散转到相应的同名内部方法(第6、7句)。对参数的处理是在内部方法(如echo()方法)中进行的。
图5 Proxy类图
代码示例5
1. public Bean invoke(ORB orb, Object stub, Call call) throws SOAPException {
2. _orb = orb;
3. _stub = stub;
4. String methodName = call.getMethodName() ;
5. Vector params = call.getParams ();
6. if (methodName.equals("echo"))
7. return echo(params);
8. else
9. throw new SOAPException(Constants.FAULT_CODE_SERVER,"Bad method name");10. }
|
代码示例6中,echo方法响应SOAP客户对Test接口echo操作的访问。它将检验SOAP服务器解码所得到的Parameter向量的有效性,并对封装请求参数的Parameter向量进行调整使之可用于激发CORBA调用----如果某参数为复合类型,则重建IDL类型参数并对替换向量中的相应元素。例如此处,echo()方法知道唯一的一个参数应为Customer对象(Customer结构的映射类),则它创建Customer对象实例,并从Parameter向量封装的CompoundType对象中取出相应的元素值,设置参数状态,最后用Customer对象替换Parameter向量中的原CompoundType对象。
代码示例6
1. private Bean echo(Vector params) throws SOAPException{
2. //准备参数
3. if ( params.size () != 1)
4. throw new SOAPException (Constants.FAULT_CODE_SERVER ,
5. "error in unmarshal params");
6. Parameter param = (Parameter) params.elementAt (0);
7. if (param.getValue() instanceof CompoundType){
8. CompoundType _ct = (CompoundType)param.getValue() ;
9. Customer _c = new Customer();
10. for (int j = 0; j< _ct.getSize() ; j++){
11. Parameter temp = _ct.getElement(j) ;
12. String s = temp.getName() ;
13. if (s.equals("name")){
14. _c.name = (String)temp.getValue() ;
15. }else{
16. if (s.equals("id") ){
17. _c.id = ((Short) temp.getValue()). shortValue() ;
18. }else{
19. throw new SOAPException (Constants.FAULT_CODE_SERVER,
20. "error in processing CompoundType");
21. }
22. }
23. }
24. params.set(0,new
25. Parameter("Customer", Customer.class,_c,param.getEncodingStyleURI() ));
26. }
27. //激活方法
28. return invokeMethodOnStub ("echo" , params);29. } |
从现在的情况看,在使用静态stub来激活CORBA对象这个前提下,虽然还没有一个比较简单、高效的方法来实现自动的参数重建,但是由于参数重建过程是一个十分例行化的工作,完全可以借助编译器等来产生Proxy类。编译器只需对代码示例5、6中所示的粗体部分进行适当替换即可。对于一个完整的系统来说,IDL2WSDL、IDL2Proxy编译器是需要的。当然,如果您使用动态DII来激活CORBA对象,借助接口池来实现自动的重建参数是可行的。
最后在CorbaProvider中调用Proxy来执行请求即可(代码示例7)。为了使CORBAProvider能载入正确的Proxy,需要在部署时刻指定服务所使用的Proxy类(由选项proxyClassName指定),正如上篇文章中指定stub类和helpler类一样。具体方法请参见本文的第一部分。
代码示例7
1. CorbaProxy proxy;
2. try{
3. Class proxyClass = reqContext.loadClass (proxyClassName) ;
4. proxy = (CorbaProxy) proxyClass.newInstance() ;
5. } catch (Exception exp) {…… }
6. Result = proxy.invoke (orb,stub,call) ;
|
至此为止,本文已向您展示了利用ApacheSOAP集成CORBA对象的完整方案。本第一部分侧重介绍基本原理,并实现一个简单的CORBAProvider;第二部分则着重解决了对复杂IDL类型参数的支持问题,这是系统是否具有实用价值的关键之一。本文所介绍的使用CompoundType统一表示各种复合类型对于减少应用开发者的负担有重要意义。除本文关注的几种复合类型之外,IDL还定义了丰富的类型,对于它们的处理也可采用类似的方法。但笔者认为,SOAP客户应保持一定透明性,因此并不是每种IDL类型对SOAP客户都有意义,故在系统设计和应用开发时应有所取舍。
当然,对于一个实用的CORBA与Web Service的桥接系统而言,本文的实现是远远不够的,许多商用产品在这方面做了不错的工作,如CapeClear公司的CapeClear、IONA的XMLBus等,您可以从它们各自的网站上下载试用版本。本文虽窥豹一斑,但相信它对于您探索这些产品的工作原理、理解其中的基本问题会有一定帮助。
Acknowlegements
本文所介绍的方法是我在参与一个小组的研发工作过程中思考和验证的,部分代码来自于小组成员的共同努力,为此向小组内的孙海燕、胡建强表示感谢。
参考资料
关于作者
对本文的评价
|