第 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] 的啟發。