第 2 章 为什么选择契约优先?

2.1 简介

在创建 Web 服务时,有两种开发风格:契约最后契约优先。当使用契约最后的方法时,您从 Java 代码开始,并让 Web 服务契约(WSDL,参见侧边栏)由此生成。当使用契约优先时,您从 WSDL 契约开始,并使用 Java 来实现该契约。

Spring-WS 仅支持契约优先的开发风格,本节将解释其原因。

2.2 对象/XML 阻抗不匹配

类似于 ORM 领域中我们遇到的对象/关系阻抗不匹配,在将 Java 对象转换为 XML 时也存在类似的问题。乍一看,O/X 映射问题似乎很简单:为每个 Java 对象创建一个 XML 元素,将所有 Java 属性和字段转换为子元素或属性。但是,事情并不像看起来那么简单:分层语言(如 XML,尤其是 XSD)与 Java 的图模型之间存在根本差异[1]

2.2.1 XSD 扩展

在 Java 中,更改类行为的唯一方法是对其进行子类化,并将新行为添加到该子类中。在 XSD 中,您可以通过限制数据类型来扩展它:即,约束元素和属性的有效值。例如,考虑以下示例

<simpleType name="AirportCode">
  <restriction base="string">
      <pattern value="[A-Z][A-Z][A-Z]"/>
  </restriction>
</simpleType>

此类型通过正则表达式限制 XSD 字符串,只允许三个大写字母。如果此类型转换为 Java,我们将得到一个普通的java.lang.String;转换过程中正则表达式会丢失,因为 Java 不允许这种扩展。

2.2.2 不可移植类型

Web 服务最重要的目标之一是互操作性:支持 Java、.NET、Python 等多种平台。由于所有这些语言都具有不同的类库,因此您必须使用一些通用的、跨语言的格式在它们之间进行通信。这种格式是 XML,所有这些语言都支持它。

由于这种转换,您必须确保在服务实现中使用可移植类型。例如,考虑一个返回java.util.TreeMap的服务,如下所示

public Map getFlights() {
  // use a tree map, to make sure it's sorted
  TreeMap map = new TreeMap();
  map.put("KL1117", "Stockholm");
  ...
  return map;
}

毫无疑问,此映射的内容可以转换为某种 XML,但由于没有标准方法来描述 XML 中的映射,因此它将是专有的。此外,即使它可以转换为 XML,许多平台也没有类似于TreeMap的数据结构。因此,当 .NET 客户端访问您的 Web 服务时,它可能会最终得到一个System.Collections.Hashtable,它具有不同的语义。

在客户端工作时也会遇到此问题。考虑以下 XSD 代码片段,它描述了一个服务契约

<element name="GetFlightsRequest">
  <complexType>
    <all>
      <element name="departureDate" type="date"/>
      <element name="from" type="string"/>
      <element name="to" type="string"/>
    </all>
  </complexType>
</element>

此契约定义了一个请求,该请求接受一个date,这是一个表示年份、月份和日期的 XSD 数据类型。如果我们从 Java 调用此服务,我们可能会使用java.util.Datejava.util.Calendar。但是,这两个类实际上都描述了时间,而不是日期。因此,我们实际上最终会发送表示 2007 年 4 月 4 日午夜(2007-04-04T00:00:00)的数据,这与2007-04-04不同。

2.2.3 循环图

假设我们有以下简单的类结构

public class Flight {
  private String number;
  private List<Passenger> passengers;
    
  // getters and setters omitted
}

public class Passenger {
  private String name;
  private Flight flight;
    
  // getters and setters omitted
}

这是一个循环图:Flight引用PassengerPassenger又引用Flight。这样的循环图在 Java 中非常常见。如果我们采用一种天真的方法将其转换为 XML,我们将得到类似以下内容

<flight number="KL1117">
  <passengers>
    <passenger>
      <name>Arjen Poutsma</name>
      <flight number="KL1117">
        <passengers>
          <passenger>
            <name>Arjen Poutsma</name>
            <flight number="KL1117">
              <passengers>
                <passenger>
                   <name>Arjen Poutsma</name>
                   ...

这将需要相当长的时间才能完成,因为此循环没有停止条件。

解决此问题的一种方法是使用对已编组对象的引用,如下所示

<flight number="KL1117">
  <passengers>
    <passenger>
      <name>Arjen Poutsma</name>
      <flight href="KL1117" />
    </passenger>
    ...
  </passengers>
</flight>

这解决了递归问题,但引入了新的问题。首先,您不能使用 XML 验证器来验证此结构。另一个问题是,在 SOAP(RPC/encoded)中使用这些引用的标准方法已被弃用,取而代之的是文档/文字(参见 WS-I基本配置文件)。

这些只是处理 O/X 映射时遇到的一些问题。在编写 Web 服务时,务必注意这些问题。尊重这些问题的最佳方法是完全专注于 XML,同时使用 Java 作为实现语言。这就是契约优先的意义所在。

2.3 契约优先与契约最后

除了上一节中提到的对象/XML 映射问题之外,还有其他一些原因让人们更喜欢契约优先的开发风格。

2.3.1 脆弱性

如前所述,契约最后的开发风格导致您的 Web 服务契约(WSDL 和您的 XSD)由您的 Java 契约(通常是接口)生成。如果您使用这种方法,则无法保证契约随时间推移保持不变。每次更改 Java 契约并重新部署它时,Web 服务契约可能会发生后续更改。

此外,并非所有 SOAP 堆栈都从 Java 契约生成相同的 Web 服务契约。这意味着将当前 SOAP 堆栈更改为另一个堆栈(无论出于何种原因),也可能会更改您的 Web 服务契约。

当 Web 服务契约发生更改时,契约的用户将需要获得新契约,并可能更改其代码以适应契约中的任何更改。

为了使契约有用,它必须尽可能长时间地保持不变。如果契约发生更改,您将需要联系服务的所有用户,并指示他们获取新版本的契约。

2.3.2 性能

当 Java 自动转换为 XML 时,无法确定发送到网络上的内容。一个对象可能引用另一个对象,另一个对象又引用另一个对象,依此类推。最终,虚拟机中堆上的对象有一半可能会转换为 XML,这会导致响应时间变慢。

当使用契约优先时,您可以明确描述发送 XML 的位置,从而确保它完全符合您的要求。

2.3.3 可重用性

在单独的文件中定义架构允许您在不同的场景中重用该文件。如果您在名为airline.xsd的文件中定义一个AirportCode,如下所示

<simpleType name="AirportCode">
    <restriction base="string">
        <pattern value="[A-Z][A-Z][A-Z]"/>
    </restriction>
</simpleType>

您可以使用import语句在其他架构甚至 WSDL 文件中重用此定义。

2.3.4 版本控制

尽管契约必须尽可能长时间地保持不变,但它们确实有时需要更改。在 Java 中,这通常会导致出现新的 Java 接口,例如AirlineService2,以及该接口的(新)实现。当然,必须保留旧服务,因为可能有一些尚未迁移的客户端。

如果使用契约优先,我们可以使契约和实现之间具有更松散的耦合。这种更松散的耦合允许我们在一个类中实现契约的两个版本。例如,我们可以使用 XSLT 样式表将任何“旧样式”消息转换为“新样式”消息。



[1] 本节中的大部分内容都受到[alpine][effective-enterprise-java]的启发。