第 2 章. 為什麼“契約優先”?

2.1. 簡介

建立 Web 服務有兩種開發方式:契約後置(Contract Last)契約優先(Contract First)。當使用契約後置方法時,您從 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>

此契約定義了一個接受日期的請求,這是一個表示年、月、日的 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,而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/編碼)已被棄用,取而代之的是文件/文字(請參閱 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]的啟發。

© . This site is unofficial and not affiliated with VMware.