第 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://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>

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("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 向位于 http://localhost: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。