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

5.1. 簡介

Spring-WS 的伺服器端支援圍繞著一個 MessageDispatcher 設計,它將接收到的訊息分派給端點(endpoint),並支援可配置的端點對映、響應生成和端點攔截。端點通常使用 @Endpoint 註解進行標註,並具有一個或多個處理方法。這些方法透過檢查訊息的各個部分(通常是負載,payload)來處理接收到的 XML 請求訊息,並生成某種響應。您可以使用另一個註解(通常是 @PayloadRoot)來標註方法,以指示它可以處理哪種型別的訊息。

Spring-WS 的 XML 處理非常靈活。端點可以選擇 Spring-WS 支援的大量 XML 處理庫中的任何一個,包括 DOM 系列(W3C DOM、JDOM、dom4j 和 XOM)、用於更快效能的 SAX 或 StAX、用於從訊息中提取資訊的 XPath,甚至是編組(marshalling)技術(JAXB、Castor、XMLBeans、JiBX 或 XStream)來實現 XML 到物件以及物件到 XML 的轉換。

5.2. MessageDispatcher

Spring-WS 的伺服器端圍繞一箇中心類設計,該類將接收到的 XML 訊息分派給端點。Spring-WS 的 MessageDispatcher 非常靈活,只要類可以在 Spring IoC 容器中配置,就可以用作端點。從某種意義上說,訊息分派器類似於 Spring 的 DispatcherServlet,後者是 Spring Web MVC 中使用的“前端控制器(Front Controller)”。

MessageDispatcher 的處理和分派流程如下圖所示。

Spring Web Services 中的請求處理工作流程

MessageDispatcher 設定好並且接收到針對該特定分派器的請求時,MessageDispatcher 便開始處理請求。以下列表描述了請求由 MessageDispatcher 處理的完整過程。

  1. 使用配置好的 EndpointMapping(s) 搜尋適當的端點。如果找到端點,將執行與該端點關聯的呼叫鏈(前置處理器、後置處理器和端點),以建立響應。

  2. 為端點搜尋適當的介面卡。MessageDispatcher 將請求委託給此介面卡來呼叫端點。

  3. 如果返回響應,則將其傳送。如果未返回響應(例如,由於前置或後置處理器出於安全原因等攔截了請求),則不傳送響應。

請求處理過程中丟擲的異常會被應用上下文中宣告的任何端點異常解析器捕獲。使用這些異常解析器可以定義自定義行為(例如,在丟擲此類異常時返回 SOAP 故障)。

MessageDispatcher 具有多個屬性,用於設定端點介面卡、對映異常解析器。但是,設定這些屬性不是必需的,因為分派器會自動檢測應用上下文中註冊的所有這些型別。只有當需要覆蓋自動檢測時,才應設定這些屬性。

訊息分派器操作的是訊息上下文,而不是傳輸特定的輸入流和輸出流。因此,傳輸特定的請求需要讀取到 MessageContext 中。對於 HTTP,這可以透過 WebServiceMessageReceiverHandlerAdapter 完成,它是一個 Spring Web HandlerInterceptor,這樣 MessageDispatcher 就可以被配置在一個標準的 DispatcherServlet 中。然而,有一種更方便的方法,如第 5.3.1 節,“MessageDispatcherServlet 所示。

5.3. 傳輸

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

5.3.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,例如端點、編組器等等。

5.3.1.1. 自動公開 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="/WEB-INF/wsdl/orders.wsdl"/>

定義在 'Orders.wsdl' 檔案中的 WSDL 然後可以透過以下形式的 URL 的 GET 請求訪問(請根據實際情況替換主機、埠和 servlet 上下文路徑)。

https://:8080/spring-ws/orders.wsdl

注意

所有 WsdlDefinition bean 定義都由 MessageDispatcherServlet 以其 bean ID(或 bean 名稱)加上 .wsdl 字尾的形式公開。因此,如果 bean ID 是 echo,主機名是 "server",Servlet 上下文(war 名)是 "spring-ws",則 WSDL 可以透過 http://server/spring-ws/echo.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>

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

除了手工編寫 WSDL 並使用 <static-wsdl> 公開它之外,Spring Web Services 還可以從 XSD Schema 生成 WSDL。這是第 3.7 節,“釋出 WSDL” 中展示的方法。下一個應用上下文片段展示瞭如何建立這樣的動態 WSDL 檔案。

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

<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 portType 中。

如果您想使用多個 Schema(透過 include 或 import),您需要將 Commons XMLSchema 放在類路徑上。如果 Commons XMLSchema 在類路徑上,上面的 <dynamic-wsdl> 元素將遵循所有 XSD import 和 include,並將它們以內聯方式作為單個 XSD 放入 WSDL 中。這極大地簡化了 Schema 的部署,同時仍然可以單獨編輯它們。

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

注意

儘管在執行時從 XSD 建立 WSDL 非常方便,但這種方法也有一些缺點。首先,儘管我們努力保持不同版本之間的 WSDL 生成過程一致,但仍然存在可能發生(輕微)變化的可能性。其次,生成過程有點慢,儘管一旦生成,WSDL 會被快取以供後續引用。

因此,建議僅在專案開發階段使用 <dynamic-wsdl>。然後,我們建議使用瀏覽器下載生成的 WSDL,將其儲存在專案中,並使用 <static-wsdl> 進行公開。這是確保 WSDL 不隨時間變化的唯一方法。

5.3.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.SimpleControllerHandlerAdapter"/>
    
</beans>

注意,透過顯式新增 WebServiceMessageReceiverHandlerAdapter,分派器 servlet 不會載入預設的介面卡,因此無法處理標準的 Spring-MVC Controllers。因此,我們在最後添加了 SimpleControllerHandlerAdapter

以類似的方式,您可以配置 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.3.3. JMS 傳輸

Spring Web Services 透過 Spring 框架提供的 JMS 功能支援伺服器端 JMS 處理。Spring Web Services 提供了 WebServiceMessageListener 以便插入到 MessageListenerContainer 中。此訊息監聽器需要一個 WebServiceMessageFactory 和一個 MessageDispatcher 才能工作。以下配置片段展示了這一點。

<beans>

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

作為 WebServiceMessageListener 的替代方案,Spring Web Services 提供了一個 WebServiceMessageDrivenBean,它是一個 EJB MessageDrivenBean。有關 EJB 的更多資訊,請參閱 WebServiceMessageDrivenBean 的類級別 Javadoc。

5.3.4. 電子郵件傳輸

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

MailMessageReceiver 如何監控傳入訊息可以透過可插拔的策略進行配置:MonitoringStrategy。預設情況下,使用輪詢策略,每五分鐘輪詢一次傳入資料夾以查詢新訊息。此間隔可以透過設定策略的 pollingInterval 屬性來更改。預設情況下,所有 MonitoringStrategy 實現都會刪除已處理的訊息;這可以透過設定 deleteMessages 屬性來更改。

作為效率較低的輪詢方法的替代方案,有一種監控策略使用 IMAP IDLEIDLE 命令是 IMAP 電子郵件協議的可選擴充套件,它允許郵件伺服器非同步地向 MailMessageReceiver 傳送新訊息更新。如果您使用的 IMAP 伺服器支援 IDLE 命令,則可以將 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.3.5. 嵌入式 HTTP 伺服器傳輸

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

使用嵌入式 HTTP 伺服器時,無需外部部署描述符(web.xml)。您只需要定義伺服器例項並將其配置為處理傳入請求。Spring 核心框架中的 remoting 模組包含一個用於 HTTP 伺服器的方便的工廠 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.3.6. XMPP 傳輸

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

Spring Web Services 對 XMPP 的支援與其他傳輸非常相似:有一個用於 WebServiceTemplateXmppMessageSender,以及一個用於 MessageDispatcherXmppMessageReceiver

以下示例展示瞭如何設定伺服器端 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.4. 端點

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

您可以透過使用 @Endpoint 註解標註類來建立一個端點。在類中,您可以定義一個或多個處理傳入 XML 請求的方法,透過使用各種引數型別(例如 DOM 元素、JAXB2 物件等)。您可以使用另一個註解(通常是 @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 進行標註。這意味著訊息的負載將作為 DOM 元素傳遞給此方法。該方法具有 void 返回型別,表明不傳送響應訊息。

有關端點方法的更多資訊,請參閱第 5.4.1 節,“@Endpoint 處理方法”

4

getOrder 方法接受一個 OrderRequest 作為引數,也使用 @RequestPayload 進行標註。此引數是 JAXB2 支援的物件(它使用 @XmlRootElement 進行標註)。這意味著訊息的負載將作為反編組後的物件傳遞給此方法。SoapHeader 型別也作為引數給出。呼叫時,此引數將包含請求訊息的 SOAP 頭部。該方法也使用 @ResponsePayload 進行標註,表明返回值(Order)將用作響應訊息的負載。

有關端點方法的更多資訊,請參閱第 5.4.1 節,“@Endpoint 處理方法”

5

此端點的兩個處理方法都使用 @PayloadRoot 進行標註,指示可以處理哪種型別的請求訊息:getOrder 方法將為本地名稱為 orderRequest 且名稱空間 URI 為 http://samples 的請求呼叫;order 方法將為本地名稱為 order 的請求呼叫。

有關 @PayloadRoot 的更多資訊,請參閱第 5.5 節,“端點對映”

要啟用對 @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-3.0.xsd
    http://www.springframework.org/schema/web-services
      http://www.springframework.org/schema/web-services/web-services-2.0.xsd">

  <sws:annotation-driven />

</beans>

在接下來的幾節中,將更詳細地描述 @Endpoint 程式設計模型。

注意

端點,像其他 Spring Bean 一樣,預設情況下作用域是單例(singleton),即每個容器建立一個 bean 定義的例項。單例意味著多個執行緒可以同時使用它,因此端點必須是執行緒安全的。如果您想使用不同的作用域,例如 prototype,請參閱Spring 參考文件

請注意,Spring-WS 中提供的所有抽象基類都是執行緒安全的,除非在類級別 Javadoc 中另有說明。

5.4.1. @Endpoint 處理方法

為了使端點能夠實際處理傳入的 XML 訊息,它需要有一個或多個處理方法。處理方法可以接受各種引數和返回型別,但通常它們有一個引數用於包含訊息負載,並返回響應訊息的負載(如果有)。您將在本節中瞭解支援哪些引數和返回型別。

為了指示方法可以處理哪種型別的訊息,該方法通常使用 @PayloadRoot@SoapAction 註解進行標註。您將在第 5.5 節,“端點對映” 中瞭解有關這些註解的更多資訊。

以下是處理方法的一個示例。

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

order 方法接受一個 Element 作為引數,並使用 @RequestPayload 進行標註。這意味著訊息的負載將作為 DOM 元素傳遞給此方法。該方法具有 void 返回型別,表明不傳送響應訊息。

5.4.1.1. 處理方法引數

處理方法通常有一個或多個引數,它們引用傳入 XML 訊息的各個部分。最常見的是,處理方法會有一個單獨的引數對映到訊息的負載,但也可以對映到請求訊息的其他部分,例如 SOAP 頭部。本節將描述您可以在處理方法簽名中使用的引數。

要將引數對映到請求訊息的負載,您需要使用 @RequestPayload 註解標註此引數。此註解告訴 Spring-WS 該引數需要繫結到請求負載。

下表描述了支援的引數型別。它顯示了支援的型別、是否需要 @RequestPayload 註解以及其他注意事項。

名稱支援的引數型別需要 @RequestPayload 嗎?附加說明
TrAX javax.xml.transform.Source 及其子介面(DOMSourceSAXSourceStreamSourceStAXSource預設啟用。
W3C DOMorg.w3c.dom.Element預設啟用
dom4jorg.dom4j.Element當 dom4j 在類路徑上時啟用。
JDOMorg.jdom.Element當 JDOM 在類路徑上時啟用。
XOMnu.xom.Element當 XOM 在類路徑上時啟用。
StAX javax.xml.stream.XMLStreamReaderjavax.xml.stream.XMLEventReader當 StAX 在類路徑上時啟用。
XPath任何 boolean、double、Stringorg.w3c.Nodeorg.w3c.dom.NodeList,或者可以透過 Spring 3 轉換服務String 轉換且用 @XPathParam 註解的型別。預設啟用,請參閱第 5.4.1.1.1 節,“@XPathParam
訊息上下文org.springframework.ws.context.MessageContext預設啟用。
SOAP org.springframework.ws.soap.SoapMessageorg.springframework.ws.soap.SoapBodyorg.springframework.ws.soap.SoapEnvelopeorg.springframework.ws.soap.SoapHeader 以及與 @SoapHeader 註解結合使用時的 org.springframework.ws.soap.SoapHeaderElement預設啟用。
JAXB2任何使用 javax.xml.bind.annotation.XmlRootElement 註解的型別,以及 javax.xml.bind.JAXBElement當 JAXB2 在類路徑上時啟用。
OXMSpring OXM Unmarshaller 支援的任何型別。<sws:annotation-driven/>unmarshaller 屬性被指定時啟用。

以下是一些可能的方法簽名示例。

  • public void handle(@RequestPayload Element element)

    此方法將以請求訊息的負載作為 DOM org.w3c.dom.Element 呼叫。

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

    此方法將以請求訊息的負載作為 javax.xml.transform.dom.DOMSource 呼叫。引數 header 將繫結到請求訊息的 SOAP 頭部。

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

    此方法將以反編組到 MyJaxb2Object(使用 @XmlRootElement 註解標註)的請求訊息負載呼叫。訊息的負載也作為 DOM Element 提供。整個訊息上下文作為第三個引數傳遞。

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

5.4.1.1.1. @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 3 轉換服務String 轉換的型別。

5.4.1.2. 處理方法返回型別

要傳送響應訊息,處理方法需要指定返回型別。如果不需要響應訊息,方法可以簡單地宣告 void 返回型別。最常見的是,返回型別用於建立響應訊息的負載,但也可以對映到響應訊息的其他部分。本節將描述您可以在處理方法簽名中使用的返回型別。

要將返回值對映到響應訊息的負載,您需要使用 @ResponsePayload 註解標註該方法。此註解告訴 Spring-WS 返回值將繫結到響應負載。

下表描述了支援的返回型別。它顯示了支援的型別、引數是否需要用 @ResponsePayload 進行註解,以及其他註釋。

名稱支援的返回型別需要 @ResponsePayload 註解嗎?附加說明
無響應 void 預設啟用。
TrAX javax.xml.transform.Source 及其子介面(DOMSourceSAXSourceStreamSourceStAXSource預設啟用。
W3C DOMorg.w3c.dom.Element預設啟用
dom4jorg.dom4j.Element當 dom4j 在類路徑上時啟用。
JDOMorg.jdom.Element當 JDOM 在類路徑上時啟用。
XOMnu.xom.Element當 XOM 在類路徑上時啟用。
JAXB2任何使用 javax.xml.bind.annotation.XmlRootElement 註解的型別,以及 javax.xml.bind.JAXBElement當 JAXB2 在類路徑上時啟用。
OXM任何 Spring OXM Marshaller 支援的型別。當指定了 <sws:annotation-driven/>marshaller 屬性時啟用。

正如你所見,在定義處理方法簽名時有很多可能性。甚至可以擴充套件此機制,並支援你自己的引數型別。請參考 DefaultMethodEndpointAdapterMethodReturnValueHandler 的類級別 Javadoc 以瞭解具體方法。

5.5. Endpoint 對映

端點對映(Endpoint Mapping)負責將傳入的訊息對映到適當的端點(endpoint)。有一些端點對映是預設啟用的,例如 PayloadRootAnnotationMethodEndpointMappingSoapActionAnnotationMethodEndpointMapping,但我們首先來看一下 EndpointMapping 的一般概念。

一個 EndpointMapping 提供一個 EndpointInvocationChain,其中包含與傳入請求匹配的端點,並且可能還包含一個將應用於請求和響應的端點攔截器列表。當請求進來時,MessageDispatcher 會將其交給端點對映,讓它檢查請求並生成一個適當的 EndpointInvocationChain。然後 MessageDispatcher 將呼叫鏈中的端點和任何攔截器。

可配置的端點對映概念非常強大,它還可以選擇包含攔截器(可以處理請求或響應,或兩者)。許多輔助功能可以構建到自定義的 EndpointMapping 中。例如,可以有一個自定義的端點對映,它不僅根據訊息內容,還根據特定的 SOAP 頭(或多個 SOAP 頭)來選擇端點。

大多數端點對映繼承自 AbstractEndpointMapping,它提供了一個 'interceptors' 屬性,這是要使用的攔截器列表。EndpointInterceptor第 5.5.2 節 “攔截請求 - EndpointInterceptor 介面” 中討論。此外,還有一個 'defaultEndpoint',當此端點對映沒有找到匹配的端點時,將使用此預設端點。

第 5.4 節 “端點” 中所述,@Endpoint 風格允許你在一個端點類中處理多個請求。這是 MethodEndpointMapping 的職責。這個對映決定了對於傳入的請求訊息應該呼叫哪個方法。

有兩種端點對映可以將請求導向方法:PayloadRootAnnotationMethodEndpointMappingSoapActionAnnotationMethodEndpointMapping,這兩者都可以透過在你的應用程式上下文中使用 <sws:annotation-driven/> 來啟用。

PayloadRootAnnotationMethodEndpointMapping 使用 @PayloadRoot 註解,帶有 localPartnamespace 元素,用特定的限定名稱標記方法。每當有載荷根元素具有此限定名稱的訊息傳入時,該方法就會被呼叫。例如,請參見 上面

或者,SoapActionAnnotationMethodEndpointMapping 使用 @SoapAction 註解來用特定的 SOAP Action 標記方法。每當有訊息傳入並且帶有此 SOAPAction 頭時,該方法就會被呼叫。

5.5.1. WS-Addressing

WS-Addressing 指定了一種與傳輸無關的路由機制。它基於 ToAction SOAP 頭,分別指示 SOAP 訊息的目的地和意圖。此外,WS-Addressing 允許你定義一個返回地址(用於普通訊息和故障訊息),以及一個可用於關聯的唯一訊息識別符號[2]。以下是 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,而動作設定為 http://example.com/fabrikam/mail/Delete。此外,還有一個訊息識別符號和回覆地址。預設情況下,此地址是“匿名”地址,表示響應應使用與請求相同的通道傳送(即 HTTP 響應),但它也可以是另一個地址,如本示例所示。

在 Spring Web Services 中,WS-Addressing 作為端點對映實現。使用此對映,你可以將 WS-Addressing 動作與端點相關聯,類似於上面描述的 SoapActionAnnotationMethodEndpointMapping

5.5.1.1. AnnotationActionEndpointMapping

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

要使用 AnnotationActionEndpointMapping,請使用 @Action 註解標記處理方法,類似於 第 5.4.1 節 “@Endpoint 處理方法”第 5.5 節 “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);
    }

}

上面的對映將具有 WS-Addressing Actionhttp://samples/RequestOrder 的請求路由到 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 頭屬性進行比較。

最後,還有一個 messageSenders 屬性,這是將響應訊息傳送到非匿名、帶外地址所必需的。你可以在此屬性中設定 MessageSender 實現,就像在 WebServiceTemplate 上設定一樣。參見 第 6.2.1.1 節 “URI 和傳輸”

5.5.2. 攔截請求 - EndpointInterceptor 介面

端點對映機制具有端點攔截器的概念。當你想要對某些請求應用特定功能時,例如處理與安全相關的 SOAP 頭或日誌記錄請求和響應訊息,這些功能非常有用。

端點攔截器通常透過在應用程式上下文中使用 <sws;interceptors > 元素來定義。在此元素中,你可以簡單地定義適用於該應用程式上下文中定義的所有端點的端點攔截器 Bean。或者,你可以使用 <sws:payloadRoot><sws:soapAction> 元素來指定攔截器應適用於哪個載荷根名稱或 SOAP 動作。例如

<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),它攔截所有請求和響應。我們還定義了一個僅適用於載荷根名稱空間為 http://www.example.com 的 XML 訊息的攔截器。在這裡,我們可以除了 namespaceUri 之外,還定義一個 localPart 屬性,以進一步限制攔截器適用的訊息。最後,我們定義了兩個攔截器,當訊息具有 http://www.example.com/SoapAction SOAP 動作時應用。注意第二個攔截器實際上是對 <interceptors> 元素之外的 Bean 定義的引用。你可以在 <interceptors> 元素內的任何位置使用 Bean 引用。

攔截器必須實現 org.springframework.ws.server 包中的 EndpointInterceptor 介面。此介面定義了三個方法,一個可用於在實際端點執行之前處理請求訊息,一個可用於處理正常響應訊息,另一個可用於處理故障訊息,這兩個都將在端點執行之後呼叫。這三個方法應提供足夠的靈活性來進行各種預處理和後處理。

攔截器上的 handleRequest(..) 方法返回一個布林值。您可以使用此方法來中斷或繼續呼叫鏈的處理。當此方法返回 true 時,端點執行鏈將繼續;當它返回 false 時,MessageDispatcher 將此解釋為攔截器本身已經處理完畢,並且不再繼續執行呼叫鏈中的其他攔截器和實際端點。handleResponse(..)handleFault(..) 方法也具有布林返回值。當這些方法返回 false 時,響應將不會發送回客戶端。

有一些標準的 EndpointInterceptor 實現可以在你的 Web 服務中使用。此外,還有一個 XwsSecurityInterceptor,它在 第 7.2 節 “XwsSecurityInterceptor 中描述。

5.5.2.1. PayloadLoggingInterceptorSoapEnvelopeLoggingInterceptor

開發 Web 服務時,日誌記錄傳入和傳出的 XML 訊息非常有用。SWS 透過 PayloadLoggingInterceptorSoapEnvelopeLoggingInterceptor 類提供了便利。前者僅將訊息的載荷記錄到 Commons Logging 日誌中;後者記錄整個 SOAP 信封,包括 SOAP 頭。以下示例展示瞭如何在端點對映中定義它們

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

</beans>

這兩個攔截器都有兩個屬性:'logRequest' 和 'logResponse',可以設定為 false 以停用請求或響應訊息的日誌記錄。

5.5.2.2. PayloadValidatingInterceptor

使用契約優先開發風格的一個好處是,我們可以使用模式(schema)來驗證傳入和傳出的 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>

5.5.2.3. 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 轉換響應訊息。請注意,由於端點攔截器是在端點對映級別註冊的,你可以簡單地建立一個適用於“舊式”訊息的端點對映,並將攔截器新增到該對映中。因此,轉換將僅應用於這些“舊式”訊息。

5.6. 處理異常

Spring-WS 提供了 EndpointExceptionResolver,以減輕在端點處理與請求匹配的訊息時發生意外異常的痛苦。端點異常解析器有點類似於在 Web 應用程式描述符 web.xml 中定義的異常對映。但是,它們提供了更靈活的異常處理方式。它們提供了關於異常丟擲時呼叫了哪個端點的資訊。此外,程式設計方式處理異常為你提供了更多適當響應的選項。你不再需要透過暴露應用程式內部細節來給出異常和堆疊跟蹤,而是可以以你想要的方式處理異常,例如透過返回帶有特定故障程式碼和字串的 SOAP 故障。

端點異常解析器會自動被 MessageDispatcher 拾取,因此不需要顯式配置。

除了實現 EndpointExceptionResolver 介面(這只是實現 resolveException(MessageContext, endpoint, Exception) 方法的問題)之外,你還可以使用提供的實現之一。最簡單的實現是 SimpleSoapExceptionResolver,它只是建立一個 SOAP 1.1 Server 或 SOAP 1.2 Receiver Fault,並使用異常訊息作為故障字串。SimpleSoapExceptionExceptionResolver 是預設設定,但可以透過顯式新增另一個解析器來覆蓋。

5.6.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 的異常對映到具有故障字串 "Invalid request" 的客戶端 SOAP Fault,如下面的響應所示

<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.6.2. SoapFaultAnnotationExceptionResolver

最後,也可以用 @SoapFault 註解標記異常類,以指示每當丟擲該異常時應返回的 SOAP Fault。為了使這些註解被拾取,你需要將 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.7. 伺服器端測試

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

  • 編寫單元測試,其中為端點提供(模擬)引數以供消費。

    這種方法的優點是很容易實現(尤其是對於用 @Endpoint 註解的類);缺點是你並非真正測試透過網路傳送的 XML 訊息的確切內容。

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

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

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

Spring Web Services 2.0 引入了建立端點整合測試的支援。在此上下文中,端點是處理 (SOAP) 訊息的類(參見 第 5.4 節 “端點”)。

整合測試支援位於 org.springframework.ws.test.server 包中。該包中的核心類是 MockWebServiceClient。基本思想是此客戶端建立一個請求訊息,然後將其傳送給標準 MessageDispatcherServlet 應用程式上下文中配置的端點(參見 第 5.3.1 節 “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 中的程式碼補全功能(即 ctrl-space)來指導你完成設定模擬伺服器的過程。

注意

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

例如,考慮這個簡單的 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 註解。參見 第 5.4 節 “端點”

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 應用程式上下文(參見 第 5.3.1 節 “MessageDispatcherServlet),從 spring-ws-servlet.xml 中讀取。在這種情況下,應用程式上下文將包含 CustomerEndpoint 的 Bean 定義(或者可能使用了 <context:component-scan />)。

4

@Before 方法中,我們使用工廠方法 createClient 建立了一個 MockWebServiceClient

5

我們透過呼叫 sendRequest() 並傳入由靜態匯入的 RequestCreators 提供的 withPayload() RequestCreator 來發送請求(參見 第 5.7.2 節 “RequestCreatorRequestCreators)。

我們還透過呼叫 andExpect() 並傳入由靜態匯入的 ResponseMatchers 提供的 payload() ResponseMatcher 來設定響應期望(參見 第 5.7.3 節 “ResponseMatcherResponseMatchers)。

測試的這部分可能看起來有點令人困惑,但你的 IDE 的程式碼補全功能會非常有幫助。在輸入 sendRequest( 後,只需鍵入 ctrl-space,如果已靜態匯入 RequestCreators,你的 IDE 將為你提供可能的請求建立策略列表。這同樣適用於 andExpect(,前提是你已靜態匯入 ResponseMatchers

5.7.2. RequestCreatorRequestCreators

最初,MockWebServiceClient 需要為端點建立一個請求訊息以供消費。客戶端為此目的使用了 RequestCreator 策略介面

public interface RequestCreator {

  WebServiceMessage createRequest(WebServiceMessageFactory messageFactory)
    throws IOException;

}

你可以編寫此介面的自己的實現,使用訊息工廠建立請求訊息,但這絕對不是必須的。RequestCreators 類提供了一種在 withPayload() 方法中基於給定載荷建立 RequestCreator 的方法。通常你會靜態匯入 RequestCreators

5.7.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。



[2] 有關 WS-Addressing 的更多資訊,請參見 http://en.wikipedia.org/wiki/WS-Addressing