第 6 章. 在客戶端使用 Spring Web Services

6.1. 簡介

Spring-WS 提供了一個客戶端 Web 服務 API,允許一致地透過 XML 訪問 Web 服務。它也支援使用 marshaller 和 unmarshaller,這樣你的服務層程式碼就可以完全處理 Java 物件。

org.springframework.ws.client.core 包提供了使用客戶端訪問 API 的核心功能。它包含簡化 Web 服務使用的模板類,很像 Spring 核心的 JdbcTemplate 之於 JDBC。Spring 模板類共同的設計原則是提供輔助方法來執行常見操作,對於更復雜的使用,則委託給使用者實現的 callback 介面。Web 服務模板遵循相同的設計。這些類提供了傳送和接收 XML 訊息、在傳送前將物件 marshalling 到 XML,以及支援多種傳輸選項的各種便利方法。

6.2. 使用客戶端 API

6.2.1. WebServiceTemplate

WebServiceTemplate 是 Spring-WS 中客戶端 Web 服務訪問的核心類。它包含傳送 Source 物件,以及接收響應訊息作為 SourceResult 的方法。此外,它可以在透過傳輸傳送物件之前將物件 marshalling 到 XML,並將任何響應 XML 再次 unmarshalling 為物件。

6.2.1.1. URI 和傳輸

WebServiceTemplate 類使用 URI 作為訊息目的地。你可以在模板本身上設定 defaultUri 屬性,或在呼叫模板方法時顯式提供 URI。URI 將被解析為 WebServiceMessageSender,它負責透過傳輸層傳送 XML 訊息。你可以使用 WebServiceTemplate 類的 messageSendermessageSenders 屬性設定一個或多個訊息傳送器。

6.2.1.1.1. 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>

6.2.1.1.2. JMS 傳輸

對於透過 JMS 傳送訊息,Spring Web Services 提供了 JmsMessageSender。該類使用 Spring 框架的功能將 WebServiceMessage 轉換為 JMS Message,在 QueueTopic 上傳送,並接收響應(如果有)。

要使用 JmsMessageSender,你需要將 defaultUriuri 引數設定為 JMS URI,該 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 不能可靠地支援附件和字元編碼。

以下示例顯示瞭如何將 JMS 傳輸與 ActiveMQ 連線工廠結合使用

<beans>

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

    <bean id="connectionFactory" class="org.apache.activemq.ActiveMQConnectionFactory">
        <property name="brokerURL" value="vm://?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>

6.2.1.1.3. 電子郵件傳輸

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

要使用 MailMessageSender,請將 defaultUriuri 引數設定為 mailto URI。以下是一些 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>

6.2.1.1.4. 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>

6.2.1.2. 訊息工廠

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

6.2.2. 傳送和接收 WebServiceMessage

WebServiceTemplate 包含許多傳送和接收 Web 服務訊息的便利方法。有些方法接受並返回 Source,有些則返回 Result。此外,還有將物件 marshalling 到 XML 和 unmarshalling 物件的方法。以下是一個向 Web 服務傳送簡單 XML 訊息的示例。

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("https://:8080/AnotherWebService",
            source, result);
    }

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

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

</beans>

上述示例使用 WebServiceTemplate 向位於 https://:8080/WebService 的 Web 服務傳送一個 hello world 訊息(在 simpleSendAndReceive() 方法的情況下),並將結果寫入控制檯。WebServiceTemplate 被注入了預設 URI,該 URI 被使用是因為在 Java 程式碼中沒有顯式提供 URI。

請注意,WebServiceTemplate 類一旦配置完成就是執行緒安全的(假設它的所有依賴項也是執行緒安全的,Spring-WS 附帶的所有依賴項都是如此),因此如果需要,多個物件可以使用同一個共享的 WebServiceTemplate 例項。WebServiceTemplate 暴露了一個無參建構函式和 messageFactory/messageSender bean 屬性,可用於構造例項(使用 Spring 容器或普通 Java 程式碼)。或者,考慮從 Spring-WS 的 WebServiceGatewaySupport 便利基類派生,它暴露了便利的 bean 屬性以方便配置。(你不必繼承這個基類...它僅作為便利類提供。)

6.2.3. 傳送和接收 POJO - marshalling 和 unmarshalling

為了方便傳送普通 Java 物件,WebServiceTemplate 有許多 send(..) 方法,這些方法接受 Object 作為訊息資料內容的引數。WebServiceTemplate 類中的 marshalSendAndReceive(..) 方法將請求物件到 XML 的轉換委託給 Marshaller,並將響應 XML 到物件的轉換委託給 Unmarshaller。(有關 marshalling 和 unmarshaller 的更多資訊,請參考 Spring 文件。)透過使用 marshaller,你的應用程式程式碼可以專注於正在傳送或接收的業務物件,而無需關心它如何表示為 XML 的細節。為了使用 marshalling 功能,你必須使用 WebServiceTemplate 類的 marshaller/unmarshaller 屬性設定 marshaller 和 unmarshaller。

6.2.4. WebServiceMessageCallback

為了適應在訊息上設定 SOAP 頭和其他設定,WebServiceMessageCallback 介面允許你在訊息建立之後、傳送之前訪問訊息。下面的示例演示瞭如何在一個透過 marshalling 物件建立的訊息上設定 SOAP Action 頭。

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 Action 頭。

6.2.4.1. WS-Addressing

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

對於在客戶端設定 WS-Addressing 頭,你可以使用 org.springframework.ws.soap.addressing.client.ActionCallback。這個 callback 將所需的 Action 頭作為引數。它還有建構函式用於指定 WS-Addressing 版本和 To 頭。如果未指定,To 頭將預設為正在進行的連線的 URL。

以下是一個將 Action 頭設定為 http://samples/RequestOrder 的示例

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

6.2.5. WebServiceMessageExtractor

WebServiceMessageExtractor 介面是一個低階 callback 介面,允許你完全控制從接收到的 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.3. 客戶端測試

當涉及測試 Web 服務客戶端(即使用 WebServiceTemplate 訪問 Web 服務的類)時,有兩種可能的方法

  • 編寫單元測試,簡單地模擬 (mock) 掉 WebServiceTemplate 類、WebServiceOperations 介面或整個客戶端類。

    這種方法的優點是相當容易實現;缺點是不能真正測試線上路上傳送的 XML 訊息的確切內容,尤其是在模擬掉整個客戶端類時。

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

第一種方法可以使用 EasyMock、JMock 等模擬框架輕鬆實現。下一節將重點介紹如何編寫整合測試,使用 Spring Web Services 2.0 中引入的測試功能。

6.3.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 中的程式碼完成功能(即 ctrl-space)來指導你完成模擬伺服器的設定過程。

注意

另請注意,在單元測試中,你依賴 Spring Web Services 中可用的標準日誌記錄功能。有時檢查請求或響應訊息以找出特定測試失敗的原因可能會很有用。有關更多資訊,請參閱第 4.4 節,“訊息日誌記錄和跟蹤”

例如,考慮這個 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 來定義期望(參見第 6.3.2 節,“RequestMatcherRequestMatchers)。

我們還透過呼叫 andRespond() 並使用靜態匯入的 ResponseCreators 提供的 withPayload() ResponseCreator 來設定響應(參見第 6.3.3 節,“ResponseCreatorResponseCreators)。

測試的這部分可能看起來有點令人困惑,但 IDE 的程式碼完成功能非常有幫助。輸入 expect( 後,只需按下 ctrl-space,如果已靜態匯入 RequestMatchers,IDE 將為你提供可能的請求匹配策略列表。這同樣適用於 andRespond(,如果已靜態匯入 ResponseCreators 的話。

6

我們在 CustomerClient 上呼叫 getCustomerCount(),從而使用 WebServiceTemplate。此時模板已設定為“測試模式”,因此此方法呼叫不會建立真正的 (HTTP) 連線。我們還根據方法呼叫的結果進行一些 JUnit 斷言。

7

我們在 MockWebServiceServer 上呼叫 verify(),從而驗證是否實際接收到了預期的訊息。

6.3.2. RequestMatcherRequestMatchers

為了驗證請求訊息是否滿足某些期望,MockWebServiceServer 使用 RequestMatcher 策略介面。此介面定義的契約非常簡單

public interface RequestMatcher {

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

}

你可以編寫此介面的自己的實現,在訊息不符合你的期望時丟擲 AssertionError,但你當然不必這樣做。RequestMatchers 類提供了標準的 RequestMatcher 實現供你在測試中使用。你通常會靜態匯入這個類。

RequestMatchers 類提供以下請求匹配器

RequestMatchers 方法描述
anything()期望任何型別的請求。
payload()期望給定的請求有效載荷 (payload)。
validPayload()期望請求有效載荷 (payload) 根據給定的 XSD schema 進行驗證。
xpath()期望給定的 XPath 表示式存在、不存在或計算為給定值。
soapHeader()期望請求訊息中存在給定的 SOAP 頭。
connectionTo()期望連線到給定的 URL。

你可以透過鏈式呼叫 andExpect() 來設定多個請求期望,如下所示

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

有關 RequestMatchers 提供的請求匹配器的更多資訊,請參考類級別 Javadoc。

6.3.3. ResponseCreatorResponseCreators

當請求訊息已驗證並滿足定義的期望時,MockWebServiceServer 將建立一個供 WebServiceTemplate 使用的響應訊息。伺服器為此目的使用 ResponseCreator 策略介面

public interface ResponseCreator {

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

}

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

ResponseCreators 類提供以下響應

ResponseCreators 方法描述
withPayload()建立具有給定有效載荷 (payload) 的響應訊息。
withError()在響應連線中建立錯誤。此方法讓你有機會測試錯誤處理。
withException()從響應連線讀取時丟擲異常。此方法讓你有機會測試異常處理。
withMustUnderstandFault(), withClientOrSenderFault(), withServerOrReceiverFault()withVersionMismatchFault()建立具有給定 SOAP 錯誤的響應訊息。此方法讓你有機會測試錯誤處理。

有關 RequestMatchers 提供的請求匹配器的更多資訊,請參考類級別 Javadoc。