内容


Apache SOAP 类型映射,第 2 部分

序列化详细说明

按照这些步骤编写自己的定制序列化器和反序列化器

Comments

系列内容:

此内容是该系列 # 部分中的第 # 部分: Apache SOAP 类型映射,第 2 部分

敬请期待该系列的后续内容。

此内容是该系列的一部分:Apache SOAP 类型映射,第 2 部分

敬请期待该系列的后续内容。

本系列第一篇文章中,您明白了 SOAP 怎样把数据类型映射为 XML,并学到了如何使用包含在 Apache SOAP 工具箱内的序列化器和反序列化器(以后合称为(反)序列化器)。在这一部分,我将带您从头至尾学习这篇教您编写自己的(反)序列化器的详细说明文章。我建议您应该有一些可供参考的基本(反)序列化器的源码。您可能还想重新阅读 第 1 部分中的“类型映射模式”这一部分来回忆一下如何内部解析类型映射。

一旦我写完这篇详细说明,我将提供一个实现受模式约束的 SOAP 的简单应用程序。这个应用程序将描述一个交互,在这个交互过程中使用 SOAP 发送一个故意不遵循 Section 4 编码的购买订单文档。

根和常规(反)序列化器

如果与 Apache SOAP 工具箱捆绑在一起的(反)序列化器都不能使用您的 Java 类,您可能需要自己编写一个定制的(反)序列化器。首先,您需要区别开我所说的 根(反)序列化器常规(反)序列化器。根(反)序列化器处理 RPC 参数和响应的序列化及反序列化的初始引导。 表 1中列出了 Apache SOAP 中的三个根(反)序列化器。

表 1. 根(反)序列化器
encodingStyle(反)序列化器
Section 5ParameterSerializer
文字 XMLXMLParameterSerializer
XMIXMIParameterSerializer

调度合适的根(反)序列化器要根据两个东西: encodingStyle 和类 Parameter (对于序列化)或者 QName SOAP-ENV:Parameter (对于反序列化)。要了解实际调度是如何发生的,您需要理解导致客户端 Java 类型序列化的事件链。

 Call call = new Call();
  ...
  resp = call.invoke(url, ""); //1

当在上面第 1 行处调用 Call 类的 invoke 方法时, Call 类迭代其相关的参数。

org.apache.soap.rpc.RPCMessage:
  ...
  Serializer ser = xjmr.querySerializer(Parameter.class, actualParamEncStyle); //2
  ser.marshall(...); 
  ...

对于每个参数,都查询类型映射注册表并在返回的序列化器上随后调用 marshall() 方法。第 2 行中返回的序列器是一个根序列化器。

现在,让我们把注意力转移到 Web 服务的反序列化过程上。在服务器端,SOAP RPC 消息的侦听器被作为 servlet 实现。 doPost 方法检索 SOAP 请求并试图根据该请求重新构建 Call 对象。

org.apache.soap.rpc.RPCMessage:
...
  Bean paramBean = smr.unmarshall(actualParamEncStyle, RPCConstants.Q_ELEM_PARAMETER
 ...); //3
  Parameter param = (Parameter)paramBean.value;
  ...

在第 3 行, RPCConstants.Q_ELEM_PARAMETER 解析为 SOAP-ENV:Parmeter 。当调用 unmarshall() 方法时,就是在第 3 行这里发生到根序列化器的调度。

接下来根(反)序列化器将在类型映射注册表中查询下一个要调用的常规(反)序列化器。只有当注册表返回基本 Java 类型的(反)序列化器(序列化期间)或 XML 元素只作为简单类型的容器(反序列化期间)时,这个调用堆栈才开始解析操作。

提供的一大批常规(反)序列化器(可以在包 org.apache.soap.encoding.soapenc 中找到),遵循 Section 5 编码,就象 Apache SOAP 中的大多数助手类一样。这就是我把重点只放在这篇详细说明文章的 Section 5 编码(反)序列化器上的原因。

图 1. 用于编写(反)序列化器的 API
用于编写(反)序列化器的 API
用于编写(反)序列化器的 API

序列化器详细说明

序列化器实现 org.apache.soap.util.xml.Serializer 并实现一个方法:

 void marshall(
    java.lang.String inScopeEncStyle, 
    java.lang.Class javaType, 
    java.lang.Object src, 
    java.lang.Object context, 
    java.io.Writer sink, 
    NSStack nsStack, 
    XMLJavaMappingRegistry xjmr, 
    SOAPContext ctx) 
      throws java.lang.IllegalArgumentException, 
             java.io.IOException

我们逐个研究一下 marshall() 的每个参数:

  • inScopeEncStyle : 象外围 CallResponse 对象中指定的那样,它代表 encodingStyleURI
  • javaType: 这是要序列化的对象的运行时类型。
  • src: 这是对要序列化的 Java 对象的引用。
  • context : 指出访问器名称的一个 String 。如果这个序列化器是由 ParameterSerializer 调用的,那么上下文的值就等于 Parameter 类(在 SOAP 客户机上声明)中命名的属性,或者,如果它是一个 SOAP 服务器,则被硬编码为 return 。它必须是非空的。
  • sink: 目的接收器,将在此处写 SOAP XML 实例。
  • nsStack: 实现作用域内当前名称空间声明堆栈的数据结构。
  • xjmr : 这是一个 smr ,您将用它来根据 Java 类型查询下一步要使用的序列化器。您还将调用 xjmr 编组方法以便根据 javaTypeencodingStyle 将序列化工作委托给其它序列化器 ― 例如,您可以为复合结构如 HashtableVector 这样做。
  • ctx : 它被用于从 servlet 上下文传入 javax.servlet.http.HttpServletRequestjavax.servlet.http.HttpSession 之类的内容。

marshall() 方法的详细说明如下:

第 1 步:创建新的名称空间作用域

  nsStack.pushScope();

使用 NSStack 类跟踪 XML 名称空间声明的作用域。稍后在这个方法中,可以用 NSStack 来添加一个新的名称空间,还可以用它在整个堆栈中搜索给予一个 URI 的前缀。

第 2 步:检查对象参数约束

为使序列化能够发生,需要满足两个条件:必须为序列化器授予受支持的类型,并且待序列化的对象必须是非空的。下面的代码片段演示了如何在 VectorSerializer 内强制执行这些约束。

    if ( (src != null) &&
         !(src instanceof Vector) &&
         !(src instanceof Enumeration))
              throw new IllegalArgumentException("Tried to pass a '" +
                      src.getClass().toString() + "' to VectorSerializer");

几个内置的序列化器将 javaType 参数与期望的类型实际进行比较,如下所示:

 if(!javaType.equals(Foo.class)) ...

但我不推荐使用这个技术,因为它容易发生冒名顶替者(impostor)类型的错误(请参阅 参考资料)。

第 3 步:生成一个空访问器

如果对象参数为空,您需要为这个类型生成一个空访问器。

SoapEncUtils.generateNullStructure(inScopeEncStyle,
        javaType,
        context,
        sink,
        nsStack,
        xjmr);

第 4 步:序列化对象

将对象序列化为遵循 Section 5 的 SOAP XML 文档需要三个步骤:为访问器生成开元素,序列化对象的值,最后关闭元素。

调用下面的实用程序方法可以很容易地完成第一步:

 SoapEncUtils.generateStructureHeader(inScopeEncStyle,
         javaType,
         context,
         sink,
         nsStack,xjmr);

这段代码将调用 queryElementTypejavaType 查找已映射的 QName

 <context xsi:type="QName">

如果对象参数 src 属于简单类型,那么第二步,序列化对象的值,就很简单,只需调用 src.toString() 方法,然后把它写出来就行了。否则,您将需要鉴别对象的成分并将它们一个个单独传递给更基本的序列化器。如果研究一下内置序列化器的源代码,您会注意到可以用许多种方法识别这些成分:

  • Java 反射(例如, BeanSerializer
  • 迭代整个 List 数据结构(例如, ArraySerializer
  • 通过对类的预先了解直接访问(也就是,您预先知道您的序列化器只为一个特定的类工作)

鉴别了其它序列化器后,您可以通过调用下面的程序将序列化工作委托给它们:

 xjmr.marshall(inScopeEncStyle,
                componentType,
                componentValue,
                accessorName,
                sink, nsStack, ctx);

这里, componentTypecomponentValue 分别代表运行时类型和对象引用,对于原始 src 参数的任何成分都是这样。 marshall() 方法实际调用 querySerializer 来检索相关的序列化器,并随后调用这个相关序列化器的 marshall() 方法。显然,只有当您已经在类型映射注册表中注册了所有组件的序列化器时,这才有效。

最后一步是关闭元素,只要为这个访问器写一个关闭标记这一步即告完成。

 sink.write("</" + context + '>');

最后,在离开当前名称空间作用域之前,您必须自己做一些清理工作。

 nsStack.popScope();

反序列化器详细说明

反序列化器实现 org.apache.soap.util.xml.Deserializer 并实现一个方法:

 Bean unmarshall(
    java.lang.String inScopeEncStyle, 
    QName elementType, 
    org.w3c.dom.Node src, 
    XMLJavaMappingRegistry xjmr, 
    SOAPContext ctx)

unmarshall() 方法的目的是将参数重新构建为 Java 对象。为实现这一目的,您需要处理 src DOM 节点包含的 XML 段。实现这个目的的更好的编程模型是协同类型映射注册表,使用 org.apache.soap.utils.xml.DOMUtils 中的 DOM 包装器方法。一般情况下,反序列化的 DOMUtils 与序列化的 SoapEncUtils 相对。

要注意的重要一点是保证 src 中包含的 XML 无多引用值。所有的多引用 href 都已经被根序列化器 ParameterSerializer 解析回实际的值。

于是,反序列化的详细说明如下:

第 1 步:检查是否为空

建议您检查可以为空的属性,如下所示:

   Element root = (Element)src;
    if (SoapEncUtils.isNull(root))
    {
        return new Bean(Your.class, null);
    }

第 2 步:重新构建 Java 对象

重新构建 Java 对象的过程随其数据类型所属的类别而异。(要了解关于这些类型类别的更多信息,请参阅 第 1 部分。)

简单类型
如果您要反序列化一种简单类型,只需使用 DOMUtils.getChildCharacterData(Element) 检索 src 的字符串值,并在使用它初始化要返回的对象之前对它进行预处理(例如,将字符串“NaN”映射为 FloatDeserializer 中的 Float.NaN ),预处理这一步可选。

混合类型
混合类型主要分为两类。第一类由带重复元素的同样结构的类型组成;示例包括 Java 数组和实现 java.util.Listjava.util.Map 的类。另一类是代表展现任意结构的所有其它 Java 类。于是,反序列化过程就归结为:先遍历 XML 结构来识别相关的后代元素,随后将反序列化的责任委托给更基本的反序列化器,如下所示:

  • 遍历 DOM。如果您正在处理第一类中的混合类型,您可以使用 DOMUtils.getFirstChildElement()DOMUtils.getNextSiblingElement() 访问它的所有重复成员。否则,请使用 DOM API 来识别代表成员属性的 元素
  • 将反序列化委托给其它的反序列化器。首先,您必须提取 SOAP 类型:
    QName soapType = SoapEncUtils.getTypeQName(rootElement);

    下一步,委托给更基本的反序列化器:

    xjmr.unmarshall(inScopeEncStyle, soapType, rootElement, ctx);

    xjmr.unmarshall 内部调用 queryDeserializer 然后调用返回的反序列化器的 unmarshall 。通过将反序列化委托给 ParameterSerializer 把上面的两步合并成一步会更好。这样做是因为当缺少 xsi:type 属性时,我们一般喜欢用设置为 QName {""}/XsoapType 调用 xjmr.unmarshall() ,其中 X 是根元素的 tagName 。由于实现这个目的的代码早已经很方便地打包在 ParameterSerializer.unmarshall() 中,所以这个过程的精简版本就变成了:

    Bean paramBean = xjmr.unmarshall(inScopeEncStyle,
                                    RPCConstants.Q_ELEM_PARAMETER,
                                    rootElement, ctx);
  • 初始化目标对象。目标对象就是您正在重新构建的对象实例。当成员属性被反序列化时,您可以通过调用目标对象上的更动子(mutator)方法恢复它们的值,如下所示:
     Foo foo = new Foo();
      foo.setS( paramBean.value );

第 3 步:返回重新构建的对象

Bean 类封装运行时类型和实际返回的实例。反序列化器知道自己应该返回什么类,因为大多数情况下它都是为某个特定类定制的。对于象 BeanSerializerArraySerializer 这样的常规反序列化器,类型映射中的 javaType 属性传递要返回的类型:

  return(new Bean(Foo.class, foo));

注册根(反)序列化器

我已经提到过,如果您想引入定制的 encodingStyles ,那么您必须编写根(反)序列化器。根(反)序列化器的实现方法与常规(反)序列化器的实现方法一样,只有一点很小的差别:所有的根(反)序列化器都以特别指定的 QName 和 Java 类型注册在类型映射注册表中,QName 和 Java 类型告诉 Apache SOAP 根据 encodingStyle 属性引导(反)序列化过程。在下面的样本代码中,请记下突出显示的值,在注册根(反)序列化器时必须使用这些值。

 [Client]
    smr.mapTypes(customEncURI,
      
        
        RPCConstants.Q_ELEM_PARAMETER
                , 
      
        
        Parameter.class

                ,
      customSerializer, 
      null);
  [Server]
    <isd:map encodingStyle="customEncURI"
      xmlns:x=
        
        "http://schemas.xmlsoap.org/soap/envelope"
                 qname=
        
        "x:Parameter"
                
      javaType=
        
        "org.apache.soap.rpc.Parameter"
                
      java2XMLClassName="foo.customSerializer" />

受模式约束的 SOAP

在这一部分,我将带您看一下用于(反)序列化复杂类型的 BeanSerializer 的替代解决方案。我将称之为 受模式约束的 SOAP(schema-constrained SOAP)的这种技术使用 XML Schema 描述 RPC 参数的文字 XML 结构。这里,我们一致同意严格按照消息格式进行互操作,而不关心客户机和服务器上的数据模型。为避免混淆,应该注意:RPC 调用仍然使用 Section 5 进行编码,而参数不是。

我将用一个示例应用程序来演示这种技术,您可以从下面的 参考资料下载这个示例的完整代码。客户机向一个 Web 服务发送一个购买订单,该服务用一个确认字符串响应。这个 Web 服务导出的方法说明是这样的:

public String eatPo (PurchaseOrder p);

为使这个技术起作用,我们需要一个 XML/对象数据绑定框架。对于这个示例,我选择使用 Exolab 的 Castor 工具箱。(请参阅下面部分的 参考资料获得到 Castor 的链接以及其它序列化框架如 JSX、JAXB 和 Schema2Java 的列表)。

使用这个技术的步骤如下:

  1. 一致同意 PurchaseOrder 的 XML 格式。
  2. 使用 Castor 生成 Java 类。
  3. 编写一个定制的(反)序列化器。
  4. 为客户机和服务器段编写类型映射。

第 1 步:一致同意 PurchaseOrder 的 XML 格式

对于这个用例,为简单起见我从 PurchaseOrder 模式中除去了订单细节部分。另外,还要注意 PONumber 属性使得这个模式不遵循 Section 5 编码。

图 2. PurchaseOrder.xsd
PurchaseOrder.xsd
PurchaseOrder.xsd

第 2 步:使用 Castor 生成 Java 类

运行 Castor 的 SourceGenerator 命令行工具生成实现 PurchaseOrder.xsd 中模式的 Java 类:

 java org.exolab.castor.builder.SourceGenerator
     -i PurchaseOrder.xsd
     -package com.raverun.po.castor

SourceGenerator 工具只识别最新的模式名称空间 ― http://www.w3.org/2001/XMLSchema

下一步,编译 Java 类集。注意,当生成的文件使用 SAX 1.0 API 时,您将需要使用 -deprecation 选项。为了避免手工编译,Exolab 正在致力于开发出一个 Ant taskdef 来自动编译。

第 3 步:编写一个定制的(反)序列化器

现在,您将通过利用 PurchaseOrder 类公开的(反)序列化方法的相应方法实现 PurchaseOrderSerializer 中的(反)序列化方法。对于序列化, PurchaseOrder 可以编组为 java.io.Writerorg.xml.sax.DocumentHandler 。如 清单 1 所示,您将可以将序列化委托给 PurchaseOrdermarshal() 方法。警告: marshal() 方法生成的 XML 流包含 XML 序言(prolog)。 PurchaseOrderSerializer 通过将 sinkFilterXmlProlog (一个 java.io.FilterWriter )包在一起省去了这段序言。 清单 2包含反序列化过程中可能出现的一些异常情况。

清单 1. 从 PurchaseOrderSerializer 中的 marshal() 方法提取的代码段
     ----o<---------
      SoapEncUtils.generateStructureHeader(inScopeEncStyle,
                                         javaType,
                                         context,
                                         sink,
                                         nsStack,
                                         xjmr);
      PurchaseOrder po = (PurchaseOrder)src; 
      try{
        po.marshal( new FilterXmlProlog(sink) );
      }catch(Exception e){ 
        throw (new java.io.IOException("Castor: Error marshalling"));
      }
      sink.write( StringUtils.lineSeparator );
      sink.write("</" + context + '>');
      ----o<---------
清单 2. PurchaseOrderSerializer 的反序列化过程中出现的异常情况
   (b1) Null PO.
                 
         ---------------------------
         <po 
           xmlns:ns2="urn:raverun" 
           xsi:type="ns2:po"
           xsi:null="true"/>
         ---------------------------
    (b2) Non null but nothing submitted in the body.
         ---------------------------
         <po 
           xmlns:ns2="urn:raverun" 
           xsi:type="ns2:po" />
         ---------------------------
    (b3) PO that violates the schema.
         ---------------------------
         <po 
           xmlns:ns2="urn:raverun" 
           xsi:type="ns2:po">
           <foo bar="123"/>
         </po>
         ---------------------------

第 4 步:为客户机和服务器段编写类型映射

最后,您需要声明类型映射来引用您定制的(反)序列化器。您可能会奇怪地看到 Section 5 被指定为 PurchaseOrder 的编码。这样做是为了方便起见,因为这样使您能够使用 ParameterSerializer 引导反序列化过程,还使您能够在序列化代码中使用 SoapEncUtils

 [Client]
    SOAPMappingRegistry smr = new SOAPMappingRegistry();
    smr.mapTypes(Constants.NS_URI_SOAP_ENC,
                   new QName("urn:raverun", "po"),
                   PurchaseOrder.class, pos, null);
  [Server]
    <isd:map
       encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"
       xmlns:x="urn:raverun" qname="x:po"
       javaType="com.raverun.po.castor.PurchaseOrder"
       xml2JavaClassName="com.raverun.po.PurchaseOrderSerializer" />

这个解决方案的潜在问题

在检查受模式约束的 SOAP 示例时,您应该记住下列问题:

  • 为了服从标准,必须关闭 <po> 元素中的任何 Section 5 声明。(关于兼容性更好的 SOAP XML 实例,请参阅 清单 3。)SOAP 1.1 规范(请参阅 参考资料)描述了这个要求,如下所示:

    长度为零的 URI("")的值明确表明对所包含元素的编码风格没做任何声明。

    encodingStyle 的一个替代方案是引入一个定制的 encodingStyleURI ,它是为通信的需要定制的。

  • Castor 中有一些错误需要提防,但所有的错误都有相应的解决方法。如果您用的是比 0.9.3 还老的 Castor 版本,那么模式验证就不会象预想的那样生效。解决方案是升级到最新的发行版。另一方面,Castor 0.9.3(我使用的版本)向标准输出流生成了一个假消息。我遇到的这种消息是:

             Warning : preserved is a bad entry for the whiteSpace value.

    Castor 的最新版本 0.9.3.9 除去了这个警告。

  • PurchaseOrderSerializer 并不序列化为多引用值。但它将对它们进行正确的反序列化。这 不是PurchaseOrderSerializer 本身的特征而是 ParameterSerializer 的。
清单 3. 一个兼容性更好的 SOAP 实例
<ns1:eatPo 
  xmlns:ns1="urn:poservice" 
  SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
  <po 
    xmlns:ns2="urn:raverun" 
    xsi:type="ns2:po"
    SOAP-ENV:encodingStyle="">
    <purchaseOrder xmlns="http://www.example.com/PO1">
      <header PONumber="9999-1212">
        <Date>2001-09-25T14:40:13.453</Date>
      </header>
      ...
    </purchaseOrder>
  </po>
</ns1:eatPo>

最近的接口变化

Apache SOAP(版本 2.2)的最新官方发行版于 2001 年 5 月面世。尽管开发工作的重点转移到了 Axis(目前是 beta 测试版 1),但仍继续添加错误修订。我们在等待 2.3 发行版(如果有的话)时,官方发行版的用户应该知道在 codebase,特别是 SOAPMappingRegistry 及其相关类中已经有了很大的更新。现有的代码可能需要做一些更改以便可以与这些修订进行互操作。

下面是一列值得注意的变化:

  • 现在,模式名称空间缺省情况下引用 2001 推荐名称空间。版本 2.2 引用 1999 名称空间。
  • 按照推理,如果您用 SOAPMappingRegistry 的无参数构造函数对它进行实例化,将返回一个具有 2001 名称空间意识的实例。
  • 已经根据静态工厂模式(factory pattern)重新设计了 SOAPMappingRegistry 的实例创建。于是,现在您应该使用工厂方法 getBaseRegistry(schemaURI) 来代替重载的构造函数 SOAPMappingRegistry(schemaURI)
     public static SOAPMappingRegistry getBaseRegistry (String schemaURI);
  • 版本 2.2 提供了串成注册表链的能力。最近已经添加了这些方法:
     public SOAPMappingRegistry(SOAPMappingRegistry parent);
      public SOAPMappingRegistry(SOAPMappingRegistry parent, String schemaURI);
      public SOAPMappingRegistry getParent()
      public String getSchemaURI()

    类型映射的解析将贯穿链直到找到一个匹配的。
  • DeploymentDescriptor 类把 qname 属性作为类型映射声明中的可选项。

结束语

我希望本文中的示例已经说明了这个系列第一篇文章中概述的理论性概念。如果跨网络上的许多机器执行 Web 服务操作将成为普遍的事实,那么开发者必须理解如何将编程对象从一台机器传送到另一台机器。能够更好地理解 SOAP 的类型映射应该会帮助您构建更好的分布式应用程序和服务。


相关主题


评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=SOA and web services
ArticleID=49595
ArticleTitle=Apache SOAP 类型映射,第 2 部分: 序列化详细说明
publish-date=05012002