内容


Web 服务的局部向后兼容(minor backward-compatible)

Web 服务版本控制

Comments

WSDL 1.1 规范(参见 参考资料)中没有涉及 Web 服务版本控制方面的议题,版本控制的意思是,当您的服务接口改变为新版本时该如何处理。当有人问我有关 Web 服务版本控制方面的问题时,我的回答总是简短的一句:那没有什么。然而,这并不是一个十分有用的回答,其中您确实要做一些事情,但是对于 Web 服务版本控制来说您要根据您的具体情况来做。您将亲手来开发这些解决方案。

没有必要非得弄清楚 API 版本究竟是个什么东西。Web 服务版本控制的通常方法是在新文件及新命名空间中创建全新的 Web 服务。这可是一个相当激进的解决方案。有关这个解决方案的讨论以及 版本控制的含义,可以参考文章:Web 服务版本控制最佳实践(参见 参考资料)。我并不打算在此引用该主题,相反,我将重点放在您可以对现有服务所能进行的改变上,这样就可以在原有客户端上添加向后兼容。

您可以对您的服务进行任意类型的改变。不尽人意的是,在这些类型的大部分改变中,主要的变化是要求您创建全新的 Web 服务。有一套局部改变的方式可以让您不用创建新 Web 服务而使其改变。虽然是局部的改变,但改变后的服务必须是向后兼容的。就是说旧客户端仍可以继续与您新创建的 Web 服务通信。有两类向后兼容的改变:对服务输入的改变及对服务输出的改变。

服务输入的向后兼容改变

服务输入的向后兼容改变意味着新 Web 服务可以继续接收来自旧客户端的数据。这些改变包含以下方面:

  • 新操作
  • 新数据类型
  • 现有数据类型的新的可选字段
  • 类型扩展

新操作

简单的添加新操作是更新 Web 服务的一个相当常见的原因。幸运的是,这是一个局部的改变。如果您对服务添加新操作而没有移除任何旧操作,并且没有影响到现有行为的话,那么旧版本客户端的 API 将能继续与新服务共同工作。

新数据类型

您可以为新方法添加新类型。只要您没有改变或移除任何旧类型,旧版本的客户端将能继续与新服务共同工作。

您也可以为现有类型添加扩展,但要慎重。 只有当它是做为新服务的输入来接收时,添加扩展类型才是局部变化的。新服务不能返回新的扩展类型。如果旧客户端接收了新的扩展类型,那么反序列化(deserialization)将会以失败而告终。

现有数据类型的新的可选字段

您可以为现有 complexType 添加元素,但必须使其为可选的(使用 minOccurs="0" 属性)。但要注意,只有在当它做为新服务的输入来接收时,添加可选元素才是局部变化的。新服务不能带有新字段而返回 complexType。如果旧客户端接收了新字段,那么客户端反序列化将会失败,那是因为客户端无法知道新字段的信息。

您也可以以同样的方式为 complexType 添加新属性,但必须确保属性模式为 use="optional"

类型扩展

您可以为服务接收的数据类型扩展值空间。换一种说法,如果新类型的可能值是旧类型的超集,那么您就已经对类型进行了扩展。发送旧类型的客户端对新服务来说仍然可用。

注意:不能与 RPC/编码服务协同工作。虽然类型信息是以 RPC/编码消息的形式发送,但类型不能改变。它只能与 Literal 服务协同工作,此服务也只发送值信息。当接收的值为 true 时,例如,只通过查看的方式,您不能知道它是 Boolean 类型还是 String 类型,此时就要调用服务分派器来确定确切的值类型。

这也只是对新服务接收的数据类型有效,并不是对服务发出的类型有效。如果您试图发送给客户端扩展类型的值(处于旧类型值空间范围之外),而此客户端并不知道那个新类型,那么客户端将不会成功的反序列化那个类型。

类型扩展与前面的向后兼容策略比起来显得有些冒险。其他的都是添加,但这是一个改变,API 的改变也经常涉及底层实现的改变,千万要注意!当只是做局部改变的时候,您并不想改变现有行为。您必须要保留原有值的语义。只有当旧行为继续与旧值共同存在时,类型扩展才是安全的。但是当实现的行为接受了旧值改变时,这就不是向后兼容改变。

服务输出的向后兼容改变

服务输出的向后兼容改变是:新 Web 服务可以继续向旧客户端发送数据。

我认为只有一个对输出的向后兼容改变: 类型约束。服务器可以限制发送给旧客户端字段的类型。旧客户端将仍然期望对类型值的扩展,所以它不会在意目前对值的限制,但要小心:只有当给客户端发送数据的时候类型约束才是局部改变的。不要限制服务从客户端接收的类型。旧客户端可以发送新的、受限制的值空间之外的值。

样例

清单 1 的基本服务开始。它为添加地址簿提供了一个 API。我将详细讲解一些变化的情况,他们将为在上文中列出的每一个局部改变提供样例。

清单 1. 添加地址簿服务版本 1.0
<?xml version="1.0" ?>
<definitions targetNamespace="urn:add.addressBook/1.0"
             xmlns:tns="urn:add.addressBook/1.0"
             xmlns:xsd="http://www.w3.org/2001/XMLSchema"
             xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
             xmlns="http://schemas.xmlsoap.org/wsdl/">
  <types>
    <schema targetNamespace="urn:addressBook/1.0"
            xmlns="http://www.w3.org/2001/XMLSchema"
            xmlns:tns="urn:addressBook/1.0">
      <complexType name="phone">
        <sequence>
            <element name="areaCode" type="int"/>
            <element name="exchange" type="int"/>
            <element name="number" type="int"/>
        </sequence>
      </complexType>
      <complexType name="address">
        <sequence>
            <element name="streetNum" type="int"/>
            <element name="streetName" type="string"/>
            <element name="city" type="string"/>
            <element name="state" type="string"/>
            <element name="zip" type="int"/>
            <element name="phoneNumber" type="tns:phone"/>
        </sequence>
      </complexType>
    </schema>
    <schema targetNamespace="urn:add.addressBook/1.0"
            xmlns="http://www.w3.org/2001/XMLSchema"
            xmlns:tns="urn:add.addressBook/1.0"
            xmlns:addressbook="urn:addressBook/1.0">
      <element name="addAddress" type="tns:addAddress"/>
      <complexType name="addAddress">
        <sequence>
          <element name="name" type="string"/>
          <element name="address" type="addressbook:address"/>
        </sequence>
      </complexType>
      <element name="addAddressResponse" type="tns:addAddressResponse"/>
      <complexType name="addAddressResponse">
        <sequence>
          <element name="returnCode" type="string"/>
        </sequence>
      </complexType>
    </schema>
  </types>
  <message name="AddAddressRequest">
    <part name="parameters" element="tns:addAddress"/>
  </message>
  <message name="AddAddressResponse">
    <part name="parameters" element="tns:addAddressResponse"/>
  </message>
  <portType name="AddressBook">
    <operation name="addAddress">
      <input message="tns:AddAddressRequest"/>
      <output message="tns:AddAddressResponse"/>
    </operation>
  </portType>
  <!-- binding declns -->
  <binding name="AddressBookSOAPBinding" type="tns:AddressBook">
    <soap:binding style="document"
                  transport="http://schemas.xmlsoap.org/soap/http"/>
    <operation name="addEntry">
      <soap:operation soapAction=""/>
      <input>
        <soap:body use="literal"/>
      </input>
      <output>
        <soap:body use="literal"/>
      </output>
    </operation>
    <operation name="addAddress">
      <soap:operation soapAction=""/>
      <input>
        <soap:body use="literal"/>
      </input>
      <output>
        <soap:body use="literal"/>
      </output>
    </operation>
  </binding>
  <service name="AddressBookService">
    <port name="AddressBook" binding="tns:AddressBookSOAPBinding">
      <soap:address location="http://localhost:9080/AddressBook/services/Add"/>
    </port>
  </service>
</definitions>

新操作

让我们假设有许多您的客户端想知道在您的地址簿上存有多少地址。他们提出 count 操作。对如此一个简单的操作您不必改变任何现有行为,所以您同意将其作为 API 局部改变。参见 清单 2

清单 2. 新操作:count
  <types>
    <schema ...>
              <element name="count" type="tns:count"/>
      <complexType name="count">
        <sequence/>
      </complexType>
      <element name="countResponse" type="tns:countResponse"/>
      <complexType name="countResponse">
        <sequence>
          <element name="count" type="int"/>
        </sequence>
      </complexType>
    </schema>
  </types>
  ...
          <message name="CountRequest">
    <part name="parameters" element="tns:count"/>
  </message>
  <message name="CountResponse">
    <part name="parameters" element="tns:countResponse"/>
  </message>
  <portType name="AddressBook">
    ...
            <operation name="count">
      <input message="tns:CountRequest"/>
      <output message="tns:CountResponse"/>
    </operation>
  </portType>
  ...

新数据类型

您注意到 phone 数据类型并不够。您拥有很多业务客户端并且他们其中许多还要扩展电话号码。所以您要以 businessPhone 类型扩展 phone 类型。这个新数据类型是唯一的服务输入,从不输出,所以旧客户端可以继续使用旧电话号码。新的(发送新的 businessPhone)客户端将可以工作,因为您的新服务可以接收这些新类型。

清单 3. 新数据类型: businessPhone
      <complexType name="businessPhone">
        <complexContent>
          <extension base="tns:phone">
            <sequence>
              <element name="extension" type="string"/>
            </sequence>
          </extension>
        </complexContent>
      </complexType>

新建现有数据类型的可选字段

您也注意到您的 address 数据类型也是不够用的,因为它没有记录房间地址。所以为 phone 添加一个新的、可选的 apptNum 元素(参见 清单 4)。您可以使这个元素为向后兼容的可选项,您会发现这个字段的这个可选性是一个很不错的主意,因为并不是所有的地址都有房间号码。

您可能已经注意到您既可以为 address 添加 apptNum 作为可选字段,或以 apptAddress 子类型扩展 address,然后添加新的 apptNum 字段。其实,这两个方法本质上完成的是相同的功能:添加可选字段。那么为什么要以其中一个代替另一个呢?

  • 可选字段生成更小的 SOAP 消息。当发送扩展类型时,类型信息在 SOAP 消息中被传送。当发送可选字段时,只能发送那个可选字段(或是不发送)。
  • 您的设计有时可以支配您的选择,例如,以子类型扩展基本类型则更加趋向于面向对象方法。
清单 4. 在现有数据类型中新建可选字段: apptNum
      <complexType name="address">
        <sequence>
          
        <element name="apptNum" type="int" minOccurs="0"/>
          <element name="streetNum" type="int"/>
          <element name="streetName" type="string"/>
          <element name="city" type="string"/>
          <element name="state" type="string"/>
          <element name="zip" type="int"/>
          <element name="phoneNumber" type="tns:phone"/>
        </sequence>
      </complexType>

类型扩展

由于市场因素,可能许多您的客户端在它们的电话号码中使用字符而不单单是数字。要将电话类型从整数扩展为字符串是一件很容易的事,虽然这样做将会为服务检查输入的数据带来一些工作量。但是这确实是局部 API 改变,所以您可以不妨一试。当旧客户端发送 int 时,保持原有整型数的特性,它们既可以被作为字符串也可以作为整数( ints)发送。 参见 清单 5

清单 5. 类型扩展:电话号码
      <complexType name="phone">
        <sequence>
          <element name="areaCode" type="
        string"/>
          <element name="exchange" type="
        string"/>
          <element name="number" type="
        string"/>
        </sequence>
      </complexType>

类型约束

您会发现字符串类型的 resultCode 范围有点太大了。因为字符串可以为任何内容,客户端不会确切的知道如何处理这个值。您可能要口头的告诉他们:成功的调用将返回 OK。但是您的客户端在 Internet 上,您无法为它给出口头的说明,那么它们怎么能知道结果何时是正确的呢?所以您就会冥思苦想出 resultCode 的所有可能的返回值,然后为 resultCode 创建一大堆罗列的字符串(参见 清单 6)。(当然,这只是一个普通例子。您确实可以在操作失败时返回错误信息,但我只想保持例子的简单性。)

清单 6. 类型约束: resultCode
      <simpleType name="returnCode">
        <restriction base="string">
          <enumeration value="OK"/>
          <enumeration value="We already have an entry for that name."/>
          <enumeration value="Address Book is full."/>
          <enumeration value="Some other failure."/>
        </restriction>
      </simpleType>
      ...
      <complexType name="addAddressResponse">
        <sequence>
          <element name="returnCode" type="
        tns:returnCode"/>
        </sequence>
      </complexType>

所有的这些向后兼容的改变都已经被集中到了 清单 7 中的 WSDL 中。使用这个 WSDL 建立服务的新版本。您的客户端是使用这个新版本的 WSDL 还是使用 清单 1 中的版本都没有关系,由于您已经小心谨慎的只是做着向后兼容的改变,所以他们都将工作正常。

清单 7. 完成添加地址簿服务,1.1 版本
<?xml version="1.0" ?>
<definitions targetNamespace="urn:Add.AddressBook/1.0"
             xmlns:tns="urn:Add.AddressBook/1.0"
             xmlns:xsd="http://www.w3.org/2001/XMLSchema"
             xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
             xmlns="http://schemas.xmlsoap.org/wsdl/">
  <types>
    <schema targetNamespace="urn:AddressBook/1.0"
            xmlns="http://www.w3.org/2001/XMLSchema"
            xmlns:tns="urn:AddressBook/1.0">
      <complexType name="phone">
        <sequence>
          <element name="areaCode" type="string"/>
          <element name="exchange" type="string"/>
          <element name="number" type="string"/>
        </sequence>
      </complexType>
      <complexType name="businessPhone">
        <complexContent>
          <extension base="tns:phone">
            <sequence>
              <element name="extension" type="string"/>
            </sequence>
          </extension>
        </complexContent>
      </complexType>
      <complexType name="address">
        <sequence>
          <element name="apptNum" type="int" minOccurs="0"/>
          <element name="streetNum" type="int"/>
          <element name="streetName" type="string"/>
          <element name="city" type="string"/>
          <element name="state" type="string"/>
          <element name="zip" type="int"/>
          <element name="phoneNumber" type="tns:phone"/>
        </sequence>
      </complexType>
    </schema>
    <schema targetNamespace="urn:Add.AddressBook/1.0"
            xmlns="http://www.w3.org/2001/XMLSchema"
            xmlns:tns="urn:Add.AddressBook/1.0"
            xmlns:addressbook="urn:AddressBook/1.0">
      <simpleType name="returnCode">
        <restriction base="string">
          <enumeration value="OK"/>
          <enumeration value="We already have an entry for that name."/>
          <enumeration value="Address Book is full."/>
          <enumeration value="Some other failure."/>
        </restriction>
      </simpleType>
      <element name="addAddress" type="tns:addAddress"/>
      <complexType name="addAddress">
        <sequence>
          <element name="name" type="string"/>
          <element name="address" type="addressbook:address"/>
        </sequence>
      </complexType>
      <element name="addAddressResponse" type="tns:addAddressResponse"/>
      <complexType name="addAddressResponse">
        <sequence>
          <element name="returnCode" type="tns:returnCode"/>
        </sequence>
      </complexType>
      <element name="count" type="tns:count"/>
      <complexType name="count">
        <sequence/>
      </complexType>
      <element name="countResponse" type="tns:countResponse"/>
      <complexType name="countResponse">
        <sequence>
          <element name="count" type="int"/>
        </sequence>
      </complexType>
    </schema>
  </types>
  <message name="AddAddressRequest">
    <part name="parameters" element="tns:addAddress"/>
  </message>
  <message name="AddAddressResponse">
    <part name="parameters" element="tns:addAddressResponse"/>
  </message>
  <message name="CountRequest">
    <part name="parameters" element="tns:count"/>
  </message>
  <message name="CountResponse">
    <part name="parameters" element="tns:countResponse"/>
  </message>
  <portType name="AddressBook">
    <operation name="addAddress">
      <input message="tns:AddAddressRequest"/>
      <output message="tns:AddAddressResponse"/>
    </operation>
    <operation name="count">
      <input message="tns:CountRequest"/>
      <output message="tns:CountResponse"/>
    </operation>
  </portType>
  <binding name="AddressBookSOAPBinding" type="tns:AddressBook">
    <soap:binding style="document"
                  transport="http://schemas.xmlsoap.org/soap/http"/>
    <operation name="addAddress">
      <soap:operation soapAction=""/>
      <input>
        <soap:body use="literal"/>
      </input>
      <output>
        <soap:body use="literal"/>
      </output>
    </operation>
    <operation name="count">
      <soap:operation soapAction=""/>
      <input>
        <soap:body use="literal"/>
      </input>
      <output>
        <soap:body use="literal"/>
      </output>
    </operation>
  </binding>
  <service name="AddressBookService">
    <port name="AddressBook" binding="tns:AddressBookSOAPBinding">
      <soap:address location="http://localhost:9080/AddressBook/services/Add"/>
    </port>
  </service>
</definitions>

新客户端,旧服务?

到这里为止,我只是讨论了新 Web 服务和旧客户端。虽然可能性不大,但您或许希望客户端与旧 Web 服务通信。那么可以应用一系列的向后兼容的改变么?在某种程度上来说是可以的。可能的向后兼容改变现在变成了对客户端的输入与输出,而不是对服务器,看起来这或许有一点奇怪。让我们考虑一下他们其中的每一项。要记住:您的新客户端并不知道他是与新的还是旧的服务通信。

  • 新操作?不可以。客户端不能接收操作,只能调用他们,所以不能添加新操作。旧服务不能接收他们。
  • 新数据类型?在某种程度上可以。新类型将被输入给客户端。既然您不可以拥有新操作,唯一可以输入到客户端的新类型是对只返回给客户端的现有类型的扩展。新服务可以返回这个新扩展。旧服务只能发送旧类型。
  • 新可选字段?可以。记住,这只对返回给客户端的 complexType 有效。旧客户端永远不返回新字段,而新服务可以。
  • 类型扩展?可以。旧服务为客户端返回的类型将被限制在其旧类型的值空间中,这将可以在新类型的值空间中继续工作。
  • 类型约束?可以。记住,这只对客户端的输出有效。所以您只可以约束客户端发送给服务的类型。

版本控制的设计

您可能想要考虑最初设计的服务版本控制了。如果是这样的话,那么您可要有许多操作要做。

从输出中剥离出输入

如果您仔细斟酌这篇文章的服务样例的话,这个例子就会显得有些不足。对地址簿可以做的所有操作只是添加?但是我创建如此简单的一个服务是有原因的(不是简单而明显的原因)。如果将添加与查询功能都包含在这一个服务中的话,那么我们对数据类型所做的所有改变将不再是局部改变了。他们不仅将影响对服务的输入,而且将影响从服务的输出。我所展示的局部改变都不能对输入与输出同时进行改变。这就是为什么这个服务只是一个“添加服务”的原因。假设有另外一个服务对这个地址簿进行查询。注意,当设计您的服务时,版本控制可能不是要将输入从输出中剥离出来的唯一原因。例如,查询服务通常更简单且比更新服务需要更少的服务功能(例如,安全或事务的可靠性)。

将服务分割成输入与输出服务可能很棘手。假设输入与输出服务都使用相同的数据类型。如果您对数据类型不想有重大改变的话,只要单独改变输入服务就可以了。如果在输出服务中改变数据类型,那可是个重大改变。因此要避免重大改变,输出服务实现将在返回之前从新数据类型转变为旧数据类型。

为输出服务简化这个问题的一个选择是在操作中增加显式的“version”参数,或是在“complexType”中增加显式的“version”字段。输出服务的客户端将通知服务他们可以理解哪个数据类型的局部版本,所以服务可以发送给他们适合他们的数据类型。但是现在真正到了创建您自己的版本控制的时候了。

填充字段

您或许应该避免一味的局部改变。如果您认为您以后的改变将只是附加物的话,您可以从一开始就为您的设计简单的添加填充字段。这些字段没有语义,但是他们将占据空间(即使如果他们是可选的,他们仍在逻辑上存在)所以在日后您的客户端将知道他们。 xsd:string 是这些额外字段的良好的全捕获(catch-all)类型。即使您决定将来的某个版本的类型是“integer”或“boolean”,您总可以通知您的新客户端他们应该期待及保留什么,做出恰当的转换。(如果这样做就很困难:以 xsd:int 填充字段开始,以后才决定他应该是字符串。)

即使字符串是足够的填充类型,但他们仍不是最好的。他们并不能很好的处理复杂类型。所以您可能决定填充字段应当是 xsd:anyTypexsd:any。然而,对 any 类型也要小心。他们并不像他们看起来那么有用。any 类型的明显的缺点是:在不同的语言映射中他们通常仅仅映射到可用类型。例如,在 JAX-RPC 中 xsd:any 映射到 javax.xml.soap.SOAPElement,虽然 xsd:anyType 经常映射到 java.lang.Object,但在这里却没有定义。但更糟糕的是,API 说您可以发送任何东西,但这不意味着您真的可以发送任何东西。另一方面 必须能接收您所发送出给他的东西。通常这意味着另一方面必须有类型的类型码映射、序列化、反序列化,特别是通过 xsd:anyType 发送的实例。如果您在 xsd:anyType 字段中给旧接收器发送了新类型,那个接收器很有可能不会将那个类型反序列化到特定于语言的对象中。至少在 Java 中对 JAX-RPC 的映射来说, xsd:anyxsd:anyType 更灵活些。因为它映射到 SOAPElement,这意味着类型保留于 XML 格式的固有类型中,而不是要反序列化到特定于语言的对象中。但 SOAPElement 仍不是与 API 最友好的。

对填充字段最后一个说明。您如何知道多少才够呢?当然,您不知道。所以添加的安全要点是:如果使用填充字段,就要使其为可选的(例如,使用 minOccurs="0")并且使其为开放式(open-ended)的(使用 maxOccurs="unbounded")。

结束语

目前几乎不存在 Web 服务版本控制。每个 版本本质上都是新命名空间中的新 Web 服务。但正如这篇文章所述,如果您对您的服务做一些局部的、向后兼容的改变,时刻注意不改变其行为,那么旧客户端仍可以与您的新服务进行通信。您更希望您的服务的设计从最早期开始,就能适应未来的、局部改变的、更加简单的需求。


相关主题


评论

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

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=SOA and web services, XML
ArticleID=49232
ArticleTitle=Web 服务的局部向后兼容(minor backward-compatible)
publish-date=12012004