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