© 2005-2020 原始作者。

您可以為了個人使用及散佈給他人而複製本文件,前提是您不得對此類副本收取任何費用,且進一步規定無論以印刷或電子方式散佈,每個副本都包含此版權聲明。

序言

在當前服務導向架構的時代,越來越多人使用 Web 服務來連接先前未連接的系統。最初,Web 服務被認為只是執行遠端程序呼叫 (RPC) 的另一種方式。然而,隨著時間的推移,人們發現 RPC 和 Web 服務之間存在很大的差異。特別是當與其他平台互通性很重要時,最好發送封裝的 XML 文件,其中包含處理請求所需的所有資料。從概念上講,基於 XML 的 Web 服務更像是訊息佇列,而不是遠端解決方案。總體而言,XML 應被視為資料的平台中立表示形式,即 SOA 的通用語言。在開發或使用 Web 服務時,重點應放在此 XML 上,而不是 Java 上。

Spring Web Services 專注於建立這些文件驅動的 Web 服務。Spring Web Services 促進了 contract-first SOAP 服務開發,透過使用多種操作 XML 酬載的方式之一,從而建立彈性的 Web 服務。Spring-WS 提供了強大的訊息分派框架、與您現有的應用程式安全性解決方案整合的WS-Security 解決方案,以及遵循熟悉 Spring 範本模式的用戶端 API

I. 緒論

本參考文件的第一部分概述了 Spring Web Services 和底層概念。然後介紹了 Spring-WS,並解釋了 contract-first Web 服務開發背後的概念

1. 什麼是 Spring Web Services?

1.1. 簡介

Spring Web Services (Spring-WS) 是 Spring 社群的產品,專注於建立文件驅動的 Web 服務。Spring Web Services 旨在促進 contract-first SOAP 服務開發,透過使用多種操作 XML 酬載的方式之一,從而建立彈性的 Web 服務。該產品基於 Spring 本身,這表示您可以將 Spring 概念(例如依賴注入)用作 Web 服務的組成部分。

人們使用 Spring-WS 的原因有很多,但大多數人在發現替代的 SOAP 堆疊在遵循 Web 服務最佳實務方面有所不足後,才被它吸引。Spring-WS 使最佳實務成為一種簡單的實務。這包括 WS-I 基本設定檔、contract-first 開發以及合約與實作之間鬆散耦合等實務。Spring Web Services 的其他主要功能包括

1.1.1. 強大的映射

您可以根據訊息酬載、SOAP Action 標頭或 XPath 表達式,將傳入的 XML 請求分發到任何物件。

1.1.2. XML API 支援

傳入的 XML 訊息不僅可以使用標準 JAXP API(例如 DOM、SAX 和 StAX)處理,還可以透過 JDOM、dom4j、XOM 甚至 marshalling 技術處理。

1.1.3. 彈性的 XML Marshalling

Spring Web Services 建構於 Spring Framework 中的物件/XML 映射模組之上,該模組支援 JAXB 1 和 2、Castor、XMLBeans、JiBX 和 XStream。

1.1.4. 重用您的 Spring 專業知識

Spring-WS 對於所有配置都使用 Spring 應用程式上下文,這應有助於 Spring 開發人員快速上手。此外,Spring-WS 的架構類似於 Spring-MVC 的架構。

1.1.5. 支援 WS-Security

WS-Security 可讓您簽署 SOAP 訊息、對其進行加密和解密,或針對它們進行身份驗證。

1.1.6. 與 Spring Security 整合

Spring Web Services 的 WS-Security 實作提供了與 Spring Security 的整合。這表示您也可以將現有的 Spring Security 配置用於您的 SOAP 服務。

1.1.7. Apache 授權條款

您可以放心地在您的專案中使用 Spring-WS。

1.2. 執行環境

Spring Web Services 需要標準 Java 8 執行環境。Spring-WS 建構於 Spring Framework 4.0.9 之上,但支援更高版本。

Spring-WS 由多個模組組成,本節的其餘部分將對其進行說明。

  • XML 模組 (spring-xml.jar) 包含 Spring Web Services 的各種 XML 支援類別。此模組主要用於 Spring-WS 框架本身,而不是 Web 服務開發人員。

  • 核心模組 (spring-ws-core.jar) 是 Spring Web 服務功能的核心部分。它提供了核心WebServiceMessageSoapMessage 介面、伺服器端框架(具有強大的訊息分派功能)、用於實作 Web 服務端點的各種支援類別,以及用戶端 WebServiceTemplate

  • 支援模組 (spring-ws-support.jar) 包含其他傳輸(JMS、電子郵件和其他)。

  • 安全性套件 (spring-ws-security.jar) 提供了與核心 Web 服務套件整合的 WS-Security 實作。它可讓您簽署、解密和加密 SOAP 訊息,並將主體令牌新增至其中。此外,它還可讓您使用現有的 Spring Security 安全性實作進行身份驗證和授權。

下圖顯示了 Spring-WS 模組之間的依賴關係。箭頭表示依賴關係(也就是說,Spring-WS Core 依賴於 Spring-XML 和 Spring 3 及更高版本中找到的 OXM 模組)。

spring deps

1.3. 支援的標準

Spring Web Services 支援以下標準

  • SOAP 1.1 和 1.2

  • WSDL 1.1 和 2.0(僅 WSDL 1.1 支援基於 XSD 的產生)

  • WS-I Basic Profile 1.0、1.1、1.2 和 2.0

  • WS-Addressing 1.0 和 2004 年 8 月草案

  • SOAP Message Security 1.1、Username Token Profile 1.1、X.509 Certificate Token Profile 1.1、SAML Token Profile 1.1、Kerberos Token Profile 1.1、Basic Security Profile 1.1

2. 為什麼選擇 Contract First?

在建立 Web 服務時,有兩種開發風格:contract-last 和 contract-first。當您使用 contract-last 方法時,您從 Java 程式碼開始,並讓 Web 服務契約(在 WSDL 中 — 請參閱側邊欄)從中產生。當使用 contract-first 時,您從 WSDL 契約開始,並使用 Java 來實作契約。

什麼是 WSDL?

WSDL 代表 Web Service Description Language(Web 服務描述語言)。WSDL 檔案是一個描述 Web 服務的 XML 文件。它指定服務的位置以及服務公開的操作(或方法)。有關 WSDL 的更多資訊,請參閱 WSDL 規格

Spring-WS 僅支援 contract-first 開發風格,本節將說明原因。

2.1. 物件/XML 阻抗失配

與 ORM 領域類似,在 ORM 領域中我們存在物件/關係阻抗失配,將 Java 物件轉換為 XML 也存在類似的問題。乍看之下,O/X 映射問題似乎很簡單:為每個 Java 物件建立一個 XML 元素,以將所有 Java 屬性和欄位轉換為子元素或屬性。但是,事情並不像看起來那麼簡單,因為層次結構語言(例如 XML,尤其是 XSD)與 Java 的圖形模型之間存在根本差異。

本節中的大部分內容靈感來自 [alpine][effective-enterprise-java]

2.1.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.1.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.1.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>
                   ...

處理這樣的結構很可能需要很長時間才能完成,因為此迴圈沒有停止條件。

解決此問題的一種方法是使用對已 marshalling 物件的參考

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

這解決了遞迴問題,但也引入了新問題。首先,您不能使用 XML 驗證器來驗證此結構。另一個問題是,在 SOAP 中使用這些參考的標準方法 (RPC/encoded) 已被棄用,而改用 document/literal(請參閱 WS-I Basic Profile)。

這些只是處理 O/X 映射時遇到的一些問題。在編寫 Web 服務時,尊重這些問題非常重要。尊重它們的最佳方法是完全專注於 XML,同時使用 Java 作為實作語言。這就是 contract-first 的全部意義。

2.2. Contract-first 與 Contract-last 的比較

除了上一節中提到的物件/XML 映射問題外,還有其他原因偏好 contract-first 開發風格。

2.2.1. 脆弱性

如前所述,contract-last 開發風格會導致您的 Web 服務契約(WSDL 和您的 XSD)從您的 Java 契約(通常是介面)產生。如果您使用這種方法,則無法保證契約會隨著時間的推移保持不變。每次您變更 Java 契約並重新部署它時,Web 服務契約都可能會隨之變更。

此外,並非所有 SOAP 堆疊都從 Java 契約產生相同的 Web 服務契約。這表示,將您目前的 SOAP 堆疊變更為不同的堆疊(無論出於何種原因)也可能會變更您的 Web 服務契約。

當 Web 服務契約變更時,契約的使用者必須收到指示以取得新契約,並可能變更其程式碼以適應契約中的任何變更。

為了使契約有用,它必須盡可能長時間地保持不變。如果契約變更,您必須聯絡服務的所有使用者,並指示他們取得新版本的契約。

2.2.2. 效能

當 Java 物件自動轉換為 XML 時,無法確定透過網路發送了什麼。一個物件可能會參考另一個物件,而另一個物件又參考另一個物件,依此類推。最後,虛擬機器中堆積上的一半物件可能會轉換為 XML,這會導致響應時間變慢。

當使用 contract-first 時,您可以明確描述發送了哪些 XML,從而確保它正是您想要的。

2.2.3. 可重用性

在單獨的檔案中定義您的結構描述可讓您在不同的情境中重複使用該檔案。考慮在名為 airline.xsd 的檔案中定義 AirportCode

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

您可以透過使用 import 陳述式在其他結構描述或甚至是 WSDL 檔案中重複使用此定義。

2.2.4. 版本控制

即使契約必須盡可能長時間地保持不變,但有時確實需要變更。在 Java 中,這通常會產生一個新的 Java 介面,例如 AirlineService2,以及該介面的(新)實作。當然,舊服務必須保留下來,因為可能會有尚未遷移的用戶端。

如果使用 contract-first,我們可以使契約與實作之間的耦合更鬆散。這種更鬆散的耦合可讓我們在一個類別中實作兩個版本的契約。例如,我們可以使用 XSLT 樣式表將任何「舊樣式」訊息轉換為「新樣式」訊息。

3. 編寫 Contract-First Web Services

本教學課程說明如何編寫 contract-first Web 服務— 也就是說,如何開發從 XML 結構描述或 WSDL 契約開始,然後才是 Java 程式碼的 Web 服務。Spring-WS 專注於這種開發風格,本教學課程應有助於您入門。請注意,本教學課程的第一部分幾乎不包含任何 Spring-WS 特定資訊。它主要關於 XML、XSD 和 WSDL。第二部分重點介紹如何使用 Spring-WS 實作此契約。

在執行 contract-first Web 服務開發時,最重要的事情是以 XML 的角度思考。這表示 Java 語言概念的重要性較低。透過網路發送的是 XML,您應該專注於此。用於實作 Web 服務的 Java 是一個實作細節。

在本教學課程中,我們定義了一個由人力資源部門建立的 Web 服務。用戶端可以將假日請求表單發送到此服務以預訂假日。

3.1. 訊息

在本節中,我們重點介紹發送到 Web 服務和從 Web 服務發送的實際 XML 訊息。我們先確定這些訊息的外觀。

3.1.1. Holiday(假日)

在情境中,我們必須處理假日請求,因此確定假日在 XML 中的外觀是有意義的

<Holiday xmlns="http://mycompany.com/hr/schemas">
    <StartDate>2006-07-03</StartDate>
    <EndDate>2006-07-07</EndDate>
</Holiday>

假日由開始日期和結束日期組成。我們也決定對日期使用標準 ISO 8601 日期格式,因為這樣可以省去很多剖析麻煩。我們還為元素新增了命名空間,以確保我們的元素可以在其他 XML 文件中使用。

3.1.2. Employee(員工)

情境中也有員工的概念。以下是它在 XML 中的外觀

<Employee xmlns="http://mycompany.com/hr/schemas">
    <Number>42</Number>
    <FirstName>Arjen</FirstName>
    <LastName>Poutsma</LastName>
</Employee>

我們使用了與之前相同的命名空間。如果此 <Employee/> 元素可以在其他情境中使用,則使用不同的命名空間(例如 http://example.com/employees/schemas)可能更有意義。

3.1.3. HolidayRequest(假日請求)

holiday 元素和 employee 元素都可以放在 <HolidayRequest/>

<HolidayRequest xmlns="http://mycompany.com/hr/schemas">
    <Holiday>
        <StartDate>2006-07-03</StartDate>
        <EndDate>2006-07-07</EndDate>
    </Holiday>
    <Employee>
        <Number>42</Number>
        <FirstName>Arjen</FirstName>
        <LastName>Poutsma</LastName>
    </Employee>
</HolidayRequest>

兩個元素的順序無關緊要:<Employee/> 可能是第一個元素。重要的是所有資料都在那裡。事實上,資料是唯一重要的東西:我們採用資料驅動的方法。

3.2. 資料契約

現在我們已經看到了一些我們可以使用的 XML 資料範例,因此將其形式化為結構描述是有意義的。此資料契約定義了我們接受的訊息格式。有四種不同的方法可以為 XML 定義這樣的契約

DTD 的命名空間支援有限,因此不適用於 Web 服務。Relax NG 和 Schematron 比 XML 結構描述更容易。遺憾的是,它們在各平台上的支援度不高。因此,我們使用 XML 結構描述。

到目前為止,建立 XSD 最簡單的方法是從範例文件中推斷它。任何好的 XML 編輯器或 Java IDE 都提供此功能。基本上,這些工具使用一些範例 XML 文件來產生一個結構描述,該結構描述驗證所有這些文件。最終結果當然需要潤飾,但這是一個很好的起點。

使用先前描述的範例,我們最終得到以下產生的結構描述

<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
        elementFormDefault="qualified"
        targetNamespace="http://mycompany.com/hr/schemas"
        xmlns:hr="http://mycompany.com/hr/schemas">
    <xs:element name="HolidayRequest">
        <xs:complexType>
            <xs:sequence>
                <xs:element ref="hr:Holiday"/>
                <xs:element ref="hr:Employee"/>
            </xs:sequence>
        </xs:complexType>
    </xs:element>
    <xs:element name="Holiday">
        <xs:complexType>
            <xs:sequence>
                <xs:element ref="hr:StartDate"/>
                <xs:element ref="hr:EndDate"/>
            </xs:sequence>
        </xs:complexType>
    </xs:element>
    <xs:element name="StartDate" type="xs:NMTOKEN"/>
    <xs:element name="EndDate" type="xs:NMTOKEN"/>
    <xs:element name="Employee">
        <xs:complexType>
            <xs:sequence>
                <xs:element ref="hr:Number"/>
                <xs:element ref="hr:FirstName"/>
                <xs:element ref="hr:LastName"/>
            </xs:sequence>
        </xs:complexType>
    </xs:element>
    <xs:element name="Number" type="xs:integer"/>
    <xs:element name="FirstName" type="xs:NCName"/>
    <xs:element name="LastName" type="xs:NCName"/>
</xs:schema>

可以改進此產生的結構描述。首先要注意的是,每個類型都有一個根層級元素宣告。這表示 Web 服務應該能夠接受所有這些元素作為資料。這是不理想的:我們只想接受 <HolidayRequest/>。透過移除包裝元素標籤(從而保留類型)並內嵌結果,我們可以完成此操作,如下所示

<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
        xmlns:hr="http://mycompany.com/hr/schemas"
        elementFormDefault="qualified"
        targetNamespace="http://mycompany.com/hr/schemas">
    <xs:element name="HolidayRequest">
        <xs:complexType>
            <xs:sequence>
                <xs:element name="Holiday" type="hr:HolidayType"/>
                <xs:element name="Employee" type="hr:EmployeeType"/>
            </xs:sequence>
        </xs:complexType>
    </xs:element>
    <xs:complexType name="HolidayType">
        <xs:sequence>
            <xs:element name="StartDate" type="xs:NMTOKEN"/>
            <xs:element name="EndDate" type="xs:NMTOKEN"/>
        </xs:sequence>
    </xs:complexType>
    <xs:complexType name="EmployeeType">
        <xs:sequence>
            <xs:element name="Number" type="xs:integer"/>
            <xs:element name="FirstName" type="xs:NCName"/>
            <xs:element name="LastName" type="xs:NCName"/>
        </xs:sequence>
    </xs:complexType>
</xs:schema>

結構描述仍然存在一個問題:使用這樣的結構描述,您可以預期以下訊息可以驗證

<HolidayRequest xmlns="http://mycompany.com/hr/schemas">
    <Holiday>
        <StartDate>this is not a date</StartDate>
        <EndDate>neither is this</EndDate>
    </Holiday>
    PlainText Section qName:lineannotation level:4, chunks:[<, !-- ... --, >] attrs:[:]
</HolidayRequest>

顯然,我們必須確保開始日期和結束日期確實是日期。XML 結構描述有一個出色的內建 date 類型,我們可以使用它。我們也將 NCName 變更為 string 實例。最後,我們將 <HolidayRequest/> 中的 sequence 變更為 all。這告訴 XML 剖析器 <Holiday/><Employee/> 的順序並不重要。我們最終的 XSD 現在看起來像以下清單

<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
        xmlns:hr="http://mycompany.com/hr/schemas"
        elementFormDefault="qualified"
        targetNamespace="http://mycompany.com/hr/schemas">
    <xs:element name="HolidayRequest">
        <xs:complexType>
            <xs:all>
                <xs:element name="Holiday" type="hr:HolidayType"/> (1)
                <xs:element name="Employee" type="hr:EmployeeType"/> (1)
            </xs:all>
        </xs:complexType>
    </xs:element>
    <xs:complexType name="HolidayType">
        <xs:sequence>
            <xs:element name="StartDate" type="xs:date"/> (2)
            <xs:element name="EndDate" type="xs:date"/> (2)
        </xs:sequence>
    </xs:complexType>
    <xs:complexType name="EmployeeType">
        <xs:sequence>
            <xs:element name="Number" type="xs:integer"/>
            <xs:element name="FirstName" type="xs:string"/> (3)
            <xs:element name="LastName" type="xs:string"/> (3)
        </xs:sequence>
    </xs:complexType>
</xs:schema>
1 all 告訴 XML 剖析器 <Holiday/><Employee/> 的順序並不重要。
2 我們對 <StartDate/><EndDate/> 使用 xs:date 資料類型(由年、月和日組成)。
3 xs:string 用於名字和姓氏。

我們將此檔案儲存為 hr.xsd

3.3. 服務契約

服務契約通常表示為 WSDL 檔案。請注意,在 Spring-WS 中,不需要手動編寫 WSDL。根據 XSD 和一些慣例,Spring-WS 可以為您建立 WSDL,如標題為 實作端點 的章節中所述。本節的其餘部分說明如何手動編寫 WSDL。您可能想要跳到下一節

我們從標準前言開始我們的 WSDL,並匯入我們現有的 XSD。為了將結構描述與定義分開,我們為 WSDL 定義使用單獨的命名空間:http://mycompany.com/hr/definitions。以下清單顯示了前言

<wsdl:definitions xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
                  xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
                  xmlns:schema="http://mycompany.com/hr/schemas"
                  xmlns:tns="http://mycompany.com/hr/definitions"
                  targetNamespace="http://mycompany.com/hr/definitions">
    <wsdl:types>
        <xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema">
            <xsd:import namespace="http://mycompany.com/hr/schemas" schemaLocation="hr.xsd"/>
        </xsd:schema>
    </wsdl:types>

接下來,我們根據編寫的結構描述類型新增我們的訊息。我們只有一個訊息,即我們放入結構描述中的 <HolidayRequest/>

    <wsdl:message name="HolidayRequest">
        <wsdl:part element="schema:HolidayRequest" name="HolidayRequest"/>
    </wsdl:message>

我們將訊息作為操作新增到連接埠類型

    <wsdl:portType name="HumanResource">
        <wsdl:operation name="Holiday">
            <wsdl:input message="tns:HolidayRequest" name="HolidayRequest"/>
        </wsdl:operation>
    </wsdl:portType>

該訊息完成了 WSDL 的抽象部分(介面,可以這麼說),並留下了具體部分。具體部分由 binding(告訴用戶端如何調用您剛定義的操作)和 service(告訴用戶端在哪裡調用它)組成。

新增具體部分非常標準。為此,請參考您先前定義的抽象部分,確保您對 soap:binding 元素使用 document/literal (rpc/encoded 已棄用),為操作選擇 soapAction(在本例中為 http://mycompany.com/RequestHoliday,但任何 URI 都可以),並確定您希望請求到達的 location URL(在本例中為 http://mycompany.com/humanresources

<wsdl:definitions xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
                  xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
                  xmlns:schema="http://mycompany.com/hr/schemas"
                  xmlns:tns="http://mycompany.com/hr/definitions"
                  targetNamespace="http://mycompany.com/hr/definitions">
    <wsdl:types>
        <xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema">
            <xsd:import namespace="http://mycompany.com/hr/schemas"              (1)
                schemaLocation="hr.xsd"/>
        </xsd:schema>
    </wsdl:types>
    <wsdl:message name="HolidayRequest">                                         (2)
        <wsdl:part element="schema:HolidayRequest" name="HolidayRequest"/>       (3)
    </wsdl:message>
    <wsdl:portType name="HumanResource">                                         (4)
        <wsdl:operation name="Holiday">
            <wsdl:input message="tns:HolidayRequest" name="HolidayRequest"/>     (2)
        </wsdl:operation>
    </wsdl:portType>
    <wsdl:binding name="HumanResourceBinding" type="tns:HumanResource">          (4)(5)
        <soap:binding style="document"                                           (6)
            transport="http://schemas.xmlsoap.org/soap/http"/>                   (7)
        <wsdl:operation name="Holiday">
            <soap:operation soapAction="http://mycompany.com/RequestHoliday"/>   (8)
            <wsdl:input name="HolidayRequest">
                <soap:body use="literal"/>                                       (6)
            </wsdl:input>
        </wsdl:operation>
    </wsdl:binding>
    <wsdl:service name="HumanResourceService">
        <wsdl:port binding="tns:HumanResourceBinding" name="HumanResourcePort">  (5)
            <soap:address location="http://localhost:8080/holidayService/"/>     (9)
        </wsdl:port>
    </wsdl:service>
</wsdl:definitions>
1 我們匯入 資料契約 中定義的結構描述。
2 我們定義了 HolidayRequest 訊息,該訊息在 portType 中使用。
3 HolidayRequest 類型在結構描述中定義。
4 我們定義了 HumanResource 連接埠類型,該類型在 binding 中使用。
5 我們定義了 HumanResourceBinding 繫結,該繫結在 port 中使用。
6 我們使用 document/literal 風格。
7 文字 http://schemas.xmlsoap.org/soap/http 表示 HTTP 傳輸。
8 soapAction 屬性表示將與每個請求一起發送的 SOAPAction HTTP 標頭。
9 http://localhost:8080/holidayService/ 地址是可以在其中調用 Web 服務的 URL。

前面的清單顯示了最終的 WSDL。我們在下一節中描述如何實作產生的結構描述和 WSDL。

3.4. 建立專案

在本節中,我們使用 Maven 為我們建立初始專案結構。這樣做不是必需的,但可以大大減少我們必須編寫的程式碼量來設定我們的 HolidayService。

以下命令透過使用 Spring-WS archetype(也就是專案範本)為我們建立 Maven Web 應用程式專案

mvn archetype:create -DarchetypeGroupId=org.springframework.ws \
  -DarchetypeArtifactId=spring-ws-archetype \
  -DarchetypeVersion= \
  -DgroupId=com.mycompany.hr \
  -DartifactId=holidayService

前述命令會建立一個名為 holidayService 的新目錄。在此目錄中,有一個 src/main/webapp 目錄,其中包含 WAR 檔案的根目錄。您可以在此處找到標準的 Web 應用程式部署描述符 ('WEB-INF/web.xml'),它定義了一個 Spring-WS MessageDispatcherServlet 並將所有傳入的請求映射到這個 servlet。

<web-app xmlns="http://java.sun.com/xml/ns/j2ee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee
             http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"
         version="2.4">

    <display-name>MyCompany HR Holiday Service</display-name>

    <!-- take special notice of the name of this servlet -->
    <servlet>
        <servlet-name>spring-ws</servlet-name>
        <servlet-class>org.springframework.ws.transport.http.MessageDispatcherServlet</servlet-class>
    </servlet>

    <servlet-mapping>
        <servlet-name>spring-ws</servlet-name>
        <url-pattern>/*</url-pattern>
    </servlet-mapping>

</web-app>

除了前述的 WEB-INF/web.xml 檔案之外,您還需要另一個 Spring-WS 特定的組態檔,名為 WEB-INF/spring-ws-servlet.xml。這個檔案包含所有 Spring-WS 特定的 bean,例如 EndPointsWebServiceMessageReceivers,並用於建立新的 Spring 容器。這個檔案的名稱衍生自相關 servlet 的名稱 (在本例中為 'spring-ws'),並附加了 -servlet.xml。因此,如果您定義一個名為 'dynamite'MessageDispatcherServlet,則 Spring-WS 特定的組態檔名稱將變為 WEB-INF/dynamite-servlet.xml

(您可以在 [tutorial.example.sws-conf-file] 中查看此範例的 WEB-INF/spring-ws-servlet.xml 檔案內容。)

一旦您建立了專案結構,就可以將上一節中的 schema 和 WSDL 放入 'WEB-INF/' 資料夾中。

3.5. 實作端點

在 Spring-WS 中,您實作端點來處理傳入的 XML 訊息。端點通常透過使用 @Endpoint 註解註釋類別來建立。在這個端點類別中,您可以建立一個或多個方法來處理傳入的請求。方法簽章可以非常靈活。您可以包含幾乎任何與傳入 XML 訊息相關的參數類型,我們將在本章稍後解釋。

3.5.1. 處理 XML 訊息

在這個範例應用程式中,我們使用 JDom 2 來處理 XML 訊息。我們也使用 XPath,因為它可以讓我們選擇 XML JDOM 樹的特定部分,而無需嚴格的 schema 一致性。

以下清單顯示了定義我們 holiday 端點的類別

package com.mycompany.hr.ws;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.ws.server.endpoint.annotation.Endpoint;
import org.springframework.ws.server.endpoint.annotation.PayloadRoot;
import org.springframework.ws.server.endpoint.annotation.RequestPayload;

import com.mycompany.hr.service.HumanResourceService;
import org.jdom2.Element;
import org.jdom2.JDOMException;
import org.jdom2.Namespace;
import org.jdom2.filter.Filters;
import org.jdom2.xpath.XPathExpression;
import org.jdom2.xpath.XPathFactory;

@Endpoint                                                                                     (1)
public class HolidayEndpoint {

    private static final String NAMESPACE_URI = "http://mycompany.com/hr/schemas";

    private XPathExpression<Element> startDateExpression;

    private XPathExpression<Element> endDateExpression;

    private XPathExpression<Element> firstNameExpression;

    private XPathExpression<Element> lastNameExpression;

    private HumanResourceService humanResourceService;

    @Autowired                                                                                (2)
    public HolidayEndpoint(HumanResourceService humanResourceService) throws JDOMException {
        this.humanResourceService = humanResourceService;

        Namespace namespace = Namespace.getNamespace("hr", NAMESPACE_URI);
        XPathFactory xPathFactory = XPathFactory.instance();
        startDateExpression = xPathFactory.compile("//hr:StartDate", Filters.element(), null, namespace);
        endDateExpression = xPathFactory.compile("//hr:EndDate", Filters.element(), null, namespace);
        firstNameExpression = xPathFactory.compile("//hr:FirstName", Filters.element(), null, namespace);
        lastNameExpression = xPathFactory.compile("//hr:LastName", Filters.element(), null, namespace);
    }

    @PayloadRoot(namespace = NAMESPACE_URI, localPart = "HolidayRequest")                      (3)
    public void handleHolidayRequest(@RequestPayload Element holidayRequest) throws Exception {(4)
        Date startDate = parseDate(startDateExpression, holidayRequest);
        Date endDate = parseDate(endDateExpression, holidayRequest);
        String name = firstNameExpression.evaluateFirst(holidayRequest).getText() + " " + lastNameExpression.evaluateFirst(holidayRequest).getText();

        humanResourceService.bookHoliday(startDate, endDate, name);
    }

    private Date parseDate(XPathExpression<Element> expression, Element element) throws ParseException {
        Element result = expression.evaluateFirst(element);
        if (result != null) {
            SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
            return dateFormat.parse(result.getText());
        } else {
            throw new IllegalArgumentException("Could not evaluate [" + expression + "] on [" + element + "]");
        }
    }

}
1 HolidayEndpoint 使用 @Endpoint 註解進行註釋。這將類別標記為一種特殊的 @Component,適用於在 Spring-WS 中處理 XML 訊息,並且也使其有資格進行組件掃描。
2 HolidayEndpoint 需要 HumanResourceService 業務服務才能運作,因此我們在建構子中注入依賴項,並使用 @Autowired 進行註釋。接下來,我們使用 JDOM2 API 設定 XPath 表達式。共有四個表達式://hr:StartDate 用於提取 <StartDate> 文字值,//hr:EndDate 用於提取結束日期,以及兩個用於提取員工姓名。
3 @PayloadRoot 註解告訴 Spring-WS,handleHolidayRequest 方法適用於處理 XML 訊息。此方法可以處理的訊息類型由註解值指示。在本例中,它可以處理具有 HolidayRequest 本地部分和 http://mycompany.com/hr/schemas 命名空間的 XML 元素。關於將訊息映射到端點的更多資訊,請在下一節中提供。
4 handleHolidayRequest(..) 方法是主要處理方法,它從傳入的 XML 訊息中傳遞 <HolidayRequest/> 元素。@RequestPayload 註解指示應將 holidayRequest 參數映射到請求訊息的 payload。我們使用 XPath 表達式從 XML 訊息中提取字串值,並使用 SimpleDateFormat (parseData 方法) 將這些值轉換為 Date 物件。有了這些值,我們就可以調用業務服務上的方法。通常,這會導致啟動資料庫交易,並且資料庫中的某些記錄會被更改。最後,我們定義了一個 void 返回類型,這表示我們不希望發送回應訊息給 Spring-WS。如果我們想要回應訊息,我們可以返回一個 JDOM Element 來表示回應訊息的 payload。

使用 JDOM 只是處理 XML 的選項之一。其他選項包括 DOM、dom4j、XOM、SAX 和 StAX,以及封送處理技術,如 JAXB、Castor、XMLBeans、JiBX 和 XStream,如 下一章 中所述。我們選擇 JDOM 是因為它可以讓我們存取原始 XML,並且因為它是基於類別 (而不是介面和工廠方法,如同 W3C DOM 和 dom4j),這使得程式碼不那麼冗長。我們使用 XPath 是因為它比封送處理技術更不易出錯。只要我們可以找到日期和名稱,我們就不需要嚴格的 schema 一致性。

因為我們使用 JDOM,所以我們必須將一些依賴項添加到 Maven pom.xml 中,它位於我們的專案根目錄中。以下是 POM 的相關部分

<dependencies>
    <dependency>
        <groupId>org.springframework.ws</groupId>
        <artifactId>spring-ws-core</artifactId>
        <version></version>
    </dependency>
    <dependency>
        <groupId>jdom</groupId>
        <artifactId>jdom</artifactId>
        <version>2.0.1</version>
    </dependency>
    <dependency>
        <groupId>jaxen</groupId>
        <artifactId>jaxen</artifactId>
        <version>1.1</version>
    </dependency>
</dependencies>

以下是如何在我們的 spring-ws-servlet.xml Spring XML 組態檔中使用組件掃描來組態這些類別。我們也指示 Spring-WS 使用註解驅動的端點,使用 <sws:annotation-driven> 元素。

<beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:context="http://www.springframework.org/schema/context"
  xmlns:sws="http://www.springframework.org/schema/web-services"
  xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
  http://www.springframework.org/schema/web-services http://www.springframework.org/schema/web-services/web-services-2.0.xsd
  http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd">

  <context:component-scan base-package="com.mycompany.hr"/>

  <sws:annotation-driven/>

</beans>

3.5.2. 將訊息路由到端點

作為編寫端點的一部分,我們也使用了 @PayloadRoot 註解來指示 handleHolidayRequest 方法可以處理哪種類型的訊息。在 Spring-WS 中,這個過程是 EndpointMapping 的責任。在這裡,我們透過使用 PayloadRootAnnotationMethodEndpointMapping 根據訊息內容路由訊息。以下清單顯示了我們稍早使用的註解

@PayloadRoot(namespace = "http://mycompany.com/hr/schemas", localPart = "HolidayRequest")

前述範例中顯示的註解基本上表示,每當收到命名空間為 http://mycompany.com/hr/schemas 且本地名稱為 HolidayRequest 的 XML 訊息時,它都會被路由到 handleHolidayRequest 方法。透過在我們的組態中使用 <sws:annotation-driven> 元素,我們啟用了 @PayloadRoot 註解的偵測。在一個端點中擁有處理多個相關訊息的處理方法是可能的 (且非常常見),每個方法處理不同的 XML 訊息。

還有其他將端點映射到 XML 訊息的方法,這在 下一章 中描述。

3.5.3. 提供服務和 Stub 實作

現在我們有了端點,我們需要 HumanResourceService 及其實作供 HolidayEndpoint 使用。以下清單顯示了 HumanResourceService 介面

package com.mycompany.hr.service;

import java.util.Date;

public interface HumanResourceService {
    void bookHoliday(Date startDate, Date endDate, String name);
}

為了教學目的,我們使用 HumanResourceService 的簡單 stub 實作

package com.mycompany.hr.service;

import java.util.Date;

import org.springframework.stereotype.Service;

@Service                                                                 (1)
public class StubHumanResourceService implements HumanResourceService {
    public void bookHoliday(Date startDate, Date endDate, String name) {
        System.out.println("Booking holiday for [" + startDate + "-" + endDate + "] for [" + name + "] ");
    }
}
1 StubHumanResourceService 使用 @Service 進行註解。這將類別標記為業務 facade,使其成為 HolidayEndpoint@Autowired 進行注入的候選者。

3.6. 發布 WSDL

最後,我們需要發布 WSDL。如 服務合約 中所述,我們不需要自己編寫 WSDL。Spring-WS 可以根據一些慣例產生一個。以下是如何定義產生

<sws:dynamic-wsdl id="holiday"                                (1)
    portTypeName="HumanResource"                              (3)
    locationUri="/holidayService/"                            (4)
    targetNamespace="http://mycompany.com/hr/definitions">    (5)
  <sws:xsd location="/WEB-INF/hr.xsd"/>                       (2)
</sws:dynamic-wsdl>
1 id 決定了可以檢索 WSDL 的 URL。在本例中,idholiday,這表示可以在 servlet context 中以 holiday.wsdl 檢索 WSDL。完整 URL 是 http://localhost:8080/holidayService/holiday.wsdl
2 接下來,我們將 WSDL port type 設定為 HumanResource
3 我們設定可以到達服務的位置:/holidayService/。我們使用相對 URI,並指示框架將其動態轉換為絕對 URI。因此,如果服務部署到不同的 context,我們不必手動更改 URI。有關更多資訊,請參閱 標題為「自動 WSDL 暴露」的章節。為了使位置轉換正常運作,我們需要在 web.xml (如下一個清單所示) 中的 spring-ws servlet 中新增 init parameter。
4 我們為 WSDL 定義本身設定目標命名空間。設定此屬性不是必需的。如果未設定,WSDL 將具有與 XSD schema 相同的命名空間。
5 xsd 元素引用了我們在 資料合約 中定義的人力資源 schema。我們將 schema 放置在應用程式的 WEB-INF 目錄中。

以下清單顯示了如何新增 init parameter

<init-param>
  <param-name>transformWsdlLocations</param-name>
  <param-value>true</param-value>
</init-param>

您可以使用 mvn install 建立 WAR 檔案。如果您部署應用程式 (到 Tomcat、Jetty 等),並將瀏覽器指向 這個位置,您將看到產生的 WSDL。此 WSDL 已準備好供客戶端使用,例如 soapUI 或其他 SOAP 框架。

本教學到此結束。教學程式碼可以在 Spring-WS 的完整發行版中找到。如果您希望繼續,請查看發行版中包含的 echo 範例應用程式。之後,查看 airline 範例,它稍微複雜一些,因為它使用了 JAXB、WS-Security、Hibernate 和交易式服務層。最後,您可以閱讀其餘的參考文件。

II. 參考

參考文件的這部分詳細說明了組成 Spring Web Services 的各種組件。這包括 一個章節,討論了客戶端和伺服器端 WS 共有的部分,一個專門介紹 編寫伺服器端 Web 服務 的章節,一個關於在 客戶端 使用 Web 服務的章節,以及一個關於使用 WS-Security 的章節。

4. 共用組件

本章探討了客戶端和伺服器端 Spring-WS 開發之間共用的組件。這些介面和類別代表了 Spring-WS 的建構塊,因此您需要了解它們的作用,即使您沒有直接使用它們。

4.1. Web 服務訊息

本節描述 Spring-WS 使用的訊息和訊息工廠。

4.1.1. WebServiceMessage

Spring Web Services 的核心介面之一是 WebServiceMessage。這個介面代表一個與協定無關的 XML 訊息。該介面包含提供對訊息 payload 的存取權限的方法,形式為 javax.xml.transform.Sourcejavax.xml.transform.ResultSourceResult 是標記介面,代表 XML 輸入和輸出的抽象概念。具體實作封裝了各種 XML 表示形式,如下表所示

Source 或 Result 實作 封裝的 XML 表示形式

javax.xml.transform.dom.DOMSource

org.w3c.dom.Node

javax.xml.transform.dom.DOMResult

org.w3c.dom.Node

javax.xml.transform.sax.SAXSource

org.xml.sax.InputSourceorg.xml.sax.XMLReader

javax.xml.transform.sax.SAXResult

org.xml.sax.ContentHandler

javax.xml.transform.stream.StreamSource

java.io.Filejava.io.InputStreamjava.io.Reader

javax.xml.transform.stream.StreamResult

java.io.Filejava.io.OutputStreamjava.io.Writer

除了從 payload 讀取和寫入之外,Web 服務訊息還可以將自身寫入輸出串流。

4.1.2. SoapMessage

SoapMessageWebServiceMessage 的子類別。它包含 SOAP 特定的方法,例如取得 SOAP 標頭、SOAP 錯誤等等。一般來說,您的程式碼不應依賴 SoapMessage,因為 SOAP Body 的內容 (訊息的 payload) 可以透過在 WebServiceMessage 中使用 getPayloadSource()getPayloadResult() 來取得。只有在需要執行 SOAP 特定動作 (例如新增標頭、取得附件等等) 時,您才需要將 WebServiceMessage 轉換為 SoapMessage

4.1.3. 訊息工廠

具體訊息實作由 WebServiceMessageFactory 建立。這個工廠可以建立一個空訊息或從輸入串流讀取訊息。WebServiceMessageFactory 有兩個具體實作。一個是基於 SAAJ,Java 的 SOAP with Attachments API。另一個是基於 Axis 2 的 AXIOM (AXis 物件模型)。

SaajSoapMessageFactory

SaajSoapMessageFactory 使用 Java 的 SOAP with Attachments API (SAAJ) 來建立 SoapMessage 實作。SAAJ 是 J2EE 1.4 的一部分,因此大多數現代應用程式伺服器都應支援它。以下是常見應用程式伺服器提供的 SAAJ 版本概覽

應用程式伺服器 SAAJ 版本

BEA WebLogic 8

1.1

BEA WebLogic 9

1.1/1.21

IBM WebSphere 6

1.2

SUN Glassfish 1

1.3

1Weblogic 9 在 SAAJ 1.2 實作中存在已知錯誤:它實作了所有 1.2 介面,但在調用時拋出 UnsupportedOperationException。Spring Web Services 有一個解決方案:在 WebLogic 9 上運作時,它使用 SAAJ 1.1。

此外,Java SE 6 包含 SAAJ 1.3。您可以如下所示連接 SaajSoapMessageFactory

<bean id="messageFactory" class="org.springframework.ws.soap.saaj.SaajSoapMessageFactory" />
SAAJ 基於 DOM,文件物件模型。這表示所有 SOAP 訊息都儲存在記憶體中。對於較大的 SOAP 訊息,這可能效能不佳。在這種情況下,AxiomSoapMessageFactory 可能更適用。
AxiomSoapMessageFactory

AxiomSoapMessageFactory 使用 AXis 2 物件模型 (AXIOM) 來建立 SoapMessage 實作。AXIOM 基於 StAX,XML 的串流 API。StAX 提供了一種基於拉動的機制來讀取 XML 訊息,對於較大的訊息來說,這可能更有效率。

為了提高 AxiomSoapMessageFactory 的讀取效能,您可以將 payloadCaching 屬性設定為 false (預設為 true)。這樣做會導致 SOAP Body 的內容直接從 socket 串流讀取。啟用此設定後,payload 只能讀取一次。這表示您必須確保訊息的任何預處理 (記錄或其他工作) 不會消耗它。

您可以如下所示使用 AxiomSoapMessageFactory

<bean id="messageFactory" class="org.springframework.ws.soap.axiom.AxiomSoapMessageFactory">
    <property name="payloadCaching" value="true"/>
</bean>

除了 payload 快取之外,AXIOM 還支援完整串流訊息,如 StreamingWebServiceMessage 中所定義。這表示您可以直接在回應訊息上設定 payload,而不是將其寫入 DOM 樹或緩衝區。

當處理程式方法返回 JAXB2 支援的物件時,會使用 AXIOM 的完整串流。它會自動將此封送處理的物件設定到回應訊息中,並在回應傳出時將其寫出到傳出 socket 串流。

有關完整串流的更多資訊,請參閱 StreamingWebServiceMessageStreamingPayload 的類別級 Javadoc。

SOAP 1.1 或 1.2

SaajSoapMessageFactoryAxiomSoapMessageFactory 都具有 soapVersion 屬性,您可以在其中注入 SoapVersion 常數。預設情況下,版本為 1.1,但您可以將其設定為 1.2

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:util="http://www.springframework.org/schema/util"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans-2.0.xsd
       http://www.springframework.org/schema/util
       http://www.springframework.org/schema/util/spring-util-2.0.xsd">

    <bean id="messageFactory" class="org.springframework.ws.soap.saaj.SaajSoapMessageFactory">
        <property name="soapVersion">
            <util:constant static-field="org.springframework.ws.soap.SoapVersion.SOAP_12"/>
        </property>
    </bean>

</beans>

在前面的範例中,我們定義了一個僅接受 SOAP 1.2 訊息的 SaajSoapMessageFactory

即使 SOAP 的兩個版本在格式上非常相似,但 1.2 版本與 1.1 版本不向後相容,因為它使用了不同的 XML 命名空間。SOAP 1.1 和 1.2 之間的其他主要差異包括錯誤的不同結構,以及 SOAPAction HTTP 標頭實際上已被棄用,儘管它們仍然有效。

關於 SOAP 版本號碼 (或一般 WS-* 規範版本號碼) 需要注意的一個重要事項是,規範的最新版本通常不是最流行的版本。對於 SOAP 而言,這表示 (目前) 最好的版本是 1.1。版本 1.2 將來可能會更流行,但 1.1 目前是最安全的選擇。

4.1.4. MessageContext

通常,訊息成對出現:請求和回應。請求在客戶端建立,透過某種傳輸方式發送到伺服器端,伺服器端會產生回應。此回應會發送回客戶端,並在客戶端讀取。

在 Spring Web Services 中,這種對話包含在 MessageContext 中,它具有取得請求和回應訊息的屬性。在客戶端,訊息 context 由 WebServiceTemplate 建立。在伺服器端,訊息 context 從傳輸特定的輸入串流讀取。例如,在 HTTP 中,它從 HttpServletRequest 讀取,而回應寫回 HttpServletResponse

4.2. TransportContext

SOAP 協定的關鍵屬性之一是它試圖與傳輸方式無關。這就是為什麼,例如,Spring-WS 不支援透過 HTTP 請求 URL 將訊息映射到端點,而是透過訊息內容進行映射。

但是,有時需要存取底層傳輸,無論是在客戶端還是伺服器端。為此,Spring Web Services 具有 TransportContext。傳輸 context 允許存取底層 WebServiceConnection,它通常是伺服器端的 HttpServletConnection 或客戶端的 HttpUrlConnectionCommonsHttpConnection。例如,您可以在伺服器端端點或攔截器中取得目前請求的 IP 位址

TransportContext context = TransportContextHolder.getTransportContext();
HttpServletConnection connection = (HttpServletConnection )context.getConnection();
HttpServletRequest request = connection.getHttpServletRequest();
String ipAddress = request.getRemoteAddr();

4.3. 使用 XPath 處理 XML

處理 XML 的最佳方法之一是使用 XPath。引用 [effective-xml],第 35 項

XPath 是一種第四代宣告式語言,可讓您指定要處理的節點,而無需明確指定處理器應如何導航到這些節點。XPath 的資料模型設計得非常好,可以完全支援幾乎所有開發人員對 XML 的期望。例如,它合併所有相鄰文字,包括 CDATA 區段中的文字,允許計算跳過註解和處理指令的值,並包含來自子元素和後代元素的文字,並要求解析所有外部實體參考。在實務中,XPath 表達式往往比輸入文件中意外但可能不重要的變更更穩健。
— Elliotte Rusty Harold

Spring Web Services 有兩種在應用程式中使用 XPath 的方式:更快的 XPathExpression 或更靈活的 XPathTemplate

4.3.1. XPathExpression

XPathExpression 是編譯後的 XPath 表達式的抽象概念,例如 Java 5 javax.xml.xpath.XPathExpression 介面或 Jaxen XPath 類別。若要在應用程式 context 中建構表達式,您可以使用 XPathExpressionFactoryBean。以下範例使用這個工廠 bean

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
          http://www.springframework.org/schema/beans/spring-beans-2.0.xsd">

    <bean id="nameExpression" class="org.springframework.xml.xpath.XPathExpressionFactoryBean">
        <property name="expression" value="/Contacts/Contact/Name"/>
    </bean>

    <bean id="myEndpoint" class="sample.MyXPathClass">
        <constructor-arg ref="nameExpression"/>
    </bean>

</beans>

前面的表達式未使用命名空間,但我們可以透過使用工廠 bean 的 namespaces 屬性來設定這些命名空間。表達式可以在程式碼中如下所示使用

package sample;

public class MyXPathClass {

    private final XPathExpression nameExpression;

    public MyXPathClass(XPathExpression nameExpression) {
        this.nameExpression = nameExpression;
    }

    public void doXPath(Document document) {
        String name = nameExpression.evaluateAsString(document.getDocumentElement());
        System.out.println("Name: " + name);
    }

}

為了更靈活的方法,您可以使用 NodeMapper,它類似於 Spring JDBC 支援中的 RowMapper。以下範例顯示如何使用它

package sample;

public class MyXPathClass  {

   private final XPathExpression contactExpression;

   public MyXPathClass(XPathExpression contactExpression) {
      this.contactExpression = contactExpression;
   }

   public void doXPath(Document document) {
      List contacts = contactExpression.evaluate(document,
        new NodeMapper() {
           public Object mapNode(Node node, int nodeNum) throws DOMException {
              Element contactElement = (Element) node;
              Element nameElement = (Element) contactElement.getElementsByTagName("Name").item(0);
              Element phoneElement = (Element) contactElement.getElementsByTagName("Phone").item(0);
              return new Contact(nameElement.getTextContent(), phoneElement.getTextContent());
           }
        });
      PlainText Section qName; // do something with the list of Contact objects
   }
}

與在 Spring JDBC 的 RowMapper 中映射列類似,每個結果節點都使用匿名內部類別進行映射。在本例中,我們建立了一個 Contact 物件,我們稍後會使用它。

4.3.2. XPathTemplate

XPathExpression 僅允許您評估單一的預編譯表達式。更靈活但速度較慢的替代方案是 XPathTemplate。這個類別遵循 Spring 中常用的範本模式 (JdbcTemplateJmsTemplate 和其他)。以下清單顯示了一個範例

package sample;

public class MyXPathClass {

    private XPathOperations template = new Jaxp13XPathTemplate();

    public void doXPath(Source source) {
        String name = template.evaluateAsString("/Contacts/Contact/Name", request);
        // do something with name
    }

}

4.4. 訊息記錄和追蹤

在開發或偵錯 Web 服務時,查看 (SOAP) 訊息在到達時或發送之前的內容可能非常有用。Spring Web Services 透過標準 Commons Logging 介面提供此功能。

請務必使用 Commons Logging 1.1 或更高版本。較早版本存在類別載入問題,並且不與 Log4J TRACE 層級整合。

若要記錄所有伺服器端訊息,請將 org.springframework.ws.server.MessageTracing 記錄器層級設定為 DEBUGTRACE。在 DEBUG 層級,僅記錄 payload 根元素。在 TRACE 層級,記錄整個訊息內容。如果您只想記錄已發送的訊息,請使用 org.springframework.ws.server.MessageTracing.sent 記錄器。同樣地,您可以使用 org.springframework.ws.server.MessageTracing.received 僅記錄接收到的訊息。

在客戶端,存在類似的記錄器:org.springframework.ws.client.MessageTracing.sentorg.springframework.ws.client.MessageTracing.received

以下 log4j.properties 組態檔範例記錄了客戶端上已發送訊息的完整內容,以及客戶端接收訊息的 payload 根元素。在伺服器端,payload 根元素會針對已發送和接收的訊息進行記錄

log4j.rootCategory=INFO, stdout
log4j.logger.org.springframework.ws.client.MessageTracing.sent=TRACE
log4j.logger.org.springframework.ws.client.MessageTracing.received=DEBUG

log4j.logger.org.springframework.ws.server.MessageTracing=DEBUG

log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%p [%c{3}] %m%n

使用此組態,典型的輸出為

TRACE [client.MessageTracing.sent] Sent request [<SOAP-ENV:Envelope xmlns:SOAP-ENV="...
DEBUG [server.MessageTracing.received] Received request [SaajSoapMessage {http://example.com}request] ...
DEBUG [server.MessageTracing.sent] Sent response [SaajSoapMessage {http://example.com}response] ...
DEBUG [client.MessageTracing.received] Received response [SaajSoapMessage {http://example.com}response] ...

5. 使用 Spring-WS 建立 Web 服務

Spring-WS 的伺服器端支援圍繞 MessageDispatcher 設計,它將傳入的訊息分派到端點,具有可組態的端點映射、回應產生和端點攔截。端點通常使用 @Endpoint 註解進行註釋,並具有一個或多個處理方法。這些方法透過檢查訊息的部分 (通常是 payload) 來處理傳入的 XML 請求訊息,並建立某種形式的回應。您可以使用另一個註解來註釋方法,通常是 @PayloadRoot,以指示它可以處理哪種類型的訊息。

Spring-WS 的 XML 處理非常靈活。端點可以從 Spring-WS 支援的大量 XML 處理程式庫中進行選擇,包括

  • DOM 系列:W3C DOM、JDOM、dom4j 和 XOM

  • SAX 或 StAX:為了更快的效能

  • XPath:從訊息中提取資訊

  • 封送處理技術 (JAXB、Castor、XMLBeans、JiBX 或 XStream):將 XML 轉換為物件,反之亦然

5.1. MessageDispatcher

Spring-WS 的伺服器端圍繞一個中央類別設計,該類別將傳入的 XML 訊息分派到端點。Spring-WS 的 MessageDispatcher 非常靈活,可讓您將任何類型的類別用作端點,只要可以在 Spring IoC 容器中組態它即可。在某種程度上,訊息分派器類似於 Spring 的 DispatcherServlet,Spring Web MVC 中使用的「前端控制器」。

以下循序圖顯示了 MessageDispatcher 的處理和分派流程

sequence

當設定 MessageDispatcher 以供使用,並且特定分派器的請求進入時,MessageDispatcher 開始處理請求。以下流程描述了 MessageDispatcher 如何處理請求

  1. 搜尋已組態的 EndpointMapping(s) 以尋找適當的端點。如果找到端點,則會調用與端點關聯的調用鏈 (前處理器、後處理器和端點) 以建立回應。

  2. 為端點找到適當的 adapter。MessageDispatcher 委派給這個 adapter 以調用端點。

  3. 如果返回回應,則會發送出去。如果沒有返回回應 (這可能是由於前處理器或後處理器攔截了請求,例如,基於安全原因),則不會發送回應。

在處理請求期間拋出的例外會被應用程式 context 中宣告的任何端點例外解析器拾取。使用這些例外解析器可讓您定義自訂行為 (例如,返回 SOAP 錯誤),以防拋出此類例外。

MessageDispatcher 具有多個屬性,用於設定端點 adapter、映射例外解析器。但是,設定這些屬性不是必需的,因為分派器會自動偵測應用程式 context 中註冊的所有類型。只有在需要覆寫偵測時,才應設定這些屬性。

訊息分派器在 訊息 context 上運作,而不是在傳輸特定的輸入串流和輸出串流上運作。因此,傳輸特定的請求需要讀取到 MessageContext 中。對於 HTTP,這是透過 WebServiceMessageReceiverHandlerAdapter (它是 Spring Web HandlerInterceptor) 完成的,以便可以在標準 DispatcherServlet 中連接 MessageDispatcher。但是,有一種更方便的方法可以做到這一點,如 MessageDispatcherServlet 中所示。

5.2. 傳輸方式

Spring Web Services 支援多種傳輸協定。最常見的是 HTTP 傳輸,為此提供了一個自訂 servlet,但您也可以透過 JMS 甚至電子郵件發送訊息。

5.2.1. MessageDispatcherServlet

MessageDispatcherServlet 是一個標準 Servlet,它方便地從標準 Spring Web DispatcherServlet 擴展,並封裝了 MessageDispatcher。因此,它將這些屬性組合為一個。作為 MessageDispatcher,它遵循與上一節中描述的相同的請求處理流程。作為 servlet,MessageDispatcherServlet 在 Web 應用程式的 web.xml 中組態。您希望 MessageDispatcherServlet 處理的請求必須透過同一個 web.xml 檔案中的 URL 映射進行映射。這是標準 Java EE servlet 組態。以下範例顯示了此類 MessageDispatcherServlet 宣告和映射

<web-app>

    <servlet>
        <servlet-name>spring-ws</servlet-name>
        <servlet-class>org.springframework.ws.transport.http.MessageDispatcherServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>

    <servlet-mapping>
        <servlet-name>spring-ws</servlet-name>
        <url-pattern>/*</url-pattern>
    </servlet-mapping>

</web-app>

在前面的範例中,所有請求都由 spring-ws MessageDispatcherServlet 處理。這只是設定 Spring Web Services 的第一步,因為 Spring-WS 框架使用的各種組件 bean 也需要組態。此組態由標準 Spring XML <bean/> 定義組成。由於 MessageDispatcherServlet 是標準 Spring DispatcherServlet,因此它會在 Web 應用程式的 WEB-INF 目錄中尋找名為 [servlet-name]-servlet.xml 的檔案,並在 Spring 容器中建立其中定義的 bean。在前面的範例中,它會尋找 '/WEB-INF/spring-ws-servlet.xml'。這個檔案包含所有 Spring Web Services bean,例如端點、marshallers 等等。

作為 web.xml 的替代方案,如果您在 Servlet 3+ 環境中執行,則可以程式化方式組態 Spring-WS。為此,Spring-WS 提供了許多抽象基底類別,這些類別擴展了 Spring Framework 中找到的 WebApplicationInitializer 介面。如果您也使用 @Configuration 類別進行 bean 定義,則應擴展 AbstractAnnotationConfigMessageDispatcherServletInitializer

public class MyServletInitializer
    extends AbstractAnnotationConfigMessageDispatcherServletInitializer {

    @Override
    protected Class<?>[] getRootConfigClasses() {
        return new Class[]{MyRootConfig.class};
    }

    @Override
    protected Class<?>[] getServletConfigClasses() {
        return new Class[]{MyEndpointConfig.class};
    }

}

在前面的範例中,我們告訴 Spring 端點 bean 定義可以在 MyEndpointConfig 類別(這是一個 @Configuration 類別)中找到。其他 bean 定義(通常是服務、儲存庫等等)可以在 MyRootConfig 類別中找到。預設情況下,AbstractAnnotationConfigMessageDispatcherServletInitializer 將 servlet 映射到兩個模式:/services*.wsdl,但您可以通過覆寫 getServletMappings() 方法來更改此設定。關於 MessageDispatcherServlet 的程式化配置的更多詳細資訊,請參閱 AbstractMessageDispatcherServletInitializerAbstractAnnotationConfigMessageDispatcherServletInitializer 的 Javadoc。

自動 WSDL 暴露

MessageDispatcherServlet 會自動偵測在其 Spring 容器中定義的任何 WsdlDefinition bean。所有偵測到的 WsdlDefinition bean 也會通過 WsdlDefinitionHandlerAdapter 暴露。這是一種通過定義一些 bean 來向客戶端暴露 WSDL 的便捷方式。

舉例來說,考慮以下 <static-wsdl> 定義,它在 Spring-WS 配置檔案 (/WEB-INF/[servlet-name]-servlet.xml) 中定義。請注意 id 屬性的值,因為它在暴露 WSDL 時會被使用。

<sws:static-wsdl id="orders" location="orders.wsdl"/>

或者,它可以是 @Configuration 類別中的 @Bean 方法

@Bean
public SimpleWsdl11Definition orders() {
	return new SimpleWsdl11Definition(new ClassPathResource("orders.wsdl"));
}

您可以通過對以下形式的 URL 發出 GET 請求來訪問在類路徑上的 orders.wsdl 檔案中定義的 WSDL(根據需要替換主機、端口和 servlet 上下文路徑)

http://localhost:8080/spring-ws/orders.wsdl
所有 WsdlDefinition bean 定義都由 MessageDispatcherServlet 在其 bean 名稱下暴露,並帶有後綴 `.wsdl`。因此,如果 bean 名稱是 echo,主機名稱是 server,並且 Servlet 上下文(war 名稱)是 spring-ws,則可以在 http://server/spring-ws/echo.wsdl 找到 WSDL。

MessageDispatcherServlet(或更準確地說是 WsdlDefinitionHandlerAdapter)的另一個好用的功能是它可以轉換它暴露的所有 WSDL 的 location 值,以反映傳入請求的 URL。

請注意,此 location 轉換功能預設是關閉的。要開啟此功能,您需要為 MessageDispatcherServlet 指定一個初始化參數

<web-app>

  <servlet>
    <servlet-name>spring-ws</servlet-name>
    <servlet-class>org.springframework.ws.transport.http.MessageDispatcherServlet</servlet-class>
    <init-param>
      <param-name>transformWsdlLocations</param-name>
      <param-value>true</param-value>
    </init-param>
  </servlet>

  <servlet-mapping>
    <servlet-name>spring-ws</servlet-name>
    <url-pattern>/*</url-pattern>
  </servlet-mapping>

</web-app>

如果您使用 AbstractAnnotationConfigMessageDispatcherServletInitializer,啟用轉換就像覆寫 isTransformWsdlLocations() 方法並返回 true 一樣簡單。

請查閱 WsdlDefinitionHandlerAdapter 類別的類別層級 Javadoc,以了解有關整個轉換過程的更多資訊。

除了手動編寫 WSDL 並使用 <static-wsdl> 暴露它之外,Spring Web Services 也可以從 XSD schema 生成 WSDL。這是 發布 WSDL 中顯示的方法。下一個應用程式上下文程式碼片段展示了如何建立這樣的動態 WSDL 檔案

<sws:dynamic-wsdl id="orders"
    portTypeName="Orders"
    locationUri="http://localhost:8080/ordersService/">
  <sws:xsd location="Orders.xsd"/>
</sws:dynamic-wsdl>

或者,您可以使用 Java @Bean 方法

@Bean
public DefaultWsdl11Definition orders() {
    DefaultWsdl11Definition definition = new DefaultWsdl11Definition();
    definition.setPortTypeName("Orders");
    definition.setLocationUri("http://localhost:8080/ordersService/");
    definition.setSchema(new SimpleXsdSchema(new ClassPathResource("echo.xsd")));

    return definition;
}

<dynamic-wsdl> 元素依賴於 DefaultWsdl11Definition 類別。此定義類別使用 org.springframework.ws.wsdl.wsdl11.provider 套件中的 WSDL provider 和 ProviderBasedWsdl4jDefinition 類別,以便在第一次請求時生成 WSDL。請參閱這些類別的類別層級 Javadoc,以了解如何在必要時擴展此機制。

DefaultWsdl11Definition(以及因此,<dynamic-wsdl> 標籤)通過使用慣例從 XSD schema 建構 WSDL。它迭代 schema 中找到的所有 element 元素,並為所有元素建立一個 message。接下來,它為所有以定義的請求或響應後綴結尾的消息建立一個 WSDL operation。預設請求後綴是 Request。預設響應後綴是 Response,儘管可以通過分別在 <dynamic-wsdl /> 上設定 requestSuffixresponseSuffix 屬性來更改這些後綴。它還基於操作建構 portTypebindingservice

例如,如果我們的 Orders.xsd schema 定義了 GetOrdersRequestGetOrdersResponse 元素,<dynamic-wsdl> 將建立一個 GetOrdersRequestGetOrdersResponse 消息以及一個 GetOrders 操作,該操作被放入 Orders port type 中。

要使用多個 schema,無論是通過 includes 還是 imports,您都可以將 Commons XMLSchema 放在類路徑中。如果 Commons XMLSchema 在類路徑中,則 <dynamic-wsdl> 元素會遵循所有 XSD imports 和 includes,並將它們內聯到 WSDL 中作為單個 XSD。這大大簡化了 schema 的部署,同時仍然可以單獨編輯它們。

儘管在運行時從您的 XSD 建立 WSDL 可能很方便,但這種方法也有一些缺點。首先,儘管我們嘗試在版本之間保持 WSDL 生成過程的一致性,但仍然有可能會發生(輕微的)更改。其次,生成過程有點慢,但一旦生成,WSDL 就會被緩存以供以後參考。

因此,您應該僅在專案的開發階段使用 <dynamic-wsdl>。我們建議您使用瀏覽器下載生成的 WSDL,將其儲存在專案中,並使用 <static-wsdl> 暴露它。這是真正確保 WSDL 不會隨時間變化的唯一方法。

5.2.2. 在 DispatcherServlet 中配置 Spring-WS

作為 MessageDispatcherServlet 的替代方案,您可以在標準的 Spring-Web MVC DispatcherServlet 中配置 MessageDispatcher。預設情況下,DispatcherServlet 只能委派給 Controllers,但我們可以通過將 WebServiceMessageReceiverHandlerAdapter 添加到 servlet 的 web 應用程式上下文中來指示它委派給 MessageDispatcher

<beans>

    <bean class="org.springframework.ws.transport.http.WebServiceMessageReceiverHandlerAdapter"/>

    <bean class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
        <property name="defaultHandler" ref="messageDispatcher"/>
    </bean

    <bean id="messageDispatcher" class="org.springframework.ws.soap.server.SoapMessageDispatcher"/>

    ...

    <bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter"/>

</beans>

請注意,通過顯式添加 WebServiceMessageReceiverHandlerAdapter,dispatcher servlet 不會加載預設的 adapter,並且無法處理標準的 Spring-MVC @Controllers。因此,我們在末尾添加了 RequestMappingHandlerAdapter

以類似的方式,您可以配置 WsdlDefinitionHandlerAdapter 以確保 DispatcherServlet 可以處理 WsdlDefinition 介面的實作

<beans>

    <bean class="org.springframework.ws.transport.http.WebServiceMessageReceiverHandlerAdapter"/>

    <bean class="org.springframework.ws.transport.http.WsdlDefinitionHandlerAdapter"/>

    <bean class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
        <property name="mappings">
           <props>
             <prop key="*.wsdl">myServiceDefinition</prop>
           </props>
        </property>
        <property name="defaultHandler" ref="messageDispatcher"/>
    </bean>

    <bean id="messageDispatcher" class="org.springframework.ws.soap.server.SoapMessageDispatcher"/>

    <bean id="myServiceDefinition" class="org.springframework.ws.wsdl.wsdl11.SimpleWsdl11Definition">
       <prop name="wsdl" value="/WEB-INF/myServiceDefintion.wsdl"/>
    </bean>

    ...

</beans>

5.2.3. JMS 傳輸

Spring Web Services 通過 Spring 框架中提供的 JMS 功能支援伺服器端 JMS 處理。Spring Web Services 提供了 WebServiceMessageListener 來插入到 MessageListenerContainer 中。此消息監聽器需要 WebServiceMessageFactoryMessageDispatcher 才能運作。以下配置範例展示了這一點

<beans>

    <bean id="connectionFactory" class="org.apache.activemq.ActiveMQConnectionFactory">
        <property name="brokerURL" value="vm://localhost?broker.persistent=false"/>
    </bean>

    <bean id="messageFactory" class="org.springframework.ws.soap.saaj.SaajSoapMessageFactory"/>

    <bean class="org.springframework.jms.listener.DefaultMessageListenerContainer">
        <property name="connectionFactory" ref="connectionFactory"/>
        <property name="destinationName" value="RequestQueue"/>
        <property name="messageListener">
            <bean class="org.springframework.ws.transport.jms.WebServiceMessageListener">
                <property name="messageFactory" ref="messageFactory"/>
                <property name="messageReceiver" ref="messageDispatcher"/>
            </bean>
        </property>
    </bean>

    <bean id="messageDispatcher" class="org.springframework.ws.soap.server.SoapMessageDispatcher">
        <property name="endpointMappings">
            <bean
              class="org.springframework.ws.server.endpoint.mapping.PayloadRootAnnotationMethodEndpointMapping">
                <property name="defaultEndpoint">
                    <bean class="com.example.MyEndpoint"/>
                </property>
            </bean>
        </property>
    </bean>
</beans>

5.2.4. 電子郵件傳輸

除了 HTTP 和 JMS 之外,Spring Web Services 還提供伺服器端電子郵件處理。此功能通過 MailMessageReceiver 類別提供。此類別監控 POP3 或 IMAP 資料夾,將電子郵件轉換為 WebServiceMessage,並使用 SMTP 發送任何響應。您可以通過 storeUri 配置主機名稱,它指示要監控請求的郵件資料夾(通常是 POP3 或 IMAP 資料夾),以及 transportUri,它指示用於發送響應的伺服器(通常是 SMTP 伺服器)。

您可以通過可插拔的策略配置 MailMessageReceiver 如何監控傳入的消息:MonitoringStrategy。預設情況下,使用輪詢策略,其中每五分鐘輪詢傳入資料夾以查找新消息。您可以通過在策略上設定 pollingInterval 屬性來更改此間隔。預設情況下,所有 MonitoringStrategy 實作都會刪除已處理的消息。您可以通過設定 deleteMessages 屬性來更改此設定。

作為效率較低的輪詢方法的替代方案,有一種監控策略使用 IMAP IDLE。IDLE 命令是 IMAP 電子郵件協定的可選擴展,它允許郵件伺服器異步地向 MailMessageReceiver 發送新消息更新。如果您使用支援 IDLE 命令的 IMAP 伺服器,您可以將 ImapIdleMonitoringStrategy 插入到 monitoringStrategy 屬性中。除了支援伺服器之外,您還需要使用 JavaMail 版本 1.4.1 或更高版本。

以下配置片段展示了如何使用伺服器端電子郵件支援,覆寫預設輪詢間隔以每 30 秒(30,000 毫秒)檢查一次

<beans>

    <bean id="messageFactory" class="org.springframework.ws.soap.saaj.SaajSoapMessageFactory"/>

    <bean id="messagingReceiver" class="org.springframework.ws.transport.mail.MailMessageReceiver">
        <property name="messageFactory" ref="messageFactory"/>
        <property name="from" value="Spring-WS SOAP Server &lt;[email protected]&gt;"/>
        <property name="storeUri" value="imap://server:[email protected]/INBOX"/>
        <property name="transportUri" value="smtp://smtp.example.com"/>
        <property name="messageReceiver" ref="messageDispatcher"/>
        <property name="monitoringStrategy">
            <bean class="org.springframework.ws.transport.mail.monitor.PollingMonitoringStrategy">
                <property name="pollingInterval" value="30000"/>
            </bean>
        </property>
    </bean>

    <bean id="messageDispatcher" class="org.springframework.ws.soap.server.SoapMessageDispatcher">
        <property name="endpointMappings">
            <bean
              class="org.springframework.ws.server.endpoint.mapping.PayloadRootAnnotationMethodEndpointMapping">
                <property name="defaultEndpoint">
                    <bean class="com.example.MyEndpoint"/>
                </property>
            </bean>
        </property>
    </bean>
</beans>

5.2.5. 嵌入式 HTTP 伺服器傳輸

Spring Web Services 提供基於 Sun 的 JRE 1.6 HTTP 伺服器 的傳輸。嵌入式 HTTP 伺服器是一個獨立的伺服器,易於配置。它為傳統的 servlet 容器提供了一個更輕量級的替代方案。

當使用嵌入式 HTTP 伺服器時,您不需要外部部署描述符 (web.xml)。您只需要定義伺服器的實例並配置它來處理傳入的請求。Core Spring Framework 中的 remoting 模組包含一個方便的 HTTP 伺服器 factory bean:SimpleHttpServerFactoryBean。最重要的屬性是 contexts,它將上下文路徑映射到相應的 HttpHandler 實例。

Spring Web Services 提供了 HttpHandler 介面的兩個實作:WsdlDefinitionHttpHandlerWebServiceMessageReceiverHttpHandler。前者將傳入的 GET 請求映射到 WsdlDefinition。後者負責處理 web 服務消息的 POST 請求,因此需要 WebServiceMessageFactory(通常是 SaajSoapMessageFactory)和 WebServiceMessageReceiver(通常是 SoapMessageDispatcher)來完成其任務。

為了與 servlet 世界進行比較,contexts 屬性扮演了 web.xml 中 servlet 映射的角色,而 WebServiceMessageReceiverHttpHandler 相當於 MessageDispatcherServlet

以下程式碼片段展示了 HTTP 伺服器傳輸的配置範例

<beans>

    <bean id="messageFactory" class="org.springframework.ws.soap.saaj.SaajSoapMessageFactory"/>

    <bean id="messageReceiver" class="org.springframework.ws.soap.server.SoapMessageDispatcher">
        <property name="endpointMappings" ref="endpointMapping"/>
    </bean>

    <bean id="endpointMapping" class="org.springframework.ws.server.endpoint.mapping.PayloadRootAnnotationMethodEndpointMapping">
        <property name="defaultEndpoint" ref="stockEndpoint"/>
    </bean>

    <bean id="httpServer" class="org.springframework.remoting.support.SimpleHttpServerFactoryBean">
        <property name="contexts">
            <map>
                <entry key="/StockService.wsdl" value-ref="wsdlHandler"/>
                <entry key="/StockService" value-ref="soapHandler"/>
            </map>
        </property>
    </bean>

    <bean id="soapHandler" class="org.springframework.ws.transport.http.WebServiceMessageReceiverHttpHandler">
        <property name="messageFactory" ref="messageFactory"/>
        <property name="messageReceiver" ref="messageReceiver"/>
    </bean>

    <bean id="wsdlHandler" class="org.springframework.ws.transport.http.WsdlDefinitionHttpHandler">
        <property name="definition" ref="wsdlDefinition"/>
    </bean>
</beans>

有關 SimpleHttpServerFactoryBean 的更多資訊,請參閱 Javadoc

5.2.6. XMPP 傳輸

Spring Web Services 2.0 引入了對 XMPP(也稱為 Jabber)的支援。此支援基於 Smack 庫。

Spring Web Services 對 XMPP 的支援與其他傳輸非常相似:WebServiceTemplate 有一個 XmppMessageSenderMessageDispatcher 有一個 XmppMessageReceiver

以下範例展示了如何設定伺服器端 XMPP 组件

<beans>

    <bean id="messageFactory" class="org.springframework.ws.soap.saaj.SaajSoapMessageFactory"/>

    <bean id="connection" class="org.springframework.ws.transport.xmpp.support.XmppConnectionFactoryBean">
        <property name="host" value="jabber.org"/>
        <property name="username" value="username"/>
        <property name="password" value="password"/>
    </bean>

    <bean id="messagingReceiver" class="org.springframework.ws.transport.xmpp.XmppMessageReceiver">
        <property name="messageFactory" ref="messageFactory"/>
        <property name="connection" ref="connection"/>
        <property name="messageReceiver" ref="messageDispatcher"/>
    </bean>

    <bean id="messageDispatcher" class="org.springframework.ws.soap.server.SoapMessageDispatcher">
        <property name="endpointMappings">
            <bean
              class="org.springframework.ws.server.endpoint.mapping.PayloadRootAnnotationMethodEndpointMapping">
                <property name="defaultEndpoint">
                    <bean class="com.example.MyEndpoint"/>
                </property>
            </bean>
        </property>
    </bean>

</beans>

5.2.7. MTOM

MTOM 是用於在 Web 服務之間發送二進制數據的機制。您可以通過 MTOM 範例 了解如何在 Spring WS 中實作此功能。

5.3. 端點

端點是 Spring-WS 伺服器端支援的核心概念。端點提供對應用程式行為的訪問,應用程式行為通常由業務服務介面定義。端點解釋 XML 請求消息,並使用該輸入來(通常)調用業務服務上的方法。該服務調用的結果表示為響應消息。Spring-WS 具有各種各樣的端點,並使用各種方法來處理 XML 消息並建立響應。

您可以通過使用 @Endpoint 註解類別來建立端點。在該類別中,您可以使用各種參數類型(例如 DOM 元素、JAXB2 物件等)定義一個或多個處理傳入 XML 請求的方法。您可以使用另一個註解(通常是 @PayloadRoot)來指示方法可以處理的消息類型。

考慮以下範例端點

package samples;

import org.w3c.dom.Element;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.ws.server.endpoint.annotation.Endpoint;
import org.springframework.ws.server.endpoint.annotation.PayloadRoot;
import org.springframework.ws.soap.SoapHeader;

@Endpoint                                                                                      (1)
public class AnnotationOrderEndpoint {

  private final OrderService orderService;

  @Autowired                                                                                   (2)
  public AnnotationOrderEndpoint(OrderService orderService) {
      this.orderService = orderService;
  }

  @PayloadRoot(localPart = "order", namespace = "http://samples")                              (5)
  public void order(@RequestPayload Element orderElement) {                                    (3)
    Order order = createOrder(orderElement);
    orderService.createOrder(order);
  }

  @PayloadRoot(localPart = "orderRequest", namespace = "http://samples")                       (5)
  @ResponsePayload
  public Order getOrder(@RequestPayload OrderRequest orderRequest, SoapHeader header) {        (4)
    checkSoapHeaderForSomething(header);
    return orderService.getOrder(orderRequest.getId());
  }

  ...

}
1 該類別使用 @Endpoint 註解,將其標記為 Spring-WS 端點。
2 建構子使用 @Autowired 標記,以便將 OrderService 業務服務注入到此端點中。
3 order 方法採用 Element(使用 @RequestPayload 註解)作為參數。這意味著消息的 payload 作為 DOM 元素傳遞給此方法。該方法具有 void 返回類型,表示不發送響應消息。有關端點方法的更多資訊,請參閱 @Endpoint 處理方法
4 getOrder 方法採用 OrderRequest(也使用 @RequestPayload 註解)作為參數。此參數是一個 JAXB2 支援的物件(它使用 @XmlRootElement 註解)。這意味著消息的 payload 作為一個 unmarshalled 物件傳遞給此方法。SoapHeader 類型也作為參數給出。在調用時,此參數包含請求消息的 SOAP header。該方法也使用 @ResponsePayload 註解,表示返回值(Order)用作響應消息的 payload。有關端點方法的更多資訊,請參閱 @Endpoint 處理方法
5 此端點的兩個處理方法都使用 @PayloadRoot 標記,指示該方法可以處理哪種類型的請求消息:對於具有 orderRequest 本地名稱和 http://samples 命名空間 URI 的請求,將調用 getOrder 方法。對於具有 order 本地名稱的請求,將調用 order 方法。有關 @PayloadRoot 的更多資訊,請參閱 端點映射

要啟用對 @Endpoint 和相關 Spring-WS 註解的支援,您需要將以下內容添加到您的 Spring 應用程式上下文中

<beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:sws="http://www.springframework.org/schema/web-services"
  xsi:schemaLocation="http://www.springframework.org/schema/beans
      http://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/web-services
      http://www.springframework.org/schema/web-services/web-services.xsd">

  *<sws:annotation-driven />

</beans>

或者,如果您使用 @Configuration 類別而不是 Spring XML,您可以使用 @EnableWs 註解您的配置類別

@EnableWs
@Configuration
public class EchoConfig {

    // @Bean definitions go here

}

要自定義 @EnableWs 配置,您可以實作 WsConfigurer,或者更好的方法是擴展 WsConfigurerAdapter

@Configuration
@EnableWs
@ComponentScan(basePackageClasses = { MyConfiguration.class })
public class MyConfiguration extends WsConfigurerAdapter {

  @Override
  public void addInterceptors(List<EndpointInterceptor> interceptors) {
    interceptors.add(new MyInterceptor());
  }

  @Override
  public void addArgumentResolvers(List<MethodArgumentResolver> argumentResolvers) {
    argumentResolvers.add(new MyArgumentResolver());
  }

  // More overridden methods ...
}

在接下來的幾個章節中,將更詳細地描述 @Endpoint 編程模型。

端點與任何其他 Spring Bean 一樣,預設情況下範圍設定為 singleton。也就是說,每個容器創建一個 bean 定義的實例。作為 singleton 意味著多個線程可以同時使用它,因此端點必須是線程安全的。如果您想使用不同的範圍,例如 prototype,請參閱 Spring 參考文件
Note that all abstract base classes provided in Spring-WS are thread safe, unless otherwise indicated in the class-level Javadoc.

5.3.1. @Endpoint 處理方法

為了讓端點實際處理傳入的 XML 消息,它需要有一個或多個處理方法。處理方法可以採用各種各樣的參數和返回類型。但是,它們通常有一個包含消息 payload 的參數,並且它們返回響應消息的 payload(如果有的話)。本節介紹了支援哪些參數和返回類型。

為了指示方法可以處理哪種類型的消息,通常使用 @PayloadRoot@SoapAction 註解對方法進行註解。您可以在 端點映射 中了解有關這些註解的更多資訊。

以下範例展示了一個處理方法

@PayloadRoot(localPart = "order", namespace = "http://samples")
public void order(@RequestPayload Element orderElement) {
  Order order = createOrder(orderElement);
  orderService.createOrder(order);
}

order 方法採用 Element(使用 @RequestPayload 註解)作為參數。這意味著消息的 payload 作為 DOM 元素傳遞給此方法。該方法具有 void 返回類型,表示不發送響應消息。

處理方法參數

處理方法通常具有一個或多個參數,這些參數引用傳入 XML 消息的各個部分。最常見的是,處理方法具有一個映射到消息 payload 的單個參數,但它也可以映射到請求消息的其他部分,例如 SOAP header。本節描述了您可以在處理方法簽名中使用的參數。

要將參數映射到請求消息的 payload,您需要使用 @RequestPayload 註解對此參數進行註解。此註解告訴 Spring-WS 參數需要綁定到請求 payload。

下表描述了支援的參數類型。它顯示了支援的類型、參數是否應該使用 @RequestPayload 註解以及任何其他注意事項。

名稱 支援的參數類型 是否需要 @RequestPayload 其他注意事項

TrAX

javax.xml.transform.Source 和子介面(DOMSourceSAXSourceStreamSourceStAXSource

預設啟用。

W3C DOM

org.w3c.dom.Element

預設啟用

dom4j

org.dom4j.Element

當 dom4j 在類路徑上時啟用。

JDOM

org.jdom.Element

當 JDOM 在類路徑上時啟用。

XOM

nu.xom.Element

當 XOM 在類路徑上時啟用。

StAX

javax.xml.stream.XMLStreamReaderjavax.xml.stream.XMLEventReader

當 StAX 在類路徑上時啟用。

XPath

任何布林值、double、Stringorg.w3c.Nodeorg.w3c.dom.NodeList,或可以通過 Spring 轉換服務String 轉換的類型,並且使用 @XPathParam 註解。

預設啟用,請參閱 標題為 XPathParam 的章節

消息上下文

org.springframework.ws.context.MessageContext

預設啟用。

SOAP

org.springframework.ws.soap.SoapMessageorg.springframework.ws.soap.SoapBodyorg.springframework.ws.soap.SoapEnvelopeorg.springframework.ws.soap.SoapHeaderorg.springframework.ws.soap.SoapHeaderElement,當與 @SoapHeader 註解結合使用時。

預設啟用。

JAXB2

任何使用 javax.xml.bind.annotation.XmlRootElementjavax.xml.bind.JAXBElement 註解的類型。

當 JAXB2 在類路徑上時啟用。

OXM

Spring OXM Unmarshaller 支援的任何類型。

當指定 <sws:annotation-driven/>unmarshaller 屬性時啟用。

接下來的幾個範例展示了可能的方法簽名。以下方法使用請求消息的 payload 作為 DOM org.w3c.dom.Element 調用

public void handle(@RequestPayload Element element)

以下方法使用請求消息的 payload 作為 javax.xml.transform.dom.DOMSource 調用。header 參數綁定到請求消息的 SOAP header。

public void handle(@RequestPayload DOMSource domSource, SoapHeader header)

以下方法使用請求消息的 payload unmarshalled 到 MyJaxb2Object 中(使用 @XmlRootElement 註解)調用。消息的 payload 也作為 DOM Element 給出。整個 消息上下文 作為第三個參數傳遞。

public void handle(@RequestPayload MyJaxb2Object requestObject, @RequestPayload Element element, Message messageContext)

如您所見,在定義如何處理方法簽名時,有很多可能性。您甚至可以擴展此機制以支援您自己的參數類型。請參閱 DefaultMethodEndpointAdapterMethodArgumentResolver 的 Javadoc,以了解如何操作。

@XPathParam

一種參數類型需要一些額外的解釋:@XPathParam。這裡的想法是,您可以使用 XPath 表達式註解一個或多個方法參數,並且每個此類註解的參數都綁定到表達式的評估結果。以下範例展示了如何做到這一點

package samples;

import javax.xml.transform.Source;

import org.springframework.ws.server.endpoint.annotation.Endpoint;
import org.springframework.ws.server.endpoint.annotation.Namespace;
import org.springframework.ws.server.endpoint.annotation.PayloadRoot;
import org.springframework.ws.server.endpoint.annotation.XPathParam;

@Endpoint
public class AnnotationOrderEndpoint {

  private final OrderService orderService;

  public AnnotationOrderEndpoint(OrderService orderService) {
    this.orderService = orderService;
  }

  @PayloadRoot(localPart = "orderRequest", namespace = "http://samples")
  @Namespace(prefix = "s", uri="http://samples")
  public Order getOrder(@XPathParam("/s:orderRequest/@id") int orderId) {
    Order order = orderService.getOrder(orderId);
    // create Source from order and return it
  }

}

由於我們在 XPath 表達式中使用了 s 前綴,因此我們必須將其綁定到 http://samples 命名空間。這是通過 @Namespace 註解完成的。或者,我們可以將此註解放在類型層級,以便為所有處理程序方法使用相同的命名空間映射,甚至可以放在套件層級(在 package-info.java 中)以便為多個端點使用它。

通過使用 @XPathParam,您可以綁定到 XPath 支援的所有數據類型

  • booleanBoolean

  • doubleDouble

  • String

  • Node

  • NodeList

除了此列表之外,您還可以使任何可以通過 Spring 轉換服務String 轉換的類型。

處理方法返回類型

要發送響應消息,處理需要指定返回類型。如果不需要響應消息,則方法可以聲明 void 返回類型。最常見的是,返回類型用於建立響應消息的 payload。但是,您也可以映射到響應消息的其他部分。本節描述了您可以在處理方法簽名中使用的返回類型。

要將返回值映射到響應消息的 payload,您需要使用 @ResponsePayload 註解對方法進行註解。此註解告訴 Spring-WS 返回值需要綁定到響應 payload。

下表描述了支援的返回類型。它顯示了支援的類型、參數是否應該使用 @ResponsePayload 註解以及任何其他注意事項。

名稱 支援的返回類型 是否需要 @ResponsePayload 其他注意事項

無響應

void

預設啟用。

TrAX

javax.xml.transform.Source 和子介面(DOMSourceSAXSourceStreamSourceStAXSource

預設啟用。

W3C DOM

org.w3c.dom.Element

預設啟用

dom4j

org.dom4j.Element

當 dom4j 在類路徑上時啟用。

JDOM

org.jdom.Element

當 JDOM 在類路徑上時啟用。

XOM

nu.xom.Element

當 XOM 在類路徑上時啟用。

JAXB2

任何使用 javax.xml.bind.annotation.XmlRootElementjavax.xml.bind.JAXBElement 註解的類型。

當 JAXB2 在類路徑上時啟用。

OXM

Spring OXM Marshaller 支援的任何類型。

當指定 <sws:annotation-driven/>marshaller 屬性時啟用。

在定義處理方法簽名時,有很多可能性。甚至可以擴展此機制以支援您自己的參數類型。請參閱 DefaultMethodEndpointAdapterMethodReturnValueHandler 的類別層級 Javadoc,以了解如何操作。

5.4. 端點映射

端點映射負責將傳入的消息映射到適當的端點。某些端點映射預設啟用 — 例如,PayloadRootAnnotationMethodEndpointMappingSoapActionAnnotationMethodEndpointMapping。但是,我們首先需要檢查 EndpointMapping 的一般概念。

EndpointMapping 傳遞一個 EndpointInvocationChain,其中包含與傳入請求匹配的端點,並且還可以包含應用於請求和響應的端點攔截器列表。當請求進入時,MessageDispatcher 將其交給端點映射,以使其檢查請求並提出適當的 EndpointInvocationChain。然後,MessageDispatcher 調用端點和鏈中的任何攔截器。

可配置端點映射的概念,它可以選擇性地包含攔截器(攔截器可以反過來操作請求、響應或兩者),非常強大。許多支援功能可以構建到自定義 EndpointMapping 實作中。例如,自定義端點映射不僅可以基於消息的內容,還可以基於特定的 SOAP header(或實際上,多個 SOAP header)來選擇端點。

大多數端點映射都繼承自 AbstractEndpointMapping,它提供了一個「interceptors」屬性,這是要使用的攔截器列表。EndpointInterceptors攔截請求 — EndpointInterceptor 介面 中討論。此外,還有 defaultEndpoint,它是當此端點映射未產生匹配端點時要使用的預設端點。

端點 中所述,@Endpoint 風格允許您在一個端點類別中處理多個請求。這是 MethodEndpointMapping 的責任。此映射確定要為傳入的請求消息調用哪個方法。

有兩個端點映射可以將請求定向到方法:PayloadRootAnnotationMethodEndpointMappingSoapActionAnnotationMethodEndpointMapping。您可以使用應用程式上下文中的 <sws:annotation-driven/> 啟用這兩種方法。

PayloadRootAnnotationMethodEndpointMapping 使用帶有 localPartnamespace 元素的 @PayloadRoot 註解,以標記具有特定限定名稱的方法。每當收到 payload 根元素具有此限定名稱的消息時,就會調用該方法。有關範例,請參閱 上方

或者,SoapActionAnnotationMethodEndpointMapping 使用 @SoapAction 註解來標記具有特定 SOAP Action 的方法。每當收到帶有此 SOAPAction header 的消息時,就會調用該方法。

5.4.1. WS-Addressing

WS-Addressing 指定了一種與傳輸無關的路由機制。它基於 ToAction SOAP header,它們分別指示 SOAP 消息的目的地和意圖。此外,WS-Addressing 允許您定義返回地址(用於常規消息和錯誤)和唯一消息標識符,可用於關聯。有關 WS-Addressing 的更多資訊,請參閱 https://en.wikipedia.org/wiki/WS-Addressing。以下範例展示了一個 WS-Addressing 消息

<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://www.w3.org/2003/05/soap-envelope"
    xmlns:wsa="http://www.w3.org/2005/08/addressing">
  <SOAP-ENV::Header>
    <wsa:MessageID>urn:uuid:21363e0d-2645-4eb7-8afd-2f5ee1bb25cf</wsa:MessageID>
    <wsa:ReplyTo>
      <wsa:Address>http://example.com/business/client1</wsa:Address>
    </wsa:ReplyTo>
    <wsa:To S:mustUnderstand="true">http://example/com/fabrikam</wsa:To>
    <wsa:Action>http://example.com/fabrikam/mail/Delete</wsa:Action>
  </SOAP-ENV:Header>
  <SOAP-ENV:Body>
    <f:Delete xmlns:f="http://example.com/fabrikam">
      <f:maxCount>42</f:maxCount>
    </f:Delete>
  </SOAP-ENV:Body>
</SOAP-ENV:Envelope>

在前面的範例中,目的地設定為 http://example/com/fabrikam,而 action 設定為 http://example.com/fabrikam/mail/Delete。此外,還有一個消息標識符和一個 reply-to 地址。預設情況下,此地址是「anonymous」地址,表示應該使用與請求相同的通道(即 HTTP 響應)發送響應,但它也可以是另一個地址,如本範例所示。

在 Spring Web Services 中,WS-Addressing 實作為端點映射。通過使用此映射,您可以將 WS-Addressing action 與端點關聯,類似於前面描述的 SoapActionAnnotationMethodEndpointMapping

使用 AnnotationActionEndpointMapping

AnnotationActionEndpointMapping 類似於 SoapActionAnnotationMethodEndpointMapping,但使用 WS-Addressing header 而不是 SOAP Action 傳輸 header。

要使用 AnnotationActionEndpointMapping,請使用 @Action 註解對處理方法進行註解,類似於 @Endpoint 處理方法端點映射 中描述的 @PayloadRoot@SoapAction 註解。以下範例展示了如何做到這一點

package samples;

import org.springframework.ws.server.endpoint.annotation.Endpoint;
import org.springframework.ws.soap.addressing.server.annotation.Action

@Endpoint
public class AnnotationOrderEndpoint {
    private final OrderService orderService;

    public AnnotationOrderEndpoint(OrderService orderService) {
        this.orderService = orderService;
    }

    @Action("http://samples/RequestOrder")
    public Order getOrder(OrderRequest orderRequest) {
        return orderService.getOrder(orderRequest.getId());
    }

    @Action("http://samples/CreateOrder")
    public void order(Order order) {
        orderService.createOrder(order);
    }

}

前面的映射將具有 http://samples/RequestOrder 的 WS-Addressing Action 的請求路由到 getOrder 方法。具有 http://samples/CreateOrder 的請求被路由到 order 方法。

預設情況下,AnnotationActionEndpointMapping 同時支援 WS-Addressing 的 1.0 版(2006 年 5 月)和 2004 年 8 月版。這兩個版本最受歡迎,並且可與 Axis 1 和 2、JAX-WS、XFire、Windows Communication Foundation (WCF) 和 Windows Services Enhancements (WSE) 3.0 互操作。如有必要,可以將規範的特定版本注入到 versions 屬性中。

除了 @Action 註解之外,您還可以使用 @Address 註解對類別進行註解。如果設定,則將該值與傳入消息的 To header 屬性進行比較。

最後,還有 messageSenders 屬性,這是向非匿名、out-of-bound 地址發送響應消息所必需的。您可以在此屬性中設定 MessageSender 實作,就像在 WebServiceTemplate 上一樣。請參閱 URIs 和傳輸

5.4.2. 攔截請求 — EndpointInterceptor 介面

端點映射機制具有端點攔截器的概念。當您想要將特定功能應用於某些請求時,這些功能可能非常有用 — 例如,處理與安全性相關的 SOAP header 或請求和響應消息的日誌記錄。

端點攔截器通常通過在應用程式上下文中使用 <sws:interceptors> 元素來定義。在此元素中,您可以定義應用於該應用程式上下文中定義的所有端點的端點攔截器 bean。或者,您可以使用 <sws:payloadRoot><sws:soapAction> 元素來指定攔截器應應用於哪個 payload 根名稱或 SOAP action。以下範例展示了如何做到這一點

<sws:interceptors>
  <bean class="samples.MyGlobalInterceptor"/>
  <sws:payloadRoot namespaceUri="http://www.example.com">
    <bean class="samples.MyPayloadRootInterceptor"/>
  </sws:payloadRoot>
  <sws:soapAction value="http://www.example.com/SoapAction">
    <bean class="samples.MySoapActionInterceptor1"/>
    <ref bean="mySoapActionInterceptor2"/>
  </sws:soapAction>
</sws:interceptors>

<bean id="mySoapActionInterceptor2" class="samples.MySoapActionInterceptor2"/>

在前面的範例中,我們定義了一個「全域」攔截器 (MyGlobalInterceptor),它會攔截所有請求和回應。我們也定義了一個僅適用於 XML 訊息的攔截器,這些訊息的酬載根命名空間為 http://www.example.com。除了 namespaceUri 之外,我們還可以定義一個 localPart 屬性,以進一步限制攔截器適用的訊息。最後,我們定義了兩個攔截器,它們在訊息具有 http://www.example.com/SoapAction SOAP 行動時適用。請注意第二個攔截器實際上是對 <interceptors> 元素外部的 Bean 定義的參考。您可以在 <interceptors> 元素內的任何地方使用 Bean 參考。

當您使用 @Configuration 類別時,您可以從 WsConfigurerAdapter 擴展以新增攔截器

@Configuration
@EnableWs
public class MyWsConfiguration extends WsConfigurerAdapter {

  @Override
  public void addInterceptors(List<EndpointInterceptor> interceptors) {
    interceptors.add(new MyPayloadRootInterceptor());
  }

}

攔截器必須實作來自 org.springframework.ws.server 套件的 EndpointInterceptor 介面。此介面定義了三個方法,其中一個可用於在實際端點處理之前處理請求訊息,一個可用於處理正常回應訊息,另一個可用於處理錯誤訊息。後兩個方法會在端點處理之後呼叫。這三個方法應提供足夠的彈性來執行各種前處理和後處理。

攔截器上的 handleRequest(..) 方法會傳回布林值。您可以使用此方法來中斷或繼續調用鏈的處理。當此方法傳回 true 時,端點處理鏈將繼續。當它傳回 false 時,MessageDispatcher 會將其解釋為攔截器本身已處理了事情,並且不會繼續處理調用鏈中的其他攔截器和實際端點。handleResponse(..)handleFault(..) 方法也具有布林傳回值。當這些方法傳回 false 時,回應將不會傳送回用戶端。

您可以在 Web 服務中使用許多標準的 EndpointInterceptor 實作。此外,還有 XwsSecurityInterceptor,它在 XwsSecurityInterceptor 中描述。

PayloadLoggingInterceptorSoapEnvelopeLoggingInterceptor

在開發 Web 服務時,記錄傳入和傳出的 XML 訊息會很有用。Spring WS 使用 PayloadLoggingInterceptorSoapEnvelopeLoggingInterceptor 類別來簡化此操作。前者僅將訊息的酬載記錄到 Commons Logging Log。後者記錄整個 SOAP 信封,包括 SOAP 標頭。以下範例示範如何在端點映射中定義 PayloadLoggingInterceptor

  <sws:interceptors>
    <bean class="org.springframework.ws.server.endpoint.interceptor.PayloadLoggingInterceptor"/>
  </sws:interceptors>

這兩個攔截器都具有兩個屬性,logRequestlogResponse,可以將它們設定為 false 以停用請求或回應訊息的記錄。

您可以像前面描述的那樣,對 PayloadLoggingInterceptor 也使用 WsConfigurerAdapter 方法。

PayloadValidatingInterceptor

使用合約優先開發樣式的好處之一是,我們可以使用綱要來驗證傳入和傳出的 XML 訊息。Spring-WS 使用 PayloadValidatingInterceptor 來簡化此操作。此攔截器需要參考一個或多個 W3C XML 或 RELAX NG 綱要,並且可以設定為驗證請求、回應或兩者。

請注意,請求驗證聽起來是個好主意,但它會使產生的 Web 服務非常嚴格。通常,請求是否驗證並不是真正重要的,重要的是端點是否可以獲得足夠的資訊來滿足請求。驗證回應是個好主意,因為端點應遵守其綱要。請記住 Postel 定律:「對你所做的事情要保守;對你從別人那裡接受的東西要寬容。」

以下範例使用 PayloadValidatingInterceptor。在此範例中,我們使用 /WEB-INF/orders.xsd 中的綱要來驗證回應,但不驗證請求。請注意,PayloadValidatingInterceptor 也可以透過設定 schemas 屬性來接受多個綱要。

<bean id="validatingInterceptor"
        class="org.springframework.ws.soap.server.endpoint.interceptor.PayloadValidatingInterceptor">
    <property name="schema" value="/WEB-INF/orders.xsd"/>
    <property name="validateRequest" value="false"/>
    <property name="validateResponse" value="true"/>
</bean>

當然,您可以像前面描述的那樣,對 PayloadValidatingInterceptor 也使用 WsConfigurerAdapter 方法。

使用 PayloadTransformingInterceptor

為了將酬載轉換為另一種 XML 格式,Spring Web Services 提供了 PayloadTransformingInterceptor。此端點攔截器基於 XSLT 樣式表,特別適用於支援 Web 服務的多個版本,因為您可以將較舊的訊息格式轉換為較新的格式。以下範例使用 PayloadTransformingInterceptor

<bean id="transformingInterceptor"
        class="org.springframework.ws.server.endpoint.interceptor.PayloadTransformingInterceptor">
    <property name="requestXslt" value="/WEB-INF/oldRequests.xslt"/>
    <property name="responseXslt" value="/WEB-INF/oldResponses.xslt"/>
</bean>

在前面的範例中,我們透過使用 /WEB-INF/oldRequests.xslt 來轉換請求,並透過使用 /WEB-INF/oldResponses.xslt 來轉換回應訊息。請注意,由於端點攔截器是在端點映射層級註冊的,因此您可以建立一個適用於「舊樣式」訊息的端點映射,並將攔截器新增到該映射。因此,轉換僅適用於這些「舊樣式」訊息。

您可以像前面描述的那樣,對 PayloadTransformingInterceptor 也使用 WsConfigurerAdapter 方法。

5.5. 處理例外

Spring-WS 提供了 EndpointExceptionResolvers,以減輕在端點處理與請求相符的訊息時發生意外例外的痛苦。端點例外解析器在某種程度上類似於可以在 Web 應用程式描述器 web.xml 中定義的例外映射。但是,它們提供了一種更靈活的方式來處理例外。它們提供了關於在拋出例外時調用了哪個端點的資訊。此外,以程式化方式處理例外讓您有更多選擇來適當地回應。您可以隨心所欲地處理例外,而不是透過給出例外和堆疊追蹤來暴露應用程式的內部結構,例如,透過傳回具有特定錯誤代碼和字串的 SOAP 錯誤。

端點例外解析器會自動被 MessageDispatcher 拾取,因此不需要明確的組態。

除了實作 EndpointExceptionResolver 介面(這只是一個實作 resolveException(MessageContext, endpoint, Exception) 方法的問題)之外,您也可以使用提供的實作之一。最簡單的實作是 SimpleSoapExceptionResolver,它會建立 SOAP 1.1 Server 或 SOAP 1.2 Receiver 錯誤,並使用例外訊息作為錯誤字串。SimpleSoapExceptionResolver 是預設值,但可以透過明確新增另一個解析器來覆寫它。

5.5.1. SoapFaultMappingExceptionResolver

SoapFaultMappingExceptionResolver 是更複雜的實作。此解析器可讓您取得可能拋出的任何例外的類別名稱,並將其映射到 SOAP Fault

<beans>
    <bean id="exceptionResolver"
        class="org.springframework.ws.soap.server.endpoint.SoapFaultMappingExceptionResolver">
        <property name="defaultFault" value="SERVER"/>
        <property name="exceptionMappings">
            <value>
                org.springframework.oxm.ValidationFailureException=CLIENT,Invalid request
            </value>
        </property>
    </bean>
</beans>

鍵值和預設端點使用 faultCode,faultString,locale 格式,其中僅錯誤代碼是必需的。如果未設定錯誤字串,則預設為例外訊息。如果未設定語言,則預設為英語。前面的組態將 ValidationFailureException 類型的例外映射到用戶端 SOAP 錯誤,其錯誤字串為 Invalid request,如下所示

<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
    <SOAP-ENV:Body>
       <SOAP-ENV:Fault>
           <faultcode>SOAP-ENV:Client</faultcode>
           <faultstring>Invalid request</faultstring>
       </SOAP-ENV:Fault>
    </SOAP-ENV:Body>
</SOAP-ENV:Envelope>

如果發生任何其他例外,它會傳回預設錯誤:伺服器端錯誤,並以例外訊息作為錯誤字串。

5.5.2. 使用 SoapFaultAnnotationExceptionResolver

您也可以使用 @SoapFault 註釋來註釋例外類別,以指示每當拋出該例外時應傳回的 SOAP 錯誤。為了使這些註釋被拾取,您需要將 SoapFaultAnnotationExceptionResolver 新增到您的應用程式內容中。註釋的元素包括錯誤代碼列舉、錯誤字串或原因以及語言。以下範例顯示了這樣一個例外

package samples;

import org.springframework.ws.soap.server.endpoint.annotation.FaultCode;
import org.springframework.ws.soap.server.endpoint.annotation.SoapFault;

@SoapFault(faultCode = FaultCode.SERVER)
public class MyBusinessException extends Exception {

    public MyClientException(String message) {
        super(message);
    }
}

每當在端點調用期間使用建構子字串 "Oops!" 拋出 MyBusinessException 時,都會產生以下回應

<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
    <SOAP-ENV:Body>
       <SOAP-ENV:Fault>
           <faultcode>SOAP-ENV:Server</faultcode>
           <faultstring>Oops!</faultstring>
       </SOAP-ENV:Fault>
    </SOAP-ENV:Body>
</SOAP-ENV:Envelope>

5.6. 伺服器端測試

在測試您的 Web 服務端點時,您有兩種可能的方法

  • 編寫單元測試,您可以在其中為您的端點提供(模擬)參數以供使用。

    此方法的優點是它很容易完成(特別是對於使用 @Endpoint 註釋的類別)。缺點是您沒有真正測試透過網路傳送的 XML 訊息的確切內容。

  • 編寫整合測試,它確實測試訊息的內容。

第一種方法可以使用模擬框架(例如 EasyMock、JMock 等)輕鬆完成。下一節重點介紹使用 Spring Web Services 2.0 中引入的測試功能來編寫整合測試。

5.6.1. 編寫伺服器端整合測試

Spring Web Services 2.0 引入了對建立端點整合測試的支援。在此上下文中,端點是處理 (SOAP) 訊息的類別(請參閱 端點)。

整合測試支援位於 org.springframework.ws.test.server 套件中。該套件中的核心類別是 MockWebServiceClient。底層思想是,此用戶端建立請求訊息,然後將其傳送到在標準 MessageDispatcherServlet 應用程式內容中組態的端點(請參閱 MessageDispatcherServlet)。這些端點處理訊息並建立回應。然後,用戶端接收此回應並根據已註冊的期望驗證它。

MockWebServiceClient 的典型用法是:。

  1. 透過呼叫 MockWebServiceClient.createClient(ApplicationContext)MockWebServiceClient.createClient(WebServiceMessageReceiver, WebServiceMessageFactory) 來建立 MockWebServiceClient 實例。

  2. 透過呼叫 sendRequest(RequestCreator) 來傳送請求訊息,可以透過使用 RequestCreators 中提供的預設 RequestCreator 實作(可以靜態匯入)。

  3. 透過呼叫 andExpect(ResponseMatcher) 來設定回應期望,可以透過使用 ResponseMatchers 中提供的預設 ResponseMatcher 實作(可以靜態匯入)。可以透過鏈式 andExpect(ResponseMatcher) 呼叫來設定多個期望。

請注意,MockWebServiceClient(和相關類別)提供了「流暢」API,因此您通常可以使用 IDE 中的程式碼完成功能來引導您完成設定模擬伺服器的過程。
另請注意,您可以依賴 Spring Web Services 中提供的標準記錄功能來進行單元測試。有時,檢查請求或回應訊息以找出特定測試失敗的原因可能會很有用。有關更多資訊,請參閱 訊息記錄和追蹤

例如,考慮以下 Web 服務端點類別

import org.springframework.ws.server.endpoint.annotation.Endpoint;
import org.springframework.ws.server.endpoint.annotation.RequestPayload;
import org.springframework.ws.server.endpoint.annotation.ResponsePayload;

@Endpoint                                                                (1)
public class CustomerEndpoint {

  @ResponsePayload                                                       (2)
  public CustomerCountResponse getCustomerCount(                         (2)
      @RequestPayload CustomerCountRequest request) {                    (2)
    CustomerCountResponse response = new CustomerCountResponse();
    response.setCustomerCount(10);
    return response;
  }

}
1 CustomerEndpoint 使用 @Endpoint 註釋進行註釋。請參閱 端點
2 getCustomerCount() 方法採用 CustomerCountRequest 作為其引數,並傳回 CustomerCountResponse。這兩個類別都是 Marshaller 支援的物件。例如,它們可以具有 @XmlRootElement 註釋以受到 JAXB2 的支援。

以下範例顯示了 CustomerEndpoint 的典型測試

import javax.xml.transform.Source;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.xml.transform.StringSource;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;

import org.springframework.ws.test.server.MockWebServiceClient;                       (1)
import static org.springframework.ws.test.server.RequestCreators.*;                   (1)
import static org.springframework.ws.test.server.ResponseMatchers.*;                  (1)

@RunWith(SpringJUnit4ClassRunner.class)                                               (2)
@ContextConfiguration("spring-ws-servlet.xml")                                        (2)
public class CustomerEndpointIntegrationTest {

  @Autowired
  private ApplicationContext applicationContext;                                      (3)

  private MockWebServiceClient mockClient;

  @Before
  public void createClient() {
    mockClient = MockWebServiceClient.createClient(applicationContext);               (4)
  }

  @Test
  public void customerEndpoint() throws Exception {
    Source requestPayload = new StringSource(
      "<customerCountRequest xmlns='http://springframework.org/spring-ws'>" +
        "<customerName>John Doe</customerName>" +
      "</customerCountRequest>");
    Source responsePayload = new StringSource(
      "<customerCountResponse xmlns='http://springframework.org/spring-ws'>" +
        "<customerCount>10</customerCount>" +
      "</customerCountResponse>");

    mockClient.sendRequest(withPayload(requestPayload)).                              (5)
      andExpect(payload(responsePayload));                                            (5)
  }
}
1 CustomerEndpointIntegrationTest 匯入了 MockWebServiceClient 並靜態匯入了 RequestCreatorsResponseMatchers
2 此測試使用 Spring Framework 中提供的標準測試設施。這不是必需的,但通常是設定測試的最簡單方法。
3 應用程式內容是標準 Spring-WS 應用程式內容(請參閱 MessageDispatcherServlet),從 spring-ws-servlet.xml 讀取。在這種情況下,應用程式內容包含 CustomerEndpoint 的 Bean 定義(或者可能使用了 <context:component-scan />)。
4 @Before 方法中,我們透過使用 createClient 工廠方法來建立 MockWebServiceClient
5 我們透過使用靜態匯入的 RequestCreators 提供的 withPayload() RequestCreator 呼叫 sendRequest() 來傳送請求(請參閱 使用 RequestCreatorRequestCreators)。

我們也透過使用靜態匯入的 ResponseMatchers 提供的 payload() ResponseMatcher 呼叫 andExpect() 來設定回應期望(請參閱 使用 ResponseMatcherResponseMatchers)。

測試的這部分看起來可能有點令人困惑,但 IDE 的程式碼完成功能非常有幫助。在輸入 sendRequest( 之後,您的 IDE 可以為您提供可能的請求建立策略的清單,前提是您靜態匯入了 RequestCreators。這同樣適用於 andExpect(),前提是您靜態匯入了 ResponseMatchers

5.6.2. 使用 RequestCreatorRequestCreators

最初,MockWebServiceClient 需要建立一個請求訊息以供端點使用。用戶端使用 RequestCreator 策略介面來達到此目的

public interface RequestCreator {

  WebServiceMessage createRequest(WebServiceMessageFactory messageFactory)
    throws IOException;

}

您可以編寫自己的此介面實作,透過使用訊息工廠來建立請求訊息,但您當然不必這樣做。RequestCreators 類別提供了一種基於 withPayload() 方法中給定的酬載來建立 RequestCreator 的方法。您通常會靜態匯入 RequestCreators

5.6.3. 使用 ResponseMatcherResponseMatchers

當請求訊息已被端點處理並且已收到回應時,MockWebServiceClient 可以驗證此回應訊息是否符合某些期望。用戶端使用 ResponseMatcher 策略介面來達到此目的

public interface ResponseMatcher {

    void match(WebServiceMessage request,
               WebServiceMessage response)
      throws IOException, AssertionError;

}

再一次,您可以編寫自己的此介面實作,當訊息不符合您的期望時拋出 AssertionError 實例,但您當然不必這樣做,因為 ResponseMatchers 類別為您提供了標準的 ResponseMatcher 實作,可在您的測試中使用。您通常會靜態匯入此類別。

ResponseMatchers 類別提供以下回應匹配器

ResponseMatchers 方法 描述

payload()

期望給定的回應酬載。

validPayload()

期望回應酬載根據給定的 XSD 綱要進行驗證。

xpath()

期望給定的 XPath 運算式存在、不存在或評估為給定的值。

soapHeader()

期望給定的 SOAP 標頭存在於回應訊息中。

noFault()

期望回應訊息不包含 SOAP Fault。

mustUnderstandFault()clientOrSenderFault()serverOrReceiverFault()versionMismatchFault()

期望回應訊息包含特定的 SOAP Fault。

您可以透過鏈式 andExpect() 呼叫來設定多個回應期望

mockClient.sendRequest(...).
 andExpect(payload(expectedResponsePayload)).
 andExpect(validPayload(schemaResource));

有關 ResponseMatchers 提供的回應匹配器的更多資訊,請參閱 Javadoc

6. 在用戶端使用 Spring Web Services

Spring-WS 提供了一個用戶端 Web 服務 API,允許對 Web 服務進行一致的、XML 驅動的存取。它還迎合了 Marshaller 和 Unmarshaller 的使用,以便您的服務層程式碼可以專門處理 Java 物件。

org.springframework.ws.client.core 套件提供了使用用戶端存取 API 的核心功能。它包含範本類別,這些類別簡化了 Web 服務的使用,就像核心 Spring JdbcTemplate 對 JDBC 所做的那樣。Spring 範本類別的通用設計原則是提供輔助方法來執行常見操作,並且對於更複雜的用法,委派給使用者實作的回呼介面。Web 服務範本遵循相同的設計。這些類別為以下方面提供了各種便利方法

  • 傳送和接收 XML 訊息

  • 在傳送之前將物件 Marshalling 成 XML

  • 允許使用多種傳輸選項

6.1. 使用用戶端 API

本節描述如何使用用戶端 API。有關如何使用伺服器端 API,請參閱 使用 Spring-WS 建立 Web 服務

6.1.1. WebServiceTemplate

WebServiceTemplate 是 Spring-WS 中用於用戶端 Web 服務存取的核心類別。它包含用於傳送 Source 物件並接收回應訊息作為 SourceResult 的方法。此外,它可以將物件 Marshalling 成 XML,然後透過傳輸傳送它們,並再次將任何回應 XML Unmarshalling 成物件。

URI 和傳輸

WebServiceTemplate 類別使用 URI 作為訊息目的地。您可以設定範本本身的 defaultUri 屬性,也可以在呼叫範本上的方法時明確提供 URI。URI 會解析為 WebServiceMessageSender,後者負責透過傳輸層傳送 XML 訊息。您可以透過使用 WebServiceTemplate 類別的 messageSendermessageSenders 屬性來設定一個或多個訊息傳送器。

HTTP 傳輸

有兩個 WebServiceMessageSender 介面的實作,用於透過 HTTP 傳送訊息。預設實作是 HttpUrlConnectionMessageSender,它使用 Java 本身提供的設施。另一種選擇是 HttpComponentsMessageSender,它使用 Apache HttpComponents HttpClient。如果您需要更進階且易於使用的功能(例如驗證、HTTP 連線集區等等),請使用後者。

若要使用 HTTP 傳輸,請將 defaultUri 設定為類似 http://example.com/services 的內容,或為其中一種方法提供 uri 參數。

以下範例示範如何使用 HTTP 傳輸的預設組態

<beans>

    <bean id="messageFactory" class="org.springframework.ws.soap.saaj.SaajSoapMessageFactory"/>

    <bean id="webServiceTemplate" class="org.springframework.ws.client.core.WebServiceTemplate">
        <constructor-arg ref="messageFactory"/>
        <property name="defaultUri" value="http://example.com/WebService"/>
    </bean>

</beans>

以下範例示範如何覆寫預設組態以及如何使用 Apache HttpClient 透過 HTTP 驗證進行驗證

<bean id="webServiceTemplate" class="org.springframework.ws.client.core.WebServiceTemplate">
    <constructor-arg ref="messageFactory"/>
    <property name="messageSender">
        <bean class="org.springframework.ws.transport.http.HttpComponentsMessageSender">
            <property name="credentials">
                <bean class="org.apache.http.auth.UsernamePasswordCredentials">
                    <constructor-arg value="john:secret"/>
                </bean>
            </property>
        </bean>
    </property>
    <property name="defaultUri" value="http://example.com/WebService"/>
</bean>
JMS 傳輸

為了透過 JMS 傳送訊息,Spring Web Services 提供了 JmsMessageSender。此類別使用 Spring Framework 的設施將 WebServiceMessage 轉換為 JMS Message,在 QueueTopic 上傳送它,並接收回應(如果有)。

若要使用 JmsMessageSender,您需要將 defaultUriuri 參數設定為 JMS URI,後者至少包含 jms: 前綴和目的地名稱。JMS URI 的一些範例包括:jms:SomeQueuejms:SomeTopic?priority=3&deliveryMode=NON_PERSISTENTjms:RequestQueue?replyToName=ResponseName。有關此 URI 語法的更多資訊,請參閱 JmsMessageSender 的 Javadoc

預設情況下,JmsMessageSender 會傳送 JMS BytesMessage,但您可以透過使用 JMS URI 上的 messageType 參數來覆寫此設定以使用 TextMessages,例如,jms:Queue?messageType=TEXT_MESSAGE。請注意,BytesMessages 是首選類型,因為 TextMessages 無法可靠地支援附件和字元編碼。

以下範例示範如何在結合 ActiveMQ 連線工廠的情況下使用 JMS 傳輸

<beans>

    <bean id="messageFactory" class="org.springframework.ws.soap.saaj.SaajSoapMessageFactory"/>

    <bean id="connectionFactory" class="org.apache.activemq.ActiveMQConnectionFactory">
        <property name="brokerURL" value="vm://localhost?broker.persistent=false"/>
    </bean>

    <bean id="webServiceTemplate" class="org.springframework.ws.client.core.WebServiceTemplate">
        <constructor-arg ref="messageFactory"/>
        <property name="messageSender">
            <bean class="org.springframework.ws.transport.jms.JmsMessageSender">
                <property name="connectionFactory" ref="connectionFactory"/>
            </bean>
        </property>
        <property name="defaultUri" value="jms:RequestQueue?deliveryMode=NON_PERSISTENT"/>
    </bean>

</beans>
電子郵件傳輸

Spring Web Services 還提供了電子郵件傳輸,您可以使用它透過 SMTP 傳送 Web 服務訊息,並透過 POP3 或 IMAP 擷取它們。用戶端電子郵件功能包含在 MailMessageSender 類別中。此類別從請求 WebServiceMessage 建立電子郵件訊息,並透過 SMTP 傳送它。然後,它會等待回應訊息到達傳入的 POP3 或 IMAP 伺服器。

若要使用 MailMessageSender,請將 defaultUriuri 參數設定為 mailto URI,例如,mailto:[email protected]mailto:server@localhost?subject=SOAP%20Test。請確保訊息傳送器已使用 transportUri 正確組態,後者指示用於傳送請求的伺服器(通常是 SMTP 伺服器),以及 storeUri,後者指示用於輪詢回應的伺服器(通常是 POP3 或 IMAP 伺服器)。

以下範例示範如何使用電子郵件傳輸

<beans>

    <bean id="messageFactory" class="org.springframework.ws.soap.saaj.SaajSoapMessageFactory"/>

    <bean id="webServiceTemplate" class="org.springframework.ws.client.core.WebServiceTemplate">
        <constructor-arg ref="messageFactory"/>
        <property name="messageSender">
            <bean class="org.springframework.ws.transport.mail.MailMessageSender">
                <property name="from" value="Spring-WS SOAP Client &lt;[email protected]&gt;"/>
                <property name="transportUri" value="smtp://client:[email protected]"/>
                <property name="storeUri" value="imap://client:[email protected]/INBOX"/>
            </bean>
        </property>
        <property name="defaultUri" value="mailto:[email protected]?subject=SOAP%20Test"/>
    </bean>

</beans>
XMPP 傳輸

Spring Web Services 2.0 引入了 XMPP (Jabber) 傳輸,您可以使用它透過 XMPP 傳送和接收 Web 服務訊息。用戶端 XMPP 功能包含在 XmppMessageSender 類別中。此類別從請求 WebServiceMessage 建立 XMPP 訊息,並透過 XMPP 傳送它。然後,它會監聽回應訊息的到達。

若要使用 XmppMessageSender,請將 defaultUriuri 參數設定為 xmpp URI,例如,xmpp:[email protected]。傳送器還需要 XMPPConnection 才能工作,後者可以使用 org.springframework.ws.transport.xmpp.support.XmppConnectionFactoryBean 方便地建立。

以下範例示範如何使用 XMPP 傳輸

<beans>

    <bean id="messageFactory" class="org.springframework.ws.soap.saaj.SaajSoapMessageFactory"/>

    <bean id="connection" class="org.springframework.ws.transport.xmpp.support.XmppConnectionFactoryBean">
        <property name="host" value="jabber.org"/>
        <property name="username" value="username"/>
        <property name="password" value="password"/>
    </bean>

    <bean id="webServiceTemplate" class="org.springframework.ws.client.core.WebServiceTemplate">
        <constructor-arg ref="messageFactory"/>
        <property name="messageSender">
            <bean class="org.springframework.ws.transport.xmpp.XmppMessageSender">
                <property name="connection" ref="connection"/>
            </bean>
        </property>
        <property name="defaultUri" value="xmpp:[email protected]"/>
    </bean>

</beans>
訊息工廠

除了訊息傳送器之外,WebServiceTemplate 還需要 Web 服務訊息工廠。SOAP 有兩個訊息工廠:SaajSoapMessageFactoryAxiomSoapMessageFactory。如果未指定訊息工廠(透過設定 messageFactory 屬性),Spring-WS 預設使用 SaajSoapMessageFactory

6.1.2. 傳送和接收 WebServiceMessage

WebServiceTemplate 包含許多便利方法來傳送和接收 Web 服務訊息。有些方法接受並傳回 Source,有些方法傳回 Result。此外,還有一些方法可以將物件 Marshalling 和 Unmarshalling 成 XML。以下範例將簡單的 XML 訊息傳送到 Web 服務

import java.io.StringReader;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;

import org.springframework.ws.WebServiceMessageFactory;
import org.springframework.ws.client.core.WebServiceTemplate;
import org.springframework.ws.transport.WebServiceMessageSender;

public class WebServiceClient {

    private static final String MESSAGE =
        "<message xmlns=\"http://tempuri.org\">Hello, Web Service World</message>";

    private final WebServiceTemplate webServiceTemplate = new WebServiceTemplate();

    public void setDefaultUri(String defaultUri) {
        webServiceTemplate.setDefaultUri(defaultUri);
    }

    // send to the configured default URI
    public void simpleSendAndReceive() {
        StreamSource source = new StreamSource(new StringReader(MESSAGE));
        StreamResult result = new StreamResult(System.out);
        webServiceTemplate.sendSourceAndReceiveToResult(source, result);
    }

    // send to an explicit URI
    public void customSendAndReceive() {
        StreamSource source = new StreamSource(new StringReader(MESSAGE));
        StreamResult result = new StreamResult(System.out);
        webServiceTemplate.sendSourceAndReceiveToResult("http://localhost:8080/AnotherWebService",
            source, result);
    }

}
<beans xmlns="http://www.springframework.org/schema/beans">

    <bean id="webServiceClient" class="WebServiceClient">
        <property name="defaultUri" value="http://localhost:8080/WebService"/>
    </bean>

</beans>

前面的範例使用 WebServiceTemplate 將「Hello, World」訊息傳送到位於 http://localhost:8080/WebService 的 Web 服務(在 simpleSendAndReceive() 方法的情況下),並將結果寫入主控台。WebServiceTemplate 注入了預設 URI,由於 Java 程式碼中未明確提供 URI,因此使用預設 URI。

請注意,WebServiceTemplate 類別在組態完成後是執行緒安全的(假設其所有依賴項也是執行緒安全的,這對於 Spring-WS 附帶的所有依賴項都是如此),因此多個物件可以使用相同的共用 WebServiceTemplate 實例。WebServiceTemplate 公開了一個零引數建構子以及 messageFactorymessageSender Bean 屬性,您可以使用它們來建構實例(透過使用 Spring 容器或純 Java 程式碼)。或者,考慮從 Spring-WS 的 WebServiceGatewaySupport 便利基底類別派生,後者公開了便利的 Bean 屬性以啟用輕鬆組態。(您不必擴展此基底類別。它僅作為便利類別提供。)

6.1.3. 傳送和接收 POJO — Marshalling 和 Unmarshalling

為了方便傳送純 Java 物件,WebServiceTemplate 有許多 send(..) 方法,這些方法將 Object 作為訊息資料內容的引數。WebServiceTemplate 類別中的 marshalSendAndReceive(..) 方法將請求物件轉換為 XML 的工作委派給 Marshaller,並將回應 XML 轉換為物件的工作委派給 Unmarshaller。(有關 Marshalling 和 Unmarshaller 的更多資訊,請參閱 Spring Framework 參考文件。)透過使用 Marshaller,您的應用程式程式碼可以專注於正在傳送或接收的業務物件,而無需擔心它如何表示為 XML 的細節。若要使用 Marshalling 功能,您必須使用 WebServiceTemplate 類別的 marshallerunmarshaller 屬性來設定 Marshaller 和 Unmarshaller。

6.1.4. 使用 WebServiceMessageCallback

為了適應在訊息上設定 SOAP 標頭和其他設定,WebServiceMessageCallback 介面讓您可以在訊息建立後但在傳送之前存取該訊息。以下範例示範如何在使用 Marshalling 物件建立的訊息上設定 SOAP 行動標頭

public void marshalWithSoapActionHeader(MyObject o) {

    webServiceTemplate.marshalSendAndReceive(o, new WebServiceMessageCallback() {

        public void doWithMessage(WebServiceMessage message) {
            ((SoapMessage)message).setSoapAction("http://tempuri.org/Action");
        }
    });
}
請注意,您也可以使用 org.springframework.ws.soap.client.core.SoapActionCallback 來設定 SOAP 行動標頭。
WS-Addressing

除了 伺服器端 WS-Addressing 支援之外,Spring Web Services 在用戶端也支援此規範。

若要在用戶端設定 WS-Addressing 標頭,您可以使用 org.springframework.ws.soap.addressing.client.ActionCallback。此回呼採用所需的行動標頭作為參數。它還具有用於指定 WS-Addressing 版本和 To 標頭的建構子。如果未指定,To 標頭預設為正在建立的連線的 URL。

以下範例將 Action 標頭設定為 http://samples/RequestOrder

webServiceTemplate.marshalSendAndReceive(o, new ActionCallback("http://samples/RequestOrder"));

6.1.5. 使用 WebServiceMessageExtractor

WebServiceMessageExtractor 介面是一個低階回呼介面,您可以完全控制從接收到的 WebServiceMessage 中擷取 Object 的過程。當與服務資源的底層連線仍然開啟時,WebServiceTemplate 會在提供的 WebServiceMessageExtractor 上調用 extractData(..) 方法。以下範例示範了 WebServiceMessageExtractor 的作用

public void marshalWithSoapActionHeader(final Source s) {
    final Transformer transformer = transformerFactory.newTransformer();
    webServiceTemplate.sendAndReceive(new WebServiceMessageCallback() {
        public void doWithMessage(WebServiceMessage message) {
            transformer.transform(s, message.getPayloadResult());
        },
        new WebServiceMessageExtractor() {
            public Object extractData(WebServiceMessage message) throws IOException {
                // do your own transforms with message.getPayloadResult()
                //     or message.getPayloadSource()
            }
          }
        });
}

6.2. 用戶端測試

在測試您的 Web 服務用戶端(即使用 WebServiceTemplate 存取 Web 服務的類別)時,您有兩種可能的方法

  • 編寫單元測試,它會模擬掉 WebServiceTemplate 類別、WebServiceOperations 介面或完整的用戶端類別。

    此方法的優點是它很容易完成。缺點是您沒有真正測試透過網路傳送的 XML 訊息的確切內容,尤其是在模擬掉整個用戶端類別時。

  • 編寫整合測試,它確實測試訊息的內容。

第一種方法可以使用模擬框架(例如 EasyMock、JMock 等)輕鬆完成。下一節重點介紹使用 Spring Web Services 2.0 中引入的測試功能來編寫整合測試。

6.2.1. 編寫用戶端整合測試

Spring Web Services 2.0 引入了對建立 Web 服務用戶端整合測試的支援。在此上下文中,用戶端是使用 WebServiceTemplate 存取 Web 服務的類別。

整合測試支援位於 org.springframework.ws.test.client 套件中。該套件中的核心類別是 MockWebServiceServer。底層思想是,Web 服務範本連線到此模擬伺服器並將請求訊息傳送給它,模擬伺服器然後根據已註冊的期望驗證該訊息。如果滿足期望,則模擬伺服器會準備回應訊息,並將其傳送回範本。

MockWebServiceServer 的典型用法是:。

  1. 透過呼叫 MockWebServiceServer.createServer(WebServiceTemplate)MockWebServiceServer.createServer(WebServiceGatewaySupport)MockWebServiceServer.createServer(ApplicationContext) 來建立 MockWebServiceServer 實例。

  2. 透過呼叫 expect(RequestMatcher) 來設定請求期望,可以透過使用 RequestMatchers 中提供的預設 RequestMatcher 實作(可以靜態匯入)。可以透過鏈式 andExpect(RequestMatcher) 呼叫來設定多個期望。

  3. 透過呼叫 andRespond(ResponseCreator) 來建立適當的回應訊息,可以透過使用 ResponseCreators 中提供的預設 ResponseCreator 實作(可以靜態匯入)。

  4. 像平常一樣使用 WebServiceTemplate,可以直接使用或透過用戶端程式碼使用。

  5. 呼叫 MockWebServiceServer.verify() 以確保所有期望都已滿足。

請注意,MockWebServiceServer(和相關類別)提供了「流暢」API,因此您通常可以使用 IDE 中的程式碼完成功能來引導您完成設定模擬伺服器的過程。
另請注意,您可以依賴 Spring Web Services 中提供的標準記錄功能來進行單元測試。有時,檢查請求或回應訊息以找出特定測試失敗的原因可能會很有用。有關更多資訊,請參閱 訊息記錄和追蹤

例如,考慮以下 Web 服務用戶端類別

import org.springframework.ws.client.core.support.WebServiceGatewaySupport;

public class CustomerClient extends WebServiceGatewaySupport {                          (1)

  public int getCustomerCount() {
    CustomerCountRequest request = new CustomerCountRequest();                          (2)
    request.setCustomerName("John Doe");

    CustomerCountResponse response =
      (CustomerCountResponse) getWebServiceTemplate().marshalSendAndReceive(request);   (3)

    return response.getCustomerCount();
  }

}
1 CustomerClient 擴展了 WebServiceGatewaySupport,後者為其提供了 webServiceTemplate 屬性。
2 CustomerCountRequest 是 Marshaller 支援的物件。例如,它可以具有 @XmlRootElement 註釋以受到 JAXB2 的支援。
3 CustomerClient 使用 WebServiceGatewaySupport 提供的 WebServiceTemplate 將請求物件 Marshalling 成 SOAP 訊息,並將其傳送到 Web 服務。回應物件被 Unmarshalling 成 CustomerCountResponse

以下範例顯示了 CustomerClient 的典型測試

import javax.xml.transform.Source;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.xml.transform.StringSource;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;

import static org.junit.Assert.assertEquals;

import org.springframework.ws.test.client.MockWebServiceServer;                         (1)
import static org.springframework.ws.test.client.RequestMatchers.*;                     (1)
import static org.springframework.ws.test.client.ResponseCreators.*;                    (1)

@RunWith(SpringJUnit4ClassRunner.class)                                                 (2)
@ContextConfiguration("integration-test.xml")                                           (2)
public class CustomerClientIntegrationTest {

  @Autowired
  private CustomerClient client;                                                        (3)

  private MockWebServiceServer mockServer;                                              (4)

  @Before
  public void createServer() throws Exception {
    mockServer = MockWebServiceServer.createServer(client);
  }

  @Test
  public void customerClient() throws Exception {
    Source requestPayload = new StringSource(
      "<customerCountRequest xmlns='http://springframework.org/spring-ws'>" +
        "<customerName>John Doe</customerName>" +
      "</customerCountRequest>");
    Source responsePayload = new StringSource(
      "<customerCountResponse xmlns='http://springframework.org/spring-ws'>" +
        "<customerCount>10</customerCount>" +
      "</customerCountResponse>");

    mockServer.expect(payload(requestPayload)).andRespond(withPayload(responsePayload));(5)

    int result = client.getCustomerCount();                                             (6)
    assertEquals(10, result);                                                           (6)

    mockServer.verify();                                                                (7)
  }

}
1 CustomerClientIntegrationTest 匯入了 MockWebServiceServer 並靜態匯入了 RequestMatchersResponseCreators
2 此測試使用 Spring Framework 中提供的標準測試設施。這不是必需的,但通常是設定測試的最簡單方法。
3 CustomerClientintegration-test.xml 中組態,並使用 @Autowired 連接到此測試中。
4 @Before 方法中,我們透過使用 createServer 工廠方法來建立 MockWebServiceServer
5 我們藉由呼叫 expect() 並搭配由靜態匯入的 RequestMatchers 所提供的 payload() RequestMatcher 來定義期望行為 (請參閱 使用 RequestMatcherRequestMatchers)。

我們也藉由呼叫 andRespond() 並搭配由靜態匯入的 ResponseCreators 所提供的 withPayload() ResponseCreator 來設定回應 (請參閱 使用 ResponseCreatorResponseCreators)。

測試的這部分可能看起來有點令人困惑,但您的 IDE 的程式碼自動完成功能會很有幫助。在您輸入 expect( 後,如果已靜態匯入 RequestMatchers,您的 IDE 可以為您提供可能的請求比對策略列表。這同樣適用於 andRespond(`,前提是您已靜態匯入 `ResponseCreators

6 我們在 CustomerClient 上呼叫 getCustomerCount(),因此使用了 WebServiceTemplate。範本現在已設定為「測試模式」,因此此方法呼叫不會建立真正的 (HTTP) 連線。我們也根據方法呼叫的結果進行了一些 JUnit 斷言。
7 我們在 MockWebServiceServer 上呼叫 verify(),以驗證實際上已收到預期的訊息。

6.2.2. 使用 RequestMatcherRequestMatchers

為了驗證請求訊息是否符合某些期望,MockWebServiceServer 使用 RequestMatcher 策略介面。此介面定義的合約如下

public interface RequestMatcher {

  void match(URI uri,
             WebServiceMessage request)
    throws IOException,
           AssertionError;
}

您可以編寫此介面的自訂實作,在訊息不符合您的期望時拋出 AssertionError 例外,但您當然不必這麼做。RequestMatchers 類別提供了標準的 RequestMatcher 實作,供您在測試中使用。您通常會靜態匯入這個類別。

RequestMatchers 類別提供以下請求比對器

RequestMatchers 方法 描述

anything()

預期任何種類的請求。

payload()

預期給定的請求酬載。

validPayload()

預期請求酬載會根據給定的 XSD 結構描述進行驗證。

xpath()

期望給定的 XPath 運算式存在、不存在或評估為給定的值。

soapHeader()

預期給定的 SOAP 標頭存在於請求訊息中。

connectionTo()

預期連線到給定的 URL。

您可以透過串連 andExpect() 呼叫來設定多個請求期望。

mockServer.expect(connectionTo("http://example.com")).
 andExpect(payload(expectedRequestPayload)).
 andExpect(validPayload(schemaResource)).
 andRespond(...);

有關 RequestMatchers 提供的請求比對器的更多資訊,請參閱 Javadoc

6.2.3. 使用 ResponseCreatorResponseCreators

當請求訊息已驗證並符合定義的期望時,MockWebServiceServer 會建立一個回應訊息,供 WebServiceTemplate 使用。伺服器使用 ResponseCreator 策略介面來完成此目的。

public interface ResponseCreator {

  WebServiceMessage createResponse(URI uri,
                                   WebServiceMessage request,
                                   WebServiceMessageFactory messageFactory)
    throws IOException;

}

同樣地,您可以編寫此介面的自訂實作,使用訊息工廠建立回應訊息,但您當然不必這麼做,因為 ResponseCreators 類別提供了標準的 ResponseCreator 實作,供您在測試中使用。您通常會靜態匯入這個類別。

ResponseCreators 類別提供以下回應

ResponseCreators 方法 描述

withPayload()

建立具有給定酬載的回應訊息。

withError()

在回應連線中建立錯誤。此方法讓您有機會測試錯誤處理。

withException()

從回應連線讀取時拋出例外。此方法讓您有機會測試例外處理。

withMustUnderstandFault()withClientOrSenderFault()withServerOrReceiverFault()withVersionMismatchFault()

建立具有給定 SOAP 錯誤的回應訊息。此方法讓您有機會測試您的錯誤處理。

有關 RequestMatchers 提供的請求比對器的更多資訊,請參閱 Javadoc

7. 使用 Spring-WS 保護您的 Web 服務

本章說明如何將 WS-Security 方面新增至您的 Web 服務。我們專注於 WS-Security 的三個不同領域

  • 身分驗證:這是判斷主體是否為其聲稱身分的過程。在此上下文中,「主體」通常是指使用者、裝置或某些其他可以在您的應用程式中執行動作的系統。

  • 數位簽章:訊息的數位簽章是基於文件和簽署者的私密金鑰的一段資訊。它是透過使用雜湊函數和私密簽署函數(使用簽署者的私密金鑰加密)來建立的。

  • 加密和解密:加密是將資料轉換為一種形式的過程,這種形式在沒有適當金鑰的情況下無法讀取。它主要用於對任何非預期接收者隱藏資訊。解密是加密的相反過程。它是將加密資料轉換回可讀取形式的過程。

這三個領域是透過使用 XwsSecurityInterceptorWss4jSecurityInterceptor 來實作的,我們將在 XwsSecurityInterceptor使用 Wss4jSecurityInterceptor 中分別描述它們

請注意,WS-Security(尤其是加密和簽署)需要大量的記憶體,並可能降低效能。如果效能對您很重要,您可能需要考慮不使用 WS-Security 或使用基於 HTTP 的安全性。

7.1. XwsSecurityInterceptor

XwsSecurityInterceptor 是一個 EndpointInterceptor(請參閱 攔截請求 — EndpointInterceptor 介面),它基於 SUN 的 XML 和 Web 服務安全性套件 (XWSS)。此 WS-Security 實作是 Java Web Services Developer Pack (Java WSDP) 的一部分。

與任何其他端點攔截器一樣,它是在端點對應中定義的(請參閱 端點對應)。這表示您可以有選擇性地新增 WS-Security 支援。某些端點對應需要它,而其他則不需要。

請注意,XWSS 需要 SUN 1.5 JDK 和 SUN SAAJ 參考實作。WSS4J 攔截器沒有這些要求(請參閱 使用 Wss4jSecurityInterceptor)。

XwsSecurityInterceptor 需要一個安全性原則檔案才能運作。這個 XML 檔案告訴攔截器要從傳入的 SOAP 訊息中要求哪些安全性方面,以及要新增哪些方面到傳出的訊息。原則檔案的基本格式在以下章節中說明,但您可以在 此處 找到更深入的教學課程。您可以使用 policyConfiguration 屬性設定原則,這需要 Spring 資源。原則檔案可以包含多個元素 — 例如,要求傳入訊息上的使用者名稱權杖,並簽署所有傳出訊息。它包含一個 SecurityConfiguration 元素(而不是 JAXRPCSecurity 元素)作為其根元素。

此外,安全性攔截器需要一個或多個 CallbackHandler 實例才能運作。這些處理常式用於檢索憑證、私密金鑰、驗證使用者憑證等等。Spring-WS 為大多數常見的安全性問題提供了處理常式 — 例如,針對 Spring Security 驗證管理員進行驗證,以及根據 X509 憑證簽署傳出訊息。以下章節指示針對哪些安全性問題使用哪些回呼處理常式。您可以使用 callbackHandlercallbackHandlers 屬性設定回呼處理常式。

以下範例顯示如何接線 XwsSecurityInterceptor

<beans>
    <bean id="wsSecurityInterceptor"
        class="org.springframework.ws.soap.security.xwss.XwsSecurityInterceptor">
        <property name="policyConfiguration" value="classpath:securityPolicy.xml"/>
        <property name="callbackHandlers">
            <list>
                <ref bean="certificateHandler"/>
                <ref bean="authenticationHandler"/>
            </list>
        </property>
    </bean>
    ...
</beans>

此攔截器是透過使用類別路徑上的 securityPolicy.xml 檔案來設定的。它使用稍後在檔案中定義的兩個回呼處理常式。

7.1.1. 金鑰儲存庫

對於大多數加密操作,您可以使用標準的 java.security.KeyStore 物件。這些操作包括憑證驗證、訊息簽署、簽章驗證和加密。它們不包括使用者名稱和時間戳記驗證。本節旨在為您提供有關金鑰儲存庫和 Java 工具的一些背景知識,您可以使用這些工具將金鑰和憑證儲存在金鑰儲存庫檔案中。此資訊主要與 Spring-WS 無關,而是與 Java 的一般加密功能有關。

java.security.KeyStore 類別表示用於儲存加密金鑰和憑證的設施。它可以包含三種不同的元素

  • 私密金鑰:這些金鑰用於自我驗證。私密金鑰隨附對應公鑰的憑證鏈。在 WS-Security 領域中,這適用於訊息簽署和訊息解密。

  • 對稱金鑰:對稱(或秘密)金鑰也用於訊息加密和解密 — 不同之處在於雙方(傳送者和接收者)共用相同的秘密金鑰。

  • 信任憑證:這些 X509 憑證被稱為「信任憑證」,因為金鑰儲存庫擁有者信任憑證中的公鑰確實屬於憑證的擁有者。在 WS-Security 中,這些憑證用於憑證驗證、簽章驗證和加密。

使用 keytool

keytool 程式是一個金鑰和憑證管理公用程式,隨您的 Java 虛擬機器一起提供。您可以使用此工具建立新的金鑰儲存庫、向其中新增新的私密金鑰和憑證等等。提供 keytool 命令的完整參考超出本文檔的範圍,但您可以在 此處 或透過在命令列上使用 keytool -help 命令找到參考。

使用 KeyStoreFactoryBean

為了使用 Spring 設定輕鬆載入金鑰儲存庫,您可以使用 KeyStoreFactoryBean。它具有資源位置屬性,您可以將其設定為指向要載入的金鑰儲存庫的路徑。可以提供密碼來檢查金鑰儲存庫資料的完整性。如果未提供密碼,則不執行完整性檢查。以下清單設定了 KeyStoreFactoryBean

<bean id="keyStore" class="org.springframework.ws.soap.security.support.KeyStoreFactoryBean">
    <property name="password" value="password"/>
    <property name="location" value="classpath:org/springframework/ws/soap/security/xwss/test-keystore.jks"/>
</bean>
如果您未指定 location 屬性,則會建立一個新的空金鑰儲存庫,這很可能不是您想要的。
KeyStoreCallbackHandler

為了在 XwsSecurityInterceptor 中使用金鑰儲存庫,您需要定義一個 KeyStoreCallbackHandler。此回呼具有三個類型為 keystore 的屬性:(keyStoretrustStoresymmetricStore)。處理常式使用的確切儲存庫取決於此處理常式要執行的加密操作。對於私密金鑰操作,使用 keyStore。對於對稱金鑰操作,使用 symmetricStore。對於判斷信任關係,使用 trustStore。下表指示了這一點

加密操作 使用的金鑰儲存庫

憑證驗證

首先 keyStore,然後 trustStore

基於私密金鑰的解密

keyStore

基於對稱金鑰的解密

symmetricStore

基於公鑰憑證的加密

trustStore

基於對稱金鑰的加密

symmetricStore

簽署

keyStore

簽章驗證

trustStore

此外,KeyStoreCallbackHandler 具有 privateKeyPassword 屬性,應將其設定為解鎖包含在 `keyStore` 中的私密金鑰。

如果未設定 symmetricStore,則預設為 keyStore。如果未設定金鑰或信任儲存庫,則回呼處理常式會使用標準 Java 機制來載入或建立它。請參閱 KeyStoreCallbackHandler 的 JavaDoc 以了解此機制的運作方式。

例如,如果您想要使用 KeyStoreCallbackHandler 來驗證傳入的憑證或簽章,則可以使用信任儲存庫

<beans>
    <bean id="keyStoreHandler" class="org.springframework.ws.soap.security.xwss.callback.KeyStoreCallbackHandler">
        <property name="trustStore" ref="trustStore"/>
    </bean>

    <bean id="trustStore" class="org.springframework.ws.soap.security.support.KeyStoreFactoryBean">
        <property name="location" value="classpath:truststore.jks"/>
        <property name="password" value="changeit"/>
    </bean>
</beans>

如果您想要使用它來解密傳入的憑證或簽署傳出的訊息,則可以使用金鑰儲存庫

<beans>
    <bean id="keyStoreHandler" class="org.springframework.ws.soap.security.xwss.callback.KeyStoreCallbackHandler">
        <property name="keyStore" ref="keyStore"/>
        <property name="privateKeyPassword" value="changeit"/>
    </bean>

    <bean id="keyStore" class="org.springframework.ws.soap.security.support.KeyStoreFactoryBean">
        <property name="location" value="classpath:keystore.jks"/>
        <property name="password" value="changeit"/>
    </bean>
</beans>

以下章節指示可以在何處使用 KeyStoreCallbackHandler 以及針對特定加密操作設定哪些屬性。

7.1.2. 身分驗證

本章簡介中所述,身分驗證是判斷主體是否為其聲稱身分的任務。在 WS-Security 中,身分驗證可以採用兩種形式:使用使用者名稱和密碼權杖(使用純文字密碼或密碼摘要)或使用 X509 憑證。

純文字使用者名稱驗證

最簡單的使用者名稱驗證形式是使用純文字密碼。在這種情況下,SOAP 訊息包含一個 UsernameToken 元素,該元素本身包含一個 Username 元素和一個 Password 元素,其中包含純文字密碼。純文字驗證可以比作 HTTP 伺服器提供的基本驗證。

請注意,純文字密碼不是很安全。因此,如果您使用它們,則應始終為傳輸層新增其他安全措施(例如,使用 HTTPS 而不是純 HTTP)。

為了要求每個傳入訊息都包含一個帶有純文字密碼的 UsernameToken,安全性原則檔案應包含一個 RequireUsernameToken 元素,並將 passwordDigestRequired 屬性設定為 false。您可以在 此處 找到可能的子元素的參考。以下清單顯示如何包含 RequireUsernameToken 元素

<xwss:SecurityConfiguration xmlns:xwss="http://java.sun.com/xml/ns/xwss/config">
    ...
    <xwss:RequireUsernameToken passwordDigestRequired="false" nonceRequired="false"/>
    ...
</xwss:SecurityConfiguration>

如果使用者名稱權杖不存在,則 XwsSecurityInterceptor 會向傳送者傳回 SOAP 錯誤。如果它存在,則會使用 PlainTextPasswordRequest 向已註冊的處理常式觸發 PasswordValidationCallback。在 Spring-WS 中,有三個類別處理此特定回呼。

使用 SimplePasswordValidationCallbackHandler

最簡單的密碼驗證處理常式是 SimplePasswordValidationCallbackHandler。此處理常式根據記憶體中的 Properties 物件驗證密碼,您可以使用 users 屬性指定該物件

<bean id="passwordValidationHandler"
    class="org.springframework.ws.soap.security.xwss.callback.SimplePasswordValidationCallbackHandler">
    <property name="users">
        <props>
            <prop key="Bert">Ernie</prop>
        </props>
    </property>
</bean>

在這種情況下,我們僅允許使用者「Bert」使用密碼「Ernie」登入。

使用 SpringPlainTextPasswordValidationCallbackHandler

SpringPlainTextPasswordValidationCallbackHandler 使用 Spring Security 來驗證使用者身分。描述 Spring Security 超出本文檔的範圍,但它是一個功能齊全的安全性架構。您可以在 Spring Security 參考文檔中閱讀更多相關資訊。

SpringPlainTextPasswordValidationCallbackHandler 需要一個 AuthenticationManager 才能運作。它使用此管理員來驗證針對它建立的 UsernamePasswordAuthenticationToken。如果驗證成功,則權杖會儲存在 SecurityContextHolder 中。您可以使用 authenticationManager 屬性設定驗證管理員

<beans>
  <bean id="springSecurityHandler"
      class="org.springframework.ws.soap.security.xwss.callback.SpringPlainTextPasswordValidationCallbackHandler">
    <property name="authenticationManager" ref="authenticationManager"/>
  </bean>

  <bean id="authenticationManager" class="org.springframework.security.providers.ProviderManager">
      <property name="providers">
          <bean class="org.springframework.security.providers.dao.DaoAuthenticationProvider">
              <property name="userDetailsService" ref="userDetailsService"/>
          </bean>
      </property>
  </bean>

  <bean id="userDetailsService" class="com.mycompany.app.dao.UserDetailService" />
  ...
</beans>
使用 JaasPlainTextPasswordValidationCallbackHandler

JaasPlainTextPasswordValidationCallbackHandler 基於標準的 Java Authentication and Authorization Service。提供 JAAS 的完整介紹超出本文檔的範圍,但有一個 良好的教學課程 可用。

JaasPlainTextPasswordValidationCallbackHandler 僅需要一個 loginContextName 才能運作。它使用此名稱建立一個新的 JAAS LoginContext,並使用 SOAP 訊息中提供的使用者名稱和密碼來處理標準 JAAS NameCallbackPasswordCallback。這表示此回呼處理常式與在 login() 階段觸發這些回呼的任何 JAAS LoginModule 整合,這是標準行為。

您可以如下所示接線 JaasPlainTextPasswordValidationCallbackHandler

<bean id="jaasValidationHandler"
    class="org.springframework.ws.soap.security.xwss.callback.jaas.JaasPlainTextPasswordValidationCallbackHandler">
    <property name="loginContextName" value="MyLoginModule" />
</bean>

在這種情況下,回呼處理常式使用名為 MyLoginModuleLoginContext。此模組應在您的 jaas.config 檔案中定義,如 先前提及的教學課程 中所述。

摘要使用者名稱驗證

當使用密碼摘要時,SOAP 訊息也包含一個 UsernameToken 元素,該元素本身包含一個 Username 元素和一個 Password 元素。不同之處在於密碼不是以純文字形式傳送,而是以摘要形式傳送。接收者將此摘要與他從使用者已知密碼計算出的摘要進行比較,如果它們相同,則使用者通過驗證。此方法可比作 HTTP 伺服器提供的摘要驗證。

為了要求每個傳入訊息都包含一個帶有密碼摘要的 UsernameToken 元素,安全性原則檔案應包含一個 RequireUsernameToken 元素,並將 passwordDigestRequired 屬性設定為 true。此外,nonceRequired 屬性應設定為 true:您可以在 此處 找到可能的子元素的參考。以下清單顯示如何定義 RequireUsernameToken 元素

<xwss:SecurityConfiguration xmlns:xwss="http://java.sun.com/xml/ns/xwss/config">
    ...
    <xwss:RequireUsernameToken passwordDigestRequired="true" nonceRequired="true"/>
    ...
</xwss:SecurityConfiguration>

如果使用者名稱權杖不存在,則 XwsSecurityInterceptor 會向傳送者傳回 SOAP 錯誤。如果它存在,則會使用 DigestPasswordRequest 向已註冊的處理常式觸發 PasswordValidationCallback。在 Spring-WS 中,有兩個類別處理此特定回呼:SimplePasswordValidationCallbackHandlerSpringDigestPasswordValidationCallbackHandler

使用 SimplePasswordValidationCallbackHandler

SimplePasswordValidationCallbackHandler 可以處理純文字密碼以及密碼摘要。它在 使用 SimplePasswordValidationCallbackHandler 中描述。

使用 SpringDigestPasswordValidationCallbackHandler

SpringDigestPasswordValidationCallbackHandler 需要 Spring Security UserDetailService 才能運作。它使用此服務來檢索權杖中指定使用者的密碼。然後將此詳細資訊物件中包含的密碼摘要與訊息中的摘要進行比較。如果它們相等,則使用者已成功通過驗證,並且 UsernamePasswordAuthenticationToken 會儲存在 SecurityContextHolder 中。您可以使用 userDetailsService 屬性設定服務。此外,您可以設定 userCache 屬性,以快取載入的使用者詳細資訊。以下範例顯示如何執行此操作

<beans>
    <bean class="org.springframework.ws.soap.security.xwss.callback.SpringDigestPasswordValidationCallbackHandler">
        <property name="userDetailsService" ref="userDetailsService"/>
    </bean>

    <bean id="userDetailsService" class="com.mycompany.app.dao.UserDetailService" />
    ...
</beans>
憑證驗證

更安全的驗證方式是使用 X509 憑證。在這種情況下,SOAP 訊息包含一個 BinarySecurityToken,其中包含 X509 憑證的 Base 64 編碼版本。接收者使用憑證進行驗證。訊息中儲存的憑證也用於簽署訊息(請參閱 驗證簽章)。

為了確保所有傳入的 SOAP 訊息都攜帶 BinarySecurityToken,安全性原則檔案應包含一個 RequireSignature 元素。此元素還可以進一步攜帶其他元素,這些元素在 驗證簽章 中涵蓋。您可以在 此處 找到可能的子元素的參考。以下清單顯示如何定義 RequireSignature 元素

<xwss:SecurityConfiguration xmlns:xwss="http://java.sun.com/xml/ns/xwss/config">
    ...
    <xwss:RequireSignature requireTimestamp="false">
    ...
</xwss:SecurityConfiguration>

當收到沒有攜帶憑證的訊息時,XwsSecurityInterceptor 會向傳送者傳回 SOAP 錯誤。如果它存在,則會觸發 CertificateValidationCallback。Spring-WS 中的三個處理常式為驗證目的處理此回呼

在大多數情況下,憑證驗證應先於憑證驗證,因為您只想針對有效的憑證進行驗證。應忽略無效的憑證,例如過期日已過的憑證或不在您的信任憑證儲存庫中的憑證。

在 Spring-WS 術語中,這表示 SpringCertificateValidationCallbackHandlerJaasCertificateValidationCallbackHandler 應先於 KeyStoreCallbackHandler。這可以透過在 XwsSecurityInterceptor 的設定中設定 callbackHandlers 屬性的順序來完成

<bean id="wsSecurityInterceptor"
    class="org.springframework.ws.soap.security.xwss.XwsSecurityInterceptor">
    <property name="policyConfiguration" value="classpath:securityPolicy.xml"/>
    <property name="callbackHandlers">
        <list>
            <ref bean="keyStoreHandler"/>
            <ref bean="springSecurityHandler"/>
        </list>
    </property>
</bean>

使用此設定,攔截器首先判斷訊息中的憑證是否有效(透過使用金鑰儲存庫),然後針對其進行驗證。

使用 KeyStoreCallbackHandler

KeyStoreCallbackHandler 使用標準 Java 金鑰儲存庫來驗證憑證。此憑證驗證過程包括以下步驟:。

  1. 處理常式檢查憑證是否在私密 keyStore 中。如果是,則憑證有效。

  2. 如果憑證不在私密金鑰儲存庫中,則處理常式會檢查目前日期和時間是否在憑證中給定的有效期內。如果不是,則憑證無效。如果是,則繼續最後一個步驟。

  3. 為憑證建立憑證路徑。這基本上表示處理常式判斷憑證是否由 trustStore 中的任何憑證授權單位頒發。如果可以成功建立憑證路徑,則憑證有效。否則,憑證無效。

為了將 KeyStoreCallbackHandler 用於憑證驗證目的,您很可能只需要設定 trustStore 屬性

<beans>
    <bean id="keyStoreHandler" class="org.springframework.ws.soap.security.xwss.callback.KeyStoreCallbackHandler">
        <property name="trustStore" ref="trustStore"/>
    </bean>

    <bean id="trustStore" class="org.springframework.ws.soap.security.support.KeyStoreFactoryBean">
        <property name="location" value="classpath:truststore.jks"/>
        <property name="password" value="changeit"/>
    </bean>
</beans>

使用先前範例中顯示的設定,要驗證的憑證必須在信任儲存庫本身中,或者信任儲存庫必須包含頒發憑證的憑證授權單位。

使用 SpringCertificateValidationCallbackHandler

SpringCertificateValidationCallbackHandler 需要 Spring Security AuthenticationManager 才能運作。它使用此管理員來驗證針對它建立的 X509AuthenticationToken。設定的驗證管理員預期提供可以處理此權杖的提供者(通常是 X509AuthenticationProvider 的實例)。如果驗證成功,則權杖會儲存在 SecurityContextHolder 中。您可以使用 authenticationManager 屬性設定驗證管理員

<beans>
    <bean id="springSecurityCertificateHandler"
        class="org.springframework.ws.soap.security.xwss.callback.SpringCertificateValidationCallbackHandler">
        <property name="authenticationManager" ref="authenticationManager"/>
    </bean>

    <bean id="authenticationManager"
        class="org.springframework.security.providers.ProviderManager">
        <property name="providers">
            <bean class="org.springframework.ws.soap.security.x509.X509AuthenticationProvider">
                <property name="x509AuthoritiesPopulator">
                    <bean class="org.springframework.ws.soap.security.x509.populator.DaoX509AuthoritiesPopulator">
                        <property name="userDetailsService" ref="userDetailsService"/>
                    </bean>
                </property>
            </bean>
        </property>
    </bean>

  <bean id="userDetailsService" class="com.mycompany.app.dao.UserDetailService" />
  ...
</beans>

在這種情況下,我們使用自訂使用者詳細資訊服務來取得基於憑證的驗證詳細資訊。有關針對 X509 憑證進行驗證的更多資訊,請參閱 Spring Security 參考文檔

使用 JaasCertificateValidationCallbackHandler

JaasCertificateValidationCallbackHandler 需要 loginContextName 才能運作。它使用此名稱和憑證的 X500Principal 建立一個新的 JAAS LoginContext。這表示此回呼處理常式與任何處理 X500 主體的 JAAS LoginModule 整合。

您可以如下所示接線 JaasCertificateValidationCallbackHandler

<bean id="jaasValidationHandler"
    class="org.springframework.ws.soap.security.xwss.callback.jaas.JaasCertificateValidationCallbackHandler">
    <property name="loginContextName">MyLoginModule</property>
</bean>

在這種情況下,回呼處理常式使用名為 MyLoginModuleLoginContext。此模組應在您的 jaas.config 檔案中定義,並且應該能夠針對 X500 主體進行驗證。

7.1.3. 數位簽章

訊息的數位簽章是基於文件和簽署者的私密金鑰的一段資訊。在 WS-Security 中,與簽章相關的兩個主要任務是:驗證簽章和簽署訊息。

驗證簽章

基於憑證的驗證一樣,已簽署的訊息包含一個 BinarySecurityToken,其中包含用於簽署訊息的憑證。此外,它還包含一個 SignedInfo 區塊,指示訊息的哪個部分已簽署。

為了確保所有傳入的 SOAP 訊息都攜帶 BinarySecurityToken,安全性原則檔案應包含一個 RequireSignature 元素。它也可以包含一個 SignatureTarget 元素,該元素指定預期要簽署的目標訊息部分以及各種其他子元素。您也可以定義要使用的私密金鑰別名、是否使用對稱金鑰而不是私密金鑰以及許多其他屬性。您可以在 此處 找到可能的子元素的參考。以下清單設定了 RequireSignature 元素

<xwss:SecurityConfiguration xmlns:xwss="http://java.sun.com/xml/ns/xwss/config">
    <xwss:RequireSignature requireTimestamp="false"/>
</xwss:SecurityConfiguration>

如果簽章不存在,則 XwsSecurityInterceptor 會向傳送者傳回 SOAP 錯誤。如果它存在,則會向已註冊的處理常式觸發 SignatureVerificationKeyCallback。在 Spring-WS 中,一個類別處理此特定回呼:KeyStoreCallbackHandler

使用 KeyStoreCallbackHandler

KeyStoreCallbackHandler 中所述,KeyStoreCallbackHandler 使用 java.security.KeyStore 來處理各種加密回呼,包括簽章驗證。對於簽章驗證,處理常式使用 trustStore 屬性

<beans>
    <bean id="keyStoreHandler" class="org.springframework.ws.soap.security.xwss.callback.KeyStoreCallbackHandler">
        <property name="trustStore" ref="trustStore"/>
    </bean>

    <bean id="trustStore" class="org.springframework.ws.soap.security.support.KeyStoreFactoryBean">
        <property name="location" value="classpath:org/springframework/ws/soap/security/xwss/test-truststore.jks"/>
        <property name="password" value="changeit"/>
    </bean>
</beans>
簽署訊息

當簽署訊息時,XwsSecurityInterceptor 會將 BinarySecurityToken 新增至訊息。它還會新增一個 SignedInfo 區塊,指示訊息的哪個部分已簽署。

為了簽署所有傳出的 SOAP 訊息,安全性原則檔案應包含一個 Sign 元素。它也可以包含一個 SignatureTarget 元素,該元素指定預期要簽署的目標訊息部分以及各種其他子元素。您也可以定義要使用的私密金鑰別名、是否使用對稱金鑰而不是私密金鑰以及許多其他屬性。您可以在 此處 找到可能的子元素的參考。以下範例包含 Sign 元素

<xwss:SecurityConfiguration xmlns:xwss="http://java.sun.com/xml/ns/xwss/config">
	<xwss:Sign includeTimestamp="false" />
</xwss:SecurityConfiguration>

XwsSecurityInterceptor 會向已註冊的處理常式觸發 SignatureKeyCallback。在 Spring-WS 中,KeyStoreCallbackHandler 類別處理此特定回呼。

使用 KeyStoreCallbackHandler

KeyStoreCallbackHandler 中所述,KeyStoreCallbackHandler 使用 java.security.KeyStore 來處理各種加密回呼,包括簽署訊息。對於新增簽章,處理常式使用 keyStore 屬性。此外,您必須設定 privateKeyPassword 屬性以解鎖用於簽署的私密金鑰。以下範例使用 KeyStoreCallbackHandler

<beans>
    <bean id="keyStoreHandler" class="org.springframework.ws.soap.security.xwss.callback.KeyStoreCallbackHandler">
        <property name="keyStore" ref="keyStore"/>
        <property name="privateKeyPassword" value="changeit"/>
    </bean>

    <bean id="keyStore" class="org.springframework.ws.soap.security.support.KeyStoreFactoryBean">
        <property name="location" value="classpath:keystore.jks"/>
        <property name="password" value="changeit"/>
    </bean>
</beans>

7.1.4. 解密和加密

加密時,訊息會轉換為一種形式,只有使用適當的金鑰才能讀取。訊息可以解密以顯示原始的可讀訊息。

解密

為了解密傳入的 SOAP 訊息,安全性原則檔案應包含一個 RequireEncryption 元素。此元素可以進一步攜帶一個 EncryptionTarget 元素,該元素指示訊息的哪個部分應加密,以及一個 SymmetricKey,以指示應使用共用秘密而不是常規私密金鑰來解密訊息。您可以在 此處 閱讀其他元素的描述。以下範例使用 RequireEncryption 元素

<xwss:SecurityConfiguration xmlns:xwss="http://java.sun.com/xml/ns/xwss/config">
    <xwss:RequireEncryption />
</xwss:SecurityConfiguration>

如果傳入的訊息未加密,則 XwsSecurityInterceptor 會向傳送者傳回 SOAP 錯誤。如果它存在,則會向已註冊的處理常式觸發 DecryptionKeyCallback。在 Spring-WS 中,KeyStoreCallbackHandler 類別處理此特定回呼。

使用 KeyStoreCallbackHandler

KeyStoreCallbackHandler 中所述,KeyStoreCallbackHandler 使用 java.security.KeyStore 來處理各種加密回呼,包括解密。對於解密,處理常式使用 keyStore 屬性。此外,您必須設定 privateKeyPassword 屬性以解鎖用於解密的私密金鑰。對於基於對稱金鑰的解密,它使用 symmetricStore。以下範例使用 KeyStoreCallbackHandler

<beans>
    <bean id="keyStoreHandler" class="org.springframework.ws.soap.security.xwss.callback.KeyStoreCallbackHandler">
        <property name="keyStore" ref="keyStore"/>
        <property name="privateKeyPassword" value="changeit"/>
    </bean>

    <bean id="keyStore" class="org.springframework.ws.soap.security.support.KeyStoreFactoryBean">
        <property name="location" value="classpath:keystore.jks"/>
        <property name="password" value="changeit"/>
    </bean>
</beans>
加密

為了加密傳出的 SOAP 訊息,安全性原則檔案應包含一個 Encrypt 元素。此元素可以進一步攜帶一個 EncryptionTarget 元素,該元素指示訊息的哪個部分應加密,以及一個 SymmetricKey,以指示應使用共用秘密而不是常規公鑰來加密訊息。您可以在 此處 閱讀其他元素的描述。以下範例使用 Encrypt 元素

<xwss:SecurityConfiguration xmlns:xwss="http://java.sun.com/xml/ns/xwss/config">
    <xwss:Encrypt />
</xwss:SecurityConfiguration>

XwsSecurityInterceptor 會向已註冊的處理常式觸發 EncryptionKeyCallback,以檢索加密資訊。在 Spring-WS 中,KeyStoreCallbackHandler 類別處理此特定回呼。

使用 KeyStoreCallbackHandler

KeyStoreCallbackHandler 中所述,KeyStoreCallbackHandler 使用 java.security.KeyStore 來處理各種加密回呼,包括加密。對於基於公鑰的加密,處理常式使用 trustStore 屬性。對於基於對稱金鑰的加密,它使用 symmetricStore。以下範例使用 KeyStoreCallbackHandler

<beans>
    <bean id="keyStoreHandler" class="org.springframework.ws.soap.security.xwss.callback.KeyStoreCallbackHandler">
        <property name="trustStore" ref="trustStore"/>
    </bean>

    <bean id="trustStore" class="org.springframework.ws.soap.security.support.KeyStoreFactoryBean">
        <property name="location" value="classpath:truststore.jks"/>
        <property name="password" value="changeit"/>
    </bean>
</beans>

7.1.5. 安全性例外處理

當安全或驗證操作失敗時,XwsSecurityInterceptor 分別拋出 WsSecuritySecurementExceptionWsSecurityValidationException。這些例外會繞過標準例外處理機制,但由攔截器本身處理。

WsSecuritySecurementException 例外由 XwsSecurityInterceptorhandleSecurementException 方法處理。預設情況下,此方法會記錄錯誤並停止進一步處理訊息。

同樣地,WsSecurityValidationException 例外由 XwsSecurityInterceptorhandleValidationException 方法處理。預設情況下,此方法會建立 SOAP 1.1 Client 或 SOAP 1.2 sender 錯誤,並將其作為回應傳回。

handleSecurementExceptionhandleValidationException 都是受保護的方法,您可以覆寫它們以變更其預設行為。

7.2. 使用 Wss4jSecurityInterceptor

Wss4jSecurityInterceptor 是一個 EndpointInterceptor(請參閱 攔截請求 — EndpointInterceptor 介面),它基於 Apache 的 WSS4J

WSS4J 實作以下標準

  • OASIS Web Services Security: SOAP Message Security 1.0 Standard 200401, March 2004

  • Username Token profile V1.0

  • X.509 Token Profile V1.0

此攔截器支援由 AxiomSoapMessageFactorySaajSoapMessageFactory 建立的訊息。

7.2.1. 設定 Wss4jSecurityInterceptor

WSS4J 不使用外部設定檔。攔截器完全由屬性設定。由此攔截器叫用的驗證和安全操作分別透過 validationActionssecurementActions 屬性指定。動作以空格分隔的字串傳遞。以下清單顯示了組態範例

<bean class="org.springframework.ws.soap.security.wss4j.Wss4jSecurityInterceptor">
    <property name="validationActions" value="UsernameToken Encrypt"/>
    ...
    <property name="securementActions" value="Encrypt"/>
    ...
</bean>

下表顯示了可用的驗證動作

驗證動作 描述

UsernameToken

驗證使用者名稱權杖

Timestamp

驗證時間戳記

Encrypt

解密訊息

Signature

驗證簽章

NoSecurity

不執行任何動作

下表顯示了可用的安全操作

安全操作 描述

UsernameToken

新增使用者名稱權杖

UsernameTokenSignature

新增使用者名稱權杖和簽章使用者名稱權杖秘密金鑰

Timestamp

新增時間戳記

Encrypt

加密回應

Signature

簽署回應

NoSecurity

不執行任何動作

動作的順序非常重要,並由攔截器強制執行。如果其安全性動作執行的順序與 `validationActions` 指定的順序不同,攔截器將拒絕傳入的 SOAP 訊息。

7.2.2. 數位憑證處理

對於需要與金鑰儲存庫或憑證處理互動的加密操作(簽章、加密和解密操作),WSS4J 需要一個 `org.apache.ws.security.components.crypto.Crypto` 的實例。

`Crypto` 實例可以從 WSS4J 的 `CryptoFactory` 取得,或者更方便地使用 Spring-WS 的 `CryptoFactoryBean`。

CryptoFactoryBean

Spring-WS 提供了一個方便的 factory bean,CryptoFactoryBean,它通過強型別屬性(首選)或通過 Properties 物件來建構和配置 Crypto 實例。

預設情況下,CryptoFactoryBean 返回 `org.apache.ws.security.components.crypto.Merlin` 的實例。您可以通過設定 cryptoProvider 屬性(或其等效的 `org.apache.ws.security.crypto.provider` 字串屬性)來更改此設定。

以下範例配置使用 CryptoFactoryBean

<bean class="org.springframework.ws.soap.security.wss4j.support.CryptoFactoryBean">
    <property name="keyStorePassword" value="mypassword"/>
    <property name="keyStoreLocation" value="file:/path_to_keystore/keystore.jks"/>
</bean>

7.2.3. 身份驗證

本節討論如何使用 Wss4jSecurityInterceptor 進行身份驗證。

驗證使用者名稱令牌

Spring-WS 提供了一組回呼處理程序,用於與 Spring Security 整合。此外,還提供了一個簡單的回呼處理程序,SimplePasswordValidationCallbackHandler,用於使用記憶體中的 Properties 物件配置使用者和密碼。

回呼處理程序通過 Wss4jSecurityInterceptor 屬性的 validationCallbackHandler 進行配置。

使用 SimplePasswordValidationCallbackHandler

SimplePasswordValidationCallbackHandler 針對記憶體中的 Properties 物件驗證純文字和摘要使用者名稱令牌。您可以如下配置它

<bean id="callbackHandler"
    class="org.springframework.ws.soap.security.wss4j.callback.SimplePasswordValidationCallbackHandler">
    <property name="users">
        <props>
            <prop key="Bert">Ernie</prop>
        </props>
    </property>
</bean>
使用 SpringSecurityPasswordValidationCallbackHandler

SpringSecurityPasswordValidationCallbackHandler 通過使用 Spring Security UserDetailService 進行操作來驗證純文字和摘要密碼。它使用此服務來檢索令牌中指定使用者的密碼(或密碼摘要)。然後將此詳細資訊物件中包含的密碼(或密碼摘要)與訊息中的摘要進行比較。如果它們相等,則使用者已成功通過身份驗證,並且 UsernamePasswordAuthenticationToken 儲存在 `SecurityContextHolder` 中。您可以使用 userDetailsService 設定服務。此外,您可以設定 userCache 屬性,以快取已載入的使用者詳細資訊,如下所示

<beans>
    <bean class="org.springframework.ws.soap.security.wss4j.callback.SpringDigestPasswordValidationCallbackHandler">
        <property name="userDetailsService" ref="userDetailsService"/>
    </bean>

    <bean id="userDetailsService" class="com.mycompany.app.dao.UserDetailService" />
    ...
</beans>
新增使用者名稱令牌

將使用者名稱令牌新增到傳出訊息就像將 UsernameToken 新增到 Wss4jSecurityInterceptorsecurementActions 屬性,並指定 securementUsername 和 `securementPassword` 一樣簡單。

密碼類型可以通過設定 securementPasswordType 屬性來設定。可能的值為 PasswordText(用於純文字密碼)或 PasswordDigest(用於摘要密碼),這是預設值。

以下範例產生具有摘要密碼的使用者名稱令牌

<bean class="org.springframework.ws.soap.security.wss4j.Wss4jSecurityInterceptor">
    <property name="securementActions" value="UsernameToken"/>
    <property name="securementUsername" value="Ernie"/>
    <property name="securementPassword" value="Bert"/>
</bean>

如果選擇了純文字密碼類型,則可以通過設定 securementUsernameTokenElements 屬性來指示攔截器新增 NonceCreated 元素。該值必須是一個列表,其中包含以空格分隔的所需元素名稱(區分大小寫)。

以下範例產生具有純文字密碼、NonceCreated 元素的使用者名稱令牌

<bean class="org.springframework.ws.soap.security.wss4j.Wss4jSecurityInterceptor">
    <property name="securementActions" value="UsernameToken"/>
    <property name="securementUsername" value="Ernie"/>
    <property name="securementPassword" value="Bert"/>
    <property name="securementPasswordType" value="PasswordText"/>
    <property name="securementUsernameTokenElements" value="Nonce Created"/>
</bean>
憑證驗證

由於憑證驗證類似於數位簽章,WSS4J 將其作為簽章驗證和保護的一部分進行處理。具體來說,必須將 securementSignatureKeyIdentifier 屬性設定為 DirectReference,以指示 WSS4J 生成包含 X509 憑證的 BinarySecurityToken 元素,並將其包含在傳出訊息中。憑證的名稱和密碼分別通過 securementUsernamesecurementPassword 屬性傳遞,如下例所示

<bean class="org.springframework.ws.soap.security.wss4j.Wss4jSecurityInterceptor">
    <property name="securementActions" value="Signature"/>
    <property name="securementSignatureKeyIdentifier" value="DirectReference"/>
    <property name="securementUsername" value="mycert"/>
    <property name="securementPassword" value="certpass"/>
    <property name="securementSignatureCrypto">
      <bean class="org.springframework.ws.soap.security.wss4j.support.CryptoFactoryBean">
        <property name="keyStorePassword" value="123456"/>
        <property name="keyStoreLocation" value="classpath:/keystore.jks"/>
      </bean>
    </property>
</bean>

對於憑證驗證,常規簽章驗證適用

<bean class="org.springframework.ws.soap.security.wss4j.Wss4jSecurityInterceptor">
    <property name="validationActions" value="Signature"/>
    <property name="validationSignatureCrypto">
      <bean class="org.springframework.ws.soap.security.wss4j.support.CryptoFactoryBean">
        <property name="keyStorePassword" value="123456"/>
        <property name="keyStoreLocation" value="classpath:/keystore.jks"/>
      </bean>
    </property>
</bean>

在驗證結束時,攔截器會自動委託給預設的 WSS4J 實作來驗證憑證的有效性。如果需要,您可以通過重新定義 verifyCertificateTrust 方法來更改此行為。

有關更多詳細資訊,請參閱 數位簽章

7.2.4. 安全時間戳記

本節介紹 Wss4jSecurityInterceptor 中可用的各種時間戳記選項。

驗證時間戳記

要驗證時間戳記,請將 Timestamp 新增到 validationActions 屬性。您可以通過將 timestampStrict 設定為 true 並通過設定 timeToLive 屬性來指定伺服器端存活時間(預設值:300 秒),來覆蓋 SOAP 訊息發起者指定的時間戳記語義。攔截器始終拒絕已過期的時間戳記,無論 timeToLive 的值是多少。

在以下範例中,攔截器將時間戳記有效性視窗限制為 10 秒,拒絕該視窗之外的任何有效時間戳記令牌

<bean class="org.springframework.ws.soap.security.wss4j.Wss4jSecurityInterceptor">
    <property name="validationActions" value="Timestamp"/>
    <property name="timestampStrict" value="true"/>
    <property name="timeToLive" value="10"/>
</bean>
新增時間戳記

Timestamp 新增到 securementActions 屬性會在傳出訊息中產生時間戳記標頭。timestampPrecisionInMilliseconds 屬性指定產生時間戳記的精度是否以毫秒為單位。預設值為 true。以下列表新增了一個時間戳記

<bean class="org.springframework.ws.soap.security.wss4j.Wss4jSecurityInterceptor">
    <property name="securementActions" value="Timestamp"/>
    <property name="timestampPrecisionInMilliseconds" value="true"/>
</bean>

7.2.5. 數位簽章

本節介紹 Wss4jSecurityInterceptor 中可用的各種簽章選項。

驗證簽章

要指示 Wss4jSecurityInterceptorvalidationActions 必須包含 Signature 動作。此外,validationSignatureCrypto 屬性必須指向包含發起者公用憑證的金鑰儲存庫

<bean id="wsSecurityInterceptor" class="org.springframework.ws.soap.security.wss4j.Wss4jSecurityInterceptor">
    <property name="validationActions" value="Signature"/>
    <property name="validationSignatureCrypto">
        <bean class="org.springframework.ws.soap.security.wss4j.support.CryptoFactoryBean">
            <property name="keyStorePassword" value="123456"/>
            <property name="keyStoreLocation" value="classpath:/keystore.jks"/>
        </bean>
    </property>
</bean>
簽署訊息

通過將 Signature 動作新增到 securementActions 來啟用簽署傳出訊息。要使用的私鑰別名和密碼分別由 securementUsernamesecurementPassword 屬性指定。securementSignatureCrypto 必須指向包含私鑰的金鑰儲存庫

<bean class="org.springframework.ws.soap.security.wss4j.Wss4jSecurityInterceptor">
    <property name="securementActions" value="Signature"/>
    <property name="securementUsername" value="mykey"/>
    <property name="securementPassword" value="123456"/>
    <property name="securementSignatureCrypto">
        <bean class="org.springframework.ws.soap.security.wss4j.support.CryptoFactoryBean">
            <property name="keyStorePassword" value="123456"/>
            <property name="keyStoreLocation" value="classpath:/keystore.jks"/>
        </bean>
    </property>
</bean>

此外,您可以通過設定 securementSignatureAlgorithm 屬性來定義簽章演算法。

您可以通過設定 securementSignatureKeyIdentifier 屬性來自訂要使用的金鑰識別碼類型。只有 IssuerSerialDirectReference 對於簽章有效。

securementSignatureParts 屬性控制訊息的哪個部分被簽署。此屬性的值是以分號分隔的元素名稱列表,用於識別要簽署的元素。簽章部分的通用形式是 {}{namespace}Element。請注意,第一個空括號僅用於加密部分。預設行為是簽署 SOAP body。

以下範例示範如何簽署 Spring Web Services echo 範例中的 echoResponse 元素

<property name="securementSignatureParts"
    value="{}{http://www.springframework.org/spring-ws/samples/echo}echoResponse"/>

要指定沒有命名空間的元素,請使用字串 Null(區分大小寫)作為命名空間名稱。

如果請求中沒有其他元素的本機名稱為 Body,則 SOAP 命名空間識別碼可以為空 ({})。

簽章確認

通過將 enableSignatureConfirmation 設定為 true 來啟用簽章確認。請注意,簽章確認動作跨越請求和回應。這表示即使沒有對應的安全性動作,也必須將 secureResponsevalidateRequest 設定為 true(這是預設值)。以下範例將 enableSignatureConfirmation 屬性設定為 true

<bean class="org.springframework.ws.soap.security.wss4j.Wss4jSecurityInterceptor">
    <property name="validationActions" value="Signature"/>
    <property name="enableSignatureConfirmation" value="true"/>
    <property name="validationSignatureCrypto">
        <bean class="org.springframework.ws.soap.security.wss4j.support.CryptoFactoryBean">
            <property name="keyStorePassword" value="123456"/>
            <property name="keyStoreLocation" value="file:/keystore.jks"/>
        </bean>
    </property>
</bean>

7.2.6. 解密和加密

本節介紹 Wss4jSecurityInterceptor 中可用的各種解密和加密選項。

解密

解密傳入的 SOAP 訊息需要將 Encrypt 動作新增到 validationActions 屬性。其餘的配置取決於訊息中顯示的金鑰資訊。(這是因為 WSS4J 只需要一個 Crypto 用於加密金鑰,而嵌入式金鑰名稱驗證委託給回呼處理程序。)

要解密具有嵌入式加密對稱金鑰(xenc:EncryptedKey 元素)的訊息,validationDecryptionCrypto 需要指向包含解密私鑰的金鑰儲存庫。此外,validationCallbackHandler 必須注入一個 org.springframework.ws.soap.security.wss4j.callback.KeyStoreCallbackHandler,該處理程序指定金鑰的密碼

<bean class="org.springframework.ws.soap.security.wss4j.Wss4jSecurityInterceptor">
    <property name="validationActions" value="Encrypt"/>
    <property name="validationDecryptionCrypto">
        <bean class="org.springframework.ws.soap.security.wss4j.support.CryptoFactoryBean">
            <property name="keyStorePassword" value="123456"/>
            <property name="keyStoreLocation" value="classpath:/keystore.jks"/>
        </bean>
    </property>
    <property name="validationCallbackHandler">
        <bean class="org.springframework.ws.soap.security.wss4j.callback.KeyStoreCallbackHandler">
            <property name="privateKeyPassword" value="mykeypass"/>
        </bean>
    </property>
</bean>

為了支援解密具有嵌入式金鑰名稱(ds:KeyName 元素)的訊息,您可以配置一個 KeyStoreCallbackHandler,該處理程序指向具有對稱密鑰的金鑰儲存庫。symmetricKeyPassword 屬性指示金鑰的密碼,金鑰名稱是 ds:KeyName 元素指定的名稱

<bean class="org.springframework.ws.soap.security.wss4j.Wss4jSecurityInterceptor">
    <property name="validationActions" value="Encrypt"/>
    <property name="validationCallbackHandler">
        <bean class="org.springframework.ws.soap.security.wss4j.callback.KeyStoreCallbackHandler">
            <property name="keyStore">
                <bean class="org.springframework.ws.soap.security.support.KeyStoreFactoryBean">
                    <property name="location" value="classpath:keystore.jks"/>
                    <property name="type" value="JCEKS"/>
                    <property name="password" value="123456"/>
                </bean>
            </property>
            <property name="symmetricKeyPassword" value="mykeypass"/>
        </bean>
    </property>
</bean>
加密

Encrypt 新增到 securementActions 會啟用傳出訊息的加密。您可以通過設定 securementEncryptionUser 屬性來設定用於加密的憑證別名。憑證所在的金鑰儲存庫通過 securementEncryptionCrypto 屬性存取。由於加密依賴於公用憑證,因此無需傳遞密碼。以下範例使用 securementEncryptionCrypto 屬性

<bean class="org.springframework.ws.soap.security.wss4j.Wss4jSecurityInterceptor">
    <property name="securementActions" value="Encrypt"/>
    <property name="securementEncryptionUser" value="mycert"/>
    <property name="securementEncryptionCrypto">
        <bean class="org.springframework.ws.soap.security.wss4j.support.CryptoFactoryBean">
            <property name="keyStorePassword" value="123456"/>
            <property name="keyStoreLocation" value="file:/keystore.jks"/>
        </bean>
    </property>
</bean>

您可以通過多種方式自訂加密:要使用的金鑰識別碼類型由 securementEncryptionKeyIdentifier 屬性定義。可能的值為 IssuerSerialX509KeyIdentifierDirectReferenceThumbprintSKIKeyIdentifierEmbeddedKeyName

如果您選擇 EmbeddedKeyName 類型,則需要指定用於加密的秘密金鑰。金鑰的別名在 securementEncryptionUser 屬性中設定,與其他金鑰識別碼類型相同。但是,WSS4J 需要一個回呼處理程序來獲取秘密金鑰。因此,您必須為 securementCallbackHandler 提供一個指向適當金鑰儲存庫的 KeyStoreCallbackHandler。預設情況下,產生的 WS-Security 標頭中的 ds:KeyName 元素採用 securementEncryptionUser 屬性的值。要指示不同的名稱,您可以將 securementEncryptionEmbeddedKeyName 設定為所需的值。在下一個範例中,傳出訊息使用別名為 secretKey 的金鑰加密,而 myKey 出現在 ds:KeyName 元素中

<bean class="org.springframework.ws.soap.security.wss4j.Wss4jSecurityInterceptor">
    <property name="securementActions" value="Encrypt"/>
    <property name="securementEncryptionKeyIdentifier" value="EmbeddedKeyName"/>
    <property name="securementEncryptionUser" value="secretKey"/>
    <property name="securementEncryptionEmbeddedKeyName" value="myKey"/>
    <property name="securementCallbackHandler">
        <bean class="org.springframework.ws.soap.security.wss4j.callback.KeyStoreCallbackHandler">
            <property name="symmetricKeyPassword" value="keypass"/>
            <property name="keyStore">
                <bean class="org.springframework.ws.soap.security.support.KeyStoreFactoryBean">
                    <property name="location" value="file:/keystore.jks"/>
                    <property name="type" value="jceks"/>
                    <property name="password" value="123456"/>
                </bean>
            </property>
        </bean>
    </property>
</bean>

securementEncryptionKeyTransportAlgorithm 屬性定義用於加密產生的對稱金鑰的演算法。支援的值為 http://www.w3.org/2001/04/xmlenc#rsa-1_5(預設值)和 http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p

您可以通過設定 securementEncryptionSymAlgorithm 屬性來設定要使用的對稱加密演算法。支援的值為 http://www.w3.org/2001/04/xmlenc#aes128-cbc(預設值)、http://www.w3.org/2001/04/xmlenc#tripledes-cbchttp://www.w3.org/2001/04/xmlenc#aes256-cbchttp://www.w3.org/2001/04/xmlenc#aes192-cbc

最後,securementEncryptionParts 屬性定義訊息的哪些部分被加密。此屬性的值是以分號分隔的元素名稱列表,用於識別要加密的元素。加密模式指定符和命名空間識別,每個都在一對大括號內,可以放在每個元素名稱之前。加密模式指定符是 {Content}{Element}。請參閱 W3C XML 加密規範,了解元素加密和內容加密之間的差異。以下範例識別了 echo 範例中的 echoResponse

<property name="securementEncryptionParts"
    value="{Content}{http://www.springframework.org/spring-ws/samples/echo}echoResponse"/>

請注意,元素名稱、命名空間識別碼和加密修飾符區分大小寫。您可以省略加密修飾符和命名空間識別碼。如果這樣做,加密模式預設為 Content,命名空間設定為 SOAP 命名空間。

要指定沒有命名空間的元素,請使用值 Null(區分大小寫)作為命名空間名稱。如果未指定列表,則處理程序預設在 Content 模式下加密 SOAP Body。

7.2.7. 安全性例外處理

Wss4jSecurityInterceptor 的例外處理與 XwsSecurityInterceptor 的例外處理相同。有關更多資訊,請參閱 安全性例外處理

III. 其他資源

除了本參考文件外,許多其他資源可能幫助您學習如何使用 Spring Web Services。本節列舉了這些額外的第三方資源。

參考書目

  • [waldo-94] Jim Waldo、Ann Wollrath 和 Sam Kendall。分散式運算注意事項。Springer Verlag。1994 年

  • [alpine] Steve Loughran & Edmund Smith。重新思考 Java SOAP 堆疊。2005 年 5 月 17 日。© 2005 IEEE 電話實驗室公司。

  • [effective-enterprise-java] Ted Neward。Scott Meyers。Effective Enterprise Java。Addison-Wesley。2004 年

  • [effective-xml] Elliotte Rusty Harold。Scott Meyers。Effective XML。Addison-Wesley。2004 年