第 4 章. 共享元件

在本章中,我們將探討在客戶端和伺服器端 Spring-WS 開發之間共享的元件。這些介面和類代表了 Spring-WS 的構建塊,因此理解它們的作用非常重要,即使你可能不直接使用它們。

4.1. Web Service 訊息

4.1.1. WebServiceMessage

Spring Web Services 的核心介面之一是 WebServiceMessage。此介面代表協議無關的 XML 訊息。介面包含提供訪問訊息負載的方法,形式為 javax.xml.transform.Sourcejavax.xml.transform.ResultSourceResult 是標記介面,它們表示 XML 輸入和輸出的抽象。具體實現包裝了各種 XML 表示形式,如下表所示。

Source/Result 實現包裝的 XML 表示形式
javax.xml.transform.dom.DOMSourceorg.w3c.dom.Node
javax.xml.transform.dom.DOMResultorg.w3c.dom.Node
javax.xml.transform.sax.SAXSourceorg.xml.sax.InputSourceorg.xml.sax.XMLReader
javax.xml.transform.sax.SAXResultorg.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

除了從負載讀取和寫入負載外,Web Service 訊息還可以將自身寫入輸出流。

4.1.2. SoapMessage

SoapMessageWebServiceMessage 的子類。它包含 SOAP 特有的方法,例如獲取 SOAP 頭、SOAP 錯誤等。通常,你的程式碼不應依賴於 SoapMessage,因為 SOAP Body 的內容(即訊息的負載)可以透過 WebServiceMessage 中的 getPayloadSource()getPayloadResult() 方法獲得。只有在需要執行 SOAP 特定操作(例如新增頭、獲取附件等)時,才需要將 WebServiceMessage 轉換為 SoapMessage

4.1.3. 訊息工廠

具體的訊息實現由 WebServiceMessageFactory 建立。此工廠可以建立空訊息,或基於輸入流讀取訊息。有 WebServiceMessageFactory 的兩個具體實現;一個基於 SAAJ(SOAP with Attachments API for Java),另一個基於 Axis 2 的 AXIOM(AXis Object Model)。

4.1.3.1. SaajSoapMessageFactory

SaajSoapMessageFactory 使用 SOAP with Attachments API for Java 來建立 SoapMessage 實現。SAAJJ2EE1.4 的一部分,因此它應該在大多數現代應用伺服器下受到支援。以下是SAAJ常見應用伺服器提供的版本概覽

應用伺服器SAAJ版本
BEA WebLogic 81.1
BEA WebLogic 91.1/1.2[a]
IBM WebSphere 61.2
SUN Glassfish 11.3

[a] Weblogic 9 在 1.2 實現中存在一個已知 bug:它實現了所有 1.2 介面,但在呼叫時會丟擲 UnsupportedOperationException。Spring Web Services 有一個解決方法:在 WebLogic 9 上執行時使用 1.1。SAAJ1.2 實現:它實現了所有 1.2 介面,但在呼叫時會丟擲 UnsupportedOperationException。Spring Web Services 有一個變通方法:它在 WebLogic 9 上執行時使用SAAJ1.1。

此外,Java SE 6 包含SAAJ1.3。你可以像這樣配置一個 SaajSoapMessageFactory

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

注意

SAAJSAAJ 基於 DOM(Document Object Model)。這意味著所有 SOAP 訊息都 儲存在記憶體中。對於較大的 SOAP 訊息,這可能效能不高。在這種情況下,AxiomSoapMessageFactory 可能更適用。

4.1.3.2. AxiomSoapMessageFactory

AxiomSoapMessageFactory 使用 AXis 2 Object Model 來建立 SoapMessage 實現。AXIOM基於StAX(Streaming API for XML)。StAX 提供了一種基於拉取(pull-based)機制來讀取 XML 訊息,這對於較大的訊息來說效率更高。

為了提高 AxiomSoapMessageFactory 的讀取效能,你可以將 payloadCaching 屬性設定為 false(預設為 true)。這將直接從 socket 流中讀取 SOAP body 的內容。啟用此設定後,負載只能讀取一次。這意味著你必須確保訊息的任何預處理(日誌記錄等)不會消耗掉它。

你可以按如下方式使用 AxiomSoapMessageFactory

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

除了負載快取,AXIOMAXIOM 還支援完整的流式訊息,如 StreamingWebServiceMessage 中所定義。這意味著負載可以直接設定在響應訊息上,而不是寫入 DOM 樹或緩衝區。

對於AXIOMAXIOM 的完整流式處理,當處理方法返回一個JAXB2支援的物件時使用。它會自動將此經過 marshal 的物件設定到響應訊息中,並在響應發出時將其寫入輸出 socket 流。

有關完整流式處理的更多資訊,請參閱 StreamingWebServiceMessageStreamingPayload 的類級 Javadoc。

4.1.3.3. SOAP1.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>

在上面的示例中,我們定義了一個只接受SOAPSOAP 1.2 訊息的 SaajSoapMessageFactory

注意

儘管SOAPSOAP 的兩個版本格式非常相似,但 1.2 版本與 1.1 不向後相容,因為它使用了不同的 XML 名稱空間。SOAPSOAP1.1 和 1.2 之間的其他主要區別包括 Fault 的結構不同,以及 SOAPAction HTTP 頭實際上已被棄用(儘管它們仍然有效)。

關於SOAPSOAP 版本號,或更籠統地說,關於 WS-* 規範版本號,需要注意的一點是,規範的最新版本通常不是最流行的版本。對於SOAPSOAP,這意味著目前最好的版本是 1.1。將來 1.2 版本可能會變得更流行,但目前 1.1 是最安全的選擇。

4.1.4. MessageContext

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

在 Spring Web Services 中,此類對話包含在 MessageContext 中,它具有獲取請求和響應訊息的屬性。在客戶端,訊息上下文由 WebServiceTemplate 建立。在伺服器端,訊息上下文從特定於傳輸的輸入流中讀取。例如,在 HTTP 中,它從 HttpServletRequest 讀取,並將響應寫回 HttpServletResponse

4.2. TransportContext

SOAP 協議的一個關鍵特性是它試圖實現傳輸無關。這就是為什麼 Spring-WS 不透過 HTTP 請求 URL 對映訊息到端點,而是透過訊息內容進行對映的原因。

然而,有時需要訪問底層傳輸,無論是在客戶端還是伺服器端。為此,Spring Web Services 提供了 TransportContext。傳輸上下文允許訪問底層的 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 類。要在應用程式上下文中構建表示式,可以使用 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());
           }
        });
      // do something with list of Contact objects
   }
}

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

4.3.2. XPathTemplate

XPathExpression 只允許你評估單個預編譯表示式。更靈活(但較慢)的替代方案是 XpathTemplate。此類遵循 Spring 中常見的模板模式(JdbcTemplate, JmsTemplate 等)。以下是一個示例:

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 service 時,檢視 (SOAP) 訊息到達時或傳送前的內容非常有用。Spring Web Services 透過標準的 Commons Logging 介面提供了此功能。

注意

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

要記錄所有伺服器端訊息,只需將 org.springframework.ws.server.MessageTracing logger 設定為 DEBUG 或 TRACE 級別。在 DEBUG 級別,只記錄負載根元素;在 TRACE 級別,記錄整個訊息內容。如果你只想記錄已傳送訊息,請使用 org.springframework.ws.server.MessageTracing.sent logger;或使用 org.springframework.ws.server.MessageTracing.received 來記錄已接收訊息。

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

以下是一個 log4j.properties 配置示例,記錄客戶端傳送訊息的完整內容,以及客戶端接收訊息的負載根元素。在伺服器端,記錄已傳送和已接收訊息的負載根元素。

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