第 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 的请求,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 引用 Passenger,后者再次引用 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) 中使用这些引用的标准方法已被弃用,转而支持 document/literal(请参阅 WS-I 基本配置文件)。

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

2.3。契约优先与契约滞后

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

2.3.1。脆弱性

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

此外,并非所有的 SOAP 堆栈都会从 Java 契约生成相同的 Web 服务契约。这意味着将您当前的 SOAP 堆栈更改为不同的 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] 的启发。