© 2005-2020 原始作者。

您可以为自己使用和分发此文档的副本,前提是您不对这些副本收取任何费用,并且每份副本(无论是印刷版还是电子版)都包含此版权声明。

前言

在当前的面向服务架构时代,越来越多的人使用 Web 服务来连接以前未连接的系统。最初,Web 服务被认为是进行远程过程调用 (RPC) 的另一种方式。然而,随着时间的推移,人们发现 RPC 和 Web 服务之间存在很大差异。特别是当与其他平台的互操作性很重要时,发送包含处理请求所需所有数据的封装 XML 文档通常更好。从概念上讲,基于 XML 的 Web 服务更适合与消息队列进行比较,而不是远程解决方案。总的来说,XML 应该被视为数据的平台中立表示,即 SOA 的通用语言。在开发或使用 Web 服务时,重点应该放在 XML 上,而不是 Java 上。

Spring Web Services 专注于创建这些文档驱动的 Web 服务。Spring Web Services 促进契约优先的 SOAP 服务开发,通过使用多种 XML 有效负载操作方法之一来创建灵活的 Web 服务。Spring-WS 提供了一个强大的消息分派框架,一个与现有应用程序安全解决方案集成的 WS-Security 解决方案,以及一个遵循熟悉的 Spring 模板模式的客户端 API

I. 引言

参考文档的第一部分概述了 Spring Web Services 及其底层概念。然后介绍了 Spring-WS,并解释了契约优先 Web 服务开发背后的概念

1. 什么是 Spring Web Services?

1.1. 简介

Spring Web Services (Spring-WS) 是 Spring 社区的产品,专注于创建文档驱动的 Web 服务。Spring Web Services 旨在促进契约优先的 SOAP 服务开发,通过使用多种 XML 有效负载操作方法之一来创建灵活的 Web 服务。该产品基于 Spring 本身,这意味着您可以将 Spring 概念(例如依赖注入)作为 Web 服务不可或缺的一部分。

人们使用 Spring-WS 的原因有很多,但大多数人在发现其他 SOAP 栈在遵循 Web 服务最佳实践方面存在不足后,都会被它吸引。Spring-WS 使最佳实践变得容易。这包括 WS-I 基本配置文件、契约优先开发以及契约与实现之间松散耦合等实践。Spring Web Services 的其他主要功能是

1.1.1. 强大的映射

您可以根据消息负载、SOAP Action 头部或 XPath 表达式将传入的 XML 请求分发到任何对象。

1.1.2. XML API 支持

传入的 XML 消息不仅可以使用标准的 JAXP API(如 DOM、SAX 和 StAX)进行处理,还可以使用 JDOM、dom4j、XOM 甚至编组技术进行处理。

1.1.3. 灵活的 XML 编组

Spring Web Services 建立在 Spring 框架中的对象/XML 映射模块之上,该模块支持 JAXB 1 和 2、Castor、XMLBeans、JiBX 和 XStream。

1.1.4. 重用您的 Spring 专长

Spring-WS 使用 Spring 应用程序上下文进行所有配置,这应该有助于 Spring 开发人员快速上手。此外,Spring-WS 的架构类似于 Spring-MVC。

1.1.5. 支持 WS-Security

WS-Security 允许您对 SOAP 消息进行签名、加密和解密,或对其进行身份验证。

1.1.6. 与 Spring Security 集成

Spring Web Services 的 WS-Security 实现提供了与 Spring Security 的集成。这意味着您也可以将现有的 Spring Security 配置用于您的 SOAP 服务。

1.1.7. Apache 许可证

您可以放心地在项目中使用 Spring-WS。

1.2. 运行时环境

Spring Web Services 需要标准的 Java 8 运行时环境。Spring-WS 基于 Spring Framework 4.0.9 构建,但支持更高版本。

Spring-WS 由多个模块组成,本节的其余部分将对此进行描述。

  • XML 模块 (spring-xml.jar) 包含 Spring Web Services 的各种 XML 支持类。此模块主要用于 Spring-WS 框架本身,而不是 Web 服务开发人员。

  • 核心模块 (spring-ws-core.jar) 是 Spring Web 服务功能的核心部分。它提供了核心的WebServiceMessageSoapMessage接口、服务器端框架(具有强大的消息分派)、用于实现 Web 服务端点的各种支持类以及客户端 WebServiceTemplate

  • 支持模块 (spring-ws-support.jar) 包含额外的传输(JMS、电子邮件等)。

  • 安全包 (spring-ws-security.jar) 提供了一个与核心 Web 服务包集成的 WS-Security 实现。它允许您对 SOAP 消息进行签名、解密和加密,以及向其添加主体令牌。此外,它还允许您使用现有的 Spring Security 安全实现进行身份验证和授权。

下图显示了 Spring-WS 模块之间的依赖关系。箭头表示依赖关系(即,Spring-WS Core 依赖于 Spring-XML 和 Spring 3 及更高版本中的 OXM 模块)。

spring deps

1.3. 支持的标准

Spring Web Services 支持以下标准

  • SOAP 1.1 和 1.2

  • WSDL 1.1 和 2.0 (仅支持 WSDL 1.1 的基于 XSD 的生成)

  • WS-I Basic Profile 1.0, 1.1, 1.2, 和 2.0

  • WS-Addressing 1.0 和 2004 年 8 月草案

  • SOAP 消息安全 1.1、用户名令牌配置文件 1.1、X.509 证书令牌配置文件 1.1、SAML 令牌配置文件 1.1、Kerberos 令牌配置文件 1.1、基本安全配置文件 1.1

2. 为何采用契约优先?

在创建 Web 服务时,有两种开发风格:契约在后和契约优先。当您使用契约在后方法时,您从 Java 代码开始,并让 Web 服务契约(在 WSDL 中——参见侧边栏)从其中生成。当使用契约优先时,您从 WSDL 契约开始,并使用 Java 实现契约。

什么是 WSDL?

WSDL 代表 Web 服务描述语言。WSDL 文件是描述 Web 服务的 XML 文档。它指定了服务的位置和该服务公开的操作(或方法)。有关 WSDL 的更多信息,请参阅 WSDL 规范

Spring-WS 仅支持契约优先的开发风格,本节解释了原因。

2.1. 对象/XML 阻抗不匹配

与 ORM 领域类似,我们有对象/关系阻抗不匹配,将 Java 对象转换为 XML 也存在类似问题。乍一看,O/X 映射问题似乎很简单:为每个 Java 对象创建一个 XML 元素,将所有 Java 属性和字段转换为子元素或属性。然而,事情并非如它们看起来那么简单,因为分层语言(如 XML,尤其是 XSD)和 Java 的图模型之间存在根本区别。

本节的大部分内容都受到[alpine][effective-enterprise-java]的启发。

2.1.1. XSD 扩展

在 Java 中,改变类行为的唯一方法是子类化它,将新行为添加到该子类中。在 XSD 中,您可以通过限制数据类型来扩展它——即,限制元素和属性的有效值。例如,考虑以下示例

<simpleType name="AirportCode">
  <restriction base="string">
      <pattern value="[A-Z][A-Z][A-Z]"/>
  </restriction>
</simpleType>

此类型通过正则表达式限制 XSD 字符串,只允许三个大写字母。如果此类型转换为 Java,我们最终得到一个普通的 java.lang.String。正则表达式在转换过程中丢失了,因为 Java 不允许这类扩展。

2.1.2. 不可移植的类型

Web 服务最重要的目标之一是互操作性:支持 Java、.NET、Python 等多个平台。由于所有这些语言都有不同的类库,因此您必须使用一些通用的、跨语言的格式在它们之间进行通信。这种格式就是 XML,所有这些语言都支持它。

由于这种转换,您必须确保在服务实现中使用可移植类型。例如,考虑一个返回 java.util.TreeMap 的服务

public Map getFlights() {
  // use a tree map, to make sure it's sorted
  TreeMap map = new TreeMap();
  map.put("KL1117", "Stockholm");
  ...
  return map;
}

毫无疑问,此映射的内容可以转换为某种 XML,但由于没有标准方法在 XML 中描述映射,因此它是专有的。此外,即使它可以转换为 XML,许多平台也没有类似于 TreeMap 的数据结构。因此,当 .NET 客户端访问您的 Web 服务时,它可能最终得到一个具有不同语义的 System.Collections.Hashtable

在客户端工作时也存在此问题。考虑以下 XSD 片段,它描述了一个服务契约

<element name="GetFlightsRequest">
  <complexType>
    <all>
      <element name="departureDate" type="date"/>
      <element name="from" type="string"/>
      <element name="to" type="string"/>
    </all>
  </complexType>
</element>

此契约定义了一个带有 date 的请求,它是一个表示年、月、日的 XSD 数据类型。如果我们从 Java 调用此服务,我们可能会使用 java.util.Datejava.util.Calendar。然而,这两个类实际上都描述的是时间,而不是日期。因此,我们实际上最终发送的数据表示的是 2007 年 4 月 4 日午夜 (2007-04-04T00:00:00),这与 2007-04-04 不同。

2.1.3. 循环图

假设我们有以下类结构

public class Flight {
  private String number;
  private List<Passenger> passengers;

  // getters and setters omitted
}

public class Passenger {
  private String name;
  private Flight flight;

  // getters and setters omitted
}

这是一个循环图:Flight 引用 Passenger,而 Passenger 又引用 Flight。这样的循环图在 Java 中很常见。如果我们采用一种天真的方法将其转换为 XML,我们最终会得到类似以下内容

<flight number="KL1117">
  <passengers>
    <passenger>
      <name>Arjen Poutsma</name>
      <flight number="KL1117">
        <passengers>
          <passenger>
            <name>Arjen Poutsma</name>
            <flight number="KL1117">
              <passengers>
                <passenger>
                   <name>Arjen Poutsma</name>
                   ...

处理这种结构可能需要很长时间才能完成,因为此循环没有停止条件。

解决此问题的一种方法是使用对已编组对象的引用

<flight number="KL1117">
  <passengers>
    <passenger>
      <name>Arjen Poutsma</name>
      <flight href="KL1117" />
    </passenger>
    ...
  </passengers>
</flight>

这解决了递归问题,但引入了新问题。首先,您无法使用 XML 验证器来验证此结构。另一个问题是,在 SOAP 中使用这些引用的标准方式 (RPC/encoded) 已被弃用,取而代之的是 document/literal(请参阅 WS-I 基本配置文件)。

这些只是处理 O/X 映射时的一些问题。在编写 Web 服务时尊重这些问题很重要。尊重它们的最佳方式是完全专注于 XML,同时使用 Java 作为实现语言。这就是契约优先的全部意义所在。

2.2. 契约优先与契约在后

除了上一节中提到的对象/XML 映射问题外,还有其他原因偏爱契约优先的开发风格。

2.2.1. 脆弱性

如前所述,契约后置开发风格会导致您的 Web 服务契约(WSDL 和 XSD)从您的 Java 契约(通常是接口)生成。如果您使用这种方法,则无法保证契约随时间保持不变。每次您更改 Java 契约并重新部署它时,Web 服务契约可能会随之发生更改。

此外,并非所有 SOAP 栈都从 Java 契约生成相同的 Web 服务契约。这意味着更改您当前的 SOAP 栈以使用不同的栈(无论出于何种原因)也可能会更改您的 Web 服务契约。

当 Web 服务契约发生更改时,契约的用户必须被告知获取新契约并可能更改其代码以适应契约中的任何更改。

为了使契约有用,它必须尽可能长时间保持不变。如果契约发生更改,您必须联系服务的所有用户,并指示他们获取新版本的契约。

2.2.2. 性能

当 Java 对象自动转换为 XML 时,无法确定通过网络发送了什么。一个对象可能引用另一个对象,后者又引用另一个对象,依此类推。最终,虚拟机堆中的一半对象可能会转换为 XML,从而导致响应时间变慢。

当使用契约优先时,您明确描述了发送哪些 XML 到何处,从而确保这正是您想要的。

2.2.3. 可重用性

在单独的文件中定义您的模式允许您在不同的场景中重用该文件。考虑在名为 airline.xsd 的文件中定义 AirportCode

<simpleType name="AirportCode">
    <restriction base="string">
        <pattern value="[A-Z][A-Z][A-Z]"/>
    </restriction>
</simpleType>

您可以使用 import 语句在其他模式甚至 WSDL 文件中重用此定义。

2.2.4. 版本控制

尽管契约必须尽可能长时间保持不变,但有时确实需要更改。在 Java 中,这通常会导致一个新的 Java 接口,例如 AirlineService2,以及该接口的(新的)实现。当然,旧服务必须保留,因为可能存在尚未迁移的客户端。

如果使用契约优先,我们可以使契约和实现之间的耦合更松散。这种更松散的耦合允许我们在一个类中实现契约的两个版本。例如,我们可以使用 XSLT 样式表将任何“旧式”消息转换为“新式”消息。

3. 编写契约优先的 Web 服务

本教程向您展示如何编写契约优先的 Web 服务——即,如何开发首先从 XML Schema 或 WSDL 契约开始,然后才是 Java 代码的 Web 服务。Spring-WS 专注于这种开发风格,本教程应该可以帮助您入门。请注意,本教程的第一部分几乎不包含 Spring-WS 特定信息。它主要涉及 XML、XSD 和 WSDL。第二部分侧重于使用 Spring-WS 实现此契约。

进行契约优先 Web 服务开发时最重要的事情是从 XML 的角度思考。这意味着 Java 语言概念的重要性较低。通过网络发送的是 XML,您应该专注于此。Java 用作实现 Web 服务的语言是一个实现细节。

在本教程中,我们定义了一个由人力资源部门创建的 Web 服务。客户端可以向此服务发送假期请求表单以预订假期。

3.1. 消息

在本节中,我们重点关注发送到 Web 服务和从 Web 服务发送的实际 XML 消息。我们首先确定这些消息的外观。

3.1.1. 假期

在场景中,我们必须处理假期请求,因此确定假期的 XML 形式是有意义的

<Holiday xmlns="http://mycompany.com/hr/schemas">
    <StartDate>2006-07-03</StartDate>
    <EndDate>2006-07-07</EndDate>
</Holiday>

假期由开始日期和结束日期组成。我们还决定使用标准ISO 8601日期格式,因为这省去了很多解析麻烦。我们还在元素中添加了命名空间,以确保我们的元素可以在其他 XML 文档中使用。

3.1.2. 员工

场景中还有员工的概念。其 XML 形式如下

<Employee xmlns="http://mycompany.com/hr/schemas">
    <Number>42</Number>
    <FirstName>Arjen</FirstName>
    <LastName>Poutsma</LastName>
</Employee>

我们使用了与之前相同的命名空间。如果此 <Employee/> 元素可以在其他场景中使用,那么使用不同的命名空间(例如 http://example.com/employees/schemas)可能更有意义。

3.1.3. 假期请求

holiday 元素和 employee 元素都可以放入 <HolidayRequest/>

<HolidayRequest xmlns="http://mycompany.com/hr/schemas">
    <Holiday>
        <StartDate>2006-07-03</StartDate>
        <EndDate>2006-07-07</EndDate>
    </Holiday>
    <Employee>
        <Number>42</Number>
        <FirstName>Arjen</FirstName>
        <LastName>Poutsma</LastName>
    </Employee>
</HolidayRequest>

这两个元素的顺序无关紧要:<Employee/> 可以是第一个元素。重要的是所有数据都在那里。事实上,数据是唯一重要的:我们采取数据驱动的方法。

3.2. 数据契约

现在我们已经看到了一些可以使用的 XML 数据示例,将其形式化为模式是有意义的。此数据契约定义了我们接受的消息格式。有四种不同的方法来定义 XML 的此类契约

DTDs 的命名空间支持有限,因此不适用于 Web 服务。Relax NG 和 Schematron 比 XML Schema 更容易。不幸的是,它们在各个平台上的支持范围不广。因此,我们使用 XML Schema。

到目前为止,创建 XSD 的最简单方法是从示例文档推断它。任何好的 XML 编辑器或 Java IDE 都提供此功能。基本上,这些工具使用一些示例 XML 文档来生成一个模式,该模式验证所有这些文档。最终结果当然需要打磨,但它是一个很好的起点。

使用前面描述的示例,我们最终得到以下生成的模式

<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
        elementFormDefault="qualified"
        targetNamespace="http://mycompany.com/hr/schemas"
        xmlns:hr="http://mycompany.com/hr/schemas">
    <xs:element name="HolidayRequest">
        <xs:complexType>
            <xs:sequence>
                <xs:element ref="hr:Holiday"/>
                <xs:element ref="hr:Employee"/>
            </xs:sequence>
        </xs:complexType>
    </xs:element>
    <xs:element name="Holiday">
        <xs:complexType>
            <xs:sequence>
                <xs:element ref="hr:StartDate"/>
                <xs:element ref="hr:EndDate"/>
            </xs:sequence>
        </xs:complexType>
    </xs:element>
    <xs:element name="StartDate" type="xs:NMTOKEN"/>
    <xs:element name="EndDate" type="xs:NMTOKEN"/>
    <xs:element name="Employee">
        <xs:complexType>
            <xs:sequence>
                <xs:element ref="hr:Number"/>
                <xs:element ref="hr:FirstName"/>
                <xs:element ref="hr:LastName"/>
            </xs:sequence>
        </xs:complexType>
    </xs:element>
    <xs:element name="Number" type="xs:integer"/>
    <xs:element name="FirstName" type="xs:NCName"/>
    <xs:element name="LastName" type="xs:NCName"/>
</xs:schema>

这个生成的模式可以改进。首先要注意的是,每个类型都有一个根级元素声明。这意味着 Web 服务应该能够接受所有这些元素作为数据。这是不可取的:我们只想接受 <HolidayRequest/>。通过删除包装元素标签(从而保留类型)并内联结果,我们可以实现这一点,如下所示

<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
        xmlns:hr="http://mycompany.com/hr/schemas"
        elementFormDefault="qualified"
        targetNamespace="http://mycompany.com/hr/schemas">
    <xs:element name="HolidayRequest">
        <xs:complexType>
            <xs:sequence>
                <xs:element name="Holiday" type="hr:HolidayType"/>
                <xs:element name="Employee" type="hr:EmployeeType"/>
            </xs:sequence>
        </xs:complexType>
    </xs:element>
    <xs:complexType name="HolidayType">
        <xs:sequence>
            <xs:element name="StartDate" type="xs:NMTOKEN"/>
            <xs:element name="EndDate" type="xs:NMTOKEN"/>
        </xs:sequence>
    </xs:complexType>
    <xs:complexType name="EmployeeType">
        <xs:sequence>
            <xs:element name="Number" type="xs:integer"/>
            <xs:element name="FirstName" type="xs:NCName"/>
            <xs:element name="LastName" type="xs:NCName"/>
        </xs:sequence>
    </xs:complexType>
</xs:schema>

该模式仍然存在一个问题:使用这样的模式,您可以预期以下消息会进行验证

<HolidayRequest xmlns="http://mycompany.com/hr/schemas">
    <Holiday>
        <StartDate>this is not a date</StartDate>
        <EndDate>neither is this</EndDate>
    </Holiday>
    PlainText Section qName:lineannotation level:4, chunks:[<, !-- ... --, >] attrs:[:]
</HolidayRequest>

显然,我们必须确保开始日期和结束日期确实是日期。XML Schema 有一个很好的内置 date 类型我们可以使用。我们还将 NCName 更改为 string 实例。最后,我们将 <HolidayRequest/> 中的 sequence 更改为 all。这告诉 XML 解析器 <Holiday/><Employee/> 的顺序不重要。我们最终的 XSD 现在看起来像以下列表

<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
        xmlns:hr="http://mycompany.com/hr/schemas"
        elementFormDefault="qualified"
        targetNamespace="http://mycompany.com/hr/schemas">
    <xs:element name="HolidayRequest">
        <xs:complexType>
            <xs:all>
                <xs:element name="Holiday" type="hr:HolidayType"/> (1)
                <xs:element name="Employee" type="hr:EmployeeType"/> (1)
            </xs:all>
        </xs:complexType>
    </xs:element>
    <xs:complexType name="HolidayType">
        <xs:sequence>
            <xs:element name="StartDate" type="xs:date"/> (2)
            <xs:element name="EndDate" type="xs:date"/> (2)
        </xs:sequence>
    </xs:complexType>
    <xs:complexType name="EmployeeType">
        <xs:sequence>
            <xs:element name="Number" type="xs:integer"/>
            <xs:element name="FirstName" type="xs:string"/> (3)
            <xs:element name="LastName" type="xs:string"/> (3)
        </xs:sequence>
    </xs:complexType>
</xs:schema>
1 all 告诉 XML 解析器 <Holiday/><Employee/> 的顺序不重要。
2 我们对 <StartDate/><EndDate/> 使用 xs:date 数据类型(由年、月、日组成)。
3 xs:string 用于名字和姓氏。

我们将此文件保存为 hr.xsd

3.3. 服务契约

服务契约通常表示为 WSDL 文件。请注意,在 Spring-WS 中,不需要手动编写 WSDL。根据 XSD 和一些约定,Spring-WS 可以为您创建 WSDL,如实现端点一节所述。本节的其余部分演示了如何手动编写 WSDL。您可能希望跳到下一节

我们从 WSDL 的标准前导开始,并通过导入现有的 XSD。为了将模式与定义分开,我们为 WSDL 定义使用了一个单独的命名空间:http://mycompany.com/hr/definitions。以下列表显示了前导

<wsdl:definitions xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
                  xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
                  xmlns:schema="http://mycompany.com/hr/schemas"
                  xmlns:tns="http://mycompany.com/hr/definitions"
                  targetNamespace="http://mycompany.com/hr/definitions">
    <wsdl:types>
        <xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema">
            <xsd:import namespace="http://mycompany.com/hr/schemas" schemaLocation="hr.xsd"/>
        </xsd:schema>
    </wsdl:types>

接下来,我们根据编写的模式类型添加我们的消息。我们只有一条消息,即我们放入模式中的 <HolidayRequest/>

    <wsdl:message name="HolidayRequest">
        <wsdl:part element="schema:HolidayRequest" name="HolidayRequest"/>
    </wsdl:message>

我们将消息作为操作添加到端口类型中

    <wsdl:portType name="HumanResource">
        <wsdl:operation name="Holiday">
            <wsdl:input message="tns:HolidayRequest" name="HolidayRequest"/>
        </wsdl:operation>
    </wsdl:portType>

该消息完成了 WSDL 的抽象部分(即接口),并留下了具体部分。具体部分由 binding(告诉客户端如何调用您刚刚定义的操作)和 service(告诉客户端在哪里调用它)组成。

添加具体部分是相当标准的。为此,请参考您之前定义的抽象部分,确保对 soap:binding 元素使用 document/literal (rpc/encoded 已弃用),为操作选择一个 soapAction (在此示例中为 http://mycompany.com/RequestHoliday,但任何 URI 都适用),并确定您希望请求到达的 location URL (在此示例中为 http://mycompany.com/humanresources)

<wsdl:definitions xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
                  xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
                  xmlns:schema="http://mycompany.com/hr/schemas"
                  xmlns:tns="http://mycompany.com/hr/definitions"
                  targetNamespace="http://mycompany.com/hr/definitions">
    <wsdl:types>
        <xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema">
            <xsd:import namespace="http://mycompany.com/hr/schemas"              (1)
                schemaLocation="hr.xsd"/>
        </xsd:schema>
    </wsdl:types>
    <wsdl:message name="HolidayRequest">                                         (2)
        <wsdl:part element="schema:HolidayRequest" name="HolidayRequest"/>       (3)
    </wsdl:message>
    <wsdl:portType name="HumanResource">                                         (4)
        <wsdl:operation name="Holiday">
            <wsdl:input message="tns:HolidayRequest" name="HolidayRequest"/>     (2)
        </wsdl:operation>
    </wsdl:portType>
    <wsdl:binding name="HumanResourceBinding" type="tns:HumanResource">          (4)(5)
        <soap:binding style="document"                                           (6)
            transport="http://schemas.xmlsoap.org/soap/http"/>                   (7)
        <wsdl:operation name="Holiday">
            <soap:operation soapAction="http://mycompany.com/RequestHoliday"/>   (8)
            <wsdl:input name="HolidayRequest">
                <soap:body use="literal"/>                                       (6)
            </wsdl:input>
        </wsdl:operation>
    </wsdl:binding>
    <wsdl:service name="HumanResourceService">
        <wsdl:port binding="tns:HumanResourceBinding" name="HumanResourcePort">  (5)
            <soap:address location="https://:8080/holidayService/"/>     (9)
        </wsdl:port>
    </wsdl:service>
</wsdl:definitions>
1 我们导入了数据契约中定义的模式。
2 我们定义了 HolidayRequest 消息,它在 portType 中使用。
3 HolidayRequest 类型在模式中定义。
4 我们定义了 HumanResource 端口类型,它在 binding 中使用。
5 我们定义了 HumanResourceBinding 绑定,它在 port 中使用。
6 我们使用文档/字面样式。
7 字面量 http://schemas.xmlsoap.org/soap/http 表示 HTTP 传输。
8 soapAction 属性表示将随每个请求发送的 SOAPAction HTTP 头。
9 https://:8080/holidayService/ 地址是 Web 服务可以调用的 URL。

上面的列表显示了最终的 WSDL。我们将在下一节中介绍如何实现生成的模式和 WSDL。

3.4. 创建项目

在本节中,我们使用 Maven 为我们创建初始项目结构。这样做并非必需,但可以大大减少我们为设置 HolidayService 必须编写的代码量。

以下命令使用 Spring-WS 原型(即项目模板)为我们创建一个 Maven Web 应用程序项目

mvn archetype:create -DarchetypeGroupId=org.springframework.ws \
  -DarchetypeArtifactId=spring-ws-archetype \
  -DarchetypeVersion= \
  -DgroupId=com.mycompany.hr \
  -DartifactId=holidayService

上述命令创建一个名为 holidayService 的新目录。此目录中有一个 src/main/webapp 目录,其中包含 WAR 文件的根目录。您可以在此处找到标准的 Web 应用程序部署描述符 ('WEB-INF/web.xml'),它定义了一个 Spring-WS MessageDispatcherServlet 并将所有传入请求映射到此 servlet

<web-app xmlns="http://java.sun.com/xml/ns/j2ee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee
             http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"
         version="2.4">

    <display-name>MyCompany HR Holiday Service</display-name>

    <!-- take special notice of the name of this servlet -->
    <servlet>
        <servlet-name>spring-ws</servlet-name>
        <servlet-class>org.springframework.ws.transport.http.MessageDispatcherServlet</servlet-class>
    </servlet>

    <servlet-mapping>
        <servlet-name>spring-ws</servlet-name>
        <url-pattern>/*</url-pattern>
    </servlet-mapping>

</web-app>

除了上述 WEB-INF/web.xml 文件之外,您还需要另一个 Spring-WS 特定配置文件,名为 WEB-INF/spring-ws-servlet.xml。此文件包含所有 Spring-WS 特定 bean,例如 EndPointsWebServiceMessageReceivers,并用于创建新的 Spring 容器。此文件的名称派生自伴随的 servlet 名称(在此示例中为 'spring-ws'),并附加了 -servlet.xml。因此,如果您定义一个名为 'dynamite'MessageDispatcherServlet,则 Spring-WS 特定配置文件的名称将变为 WEB-INF/dynamite-servlet.xml

(您可以在[tutorial.example.sws-conf-file]中查看此示例的 WEB-INF/spring-ws-servlet.xml 文件内容。)

创建项目结构后,您可以将上一节中的模式和 WSDL 放入 'WEB-INF/' 文件夹中。

3.5. 实现端点

在 Spring-WS 中,您实现端点以处理传入的 XML 消息。端点通常通过使用 @Endpoint 注解来注解类来创建。在此端点类中,您可以创建一个或多个处理传入请求的方法。方法签名可以非常灵活。您可以包含几乎任何与传入 XML 消息相关的参数类型,正如我们将在本章后面解释的那样。

3.5.1. 处理 XML 消息

在此示例应用程序中,我们使用 JDom 2 处理 XML 消息。我们还使用 XPath,因为它允许我们选择 XML JDOM 树的特定部分,而不需要严格的模式一致性。

以下列表显示了定义我们的假期端点的类

package com.mycompany.hr.ws;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;

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.server.endpoint.annotation.RequestPayload;

import com.mycompany.hr.service.HumanResourceService;
import org.jdom2.Element;
import org.jdom2.JDOMException;
import org.jdom2.Namespace;
import org.jdom2.filter.Filters;
import org.jdom2.xpath.XPathExpression;
import org.jdom2.xpath.XPathFactory;

@Endpoint                                                                                     (1)
public class HolidayEndpoint {

    private static final String NAMESPACE_URI = "http://mycompany.com/hr/schemas";

    private XPathExpression<Element> startDateExpression;

    private XPathExpression<Element> endDateExpression;

    private XPathExpression<Element> firstNameExpression;

    private XPathExpression<Element> lastNameExpression;

    private HumanResourceService humanResourceService;

    @Autowired                                                                                (2)
    public HolidayEndpoint(HumanResourceService humanResourceService) throws JDOMException {
        this.humanResourceService = humanResourceService;

        Namespace namespace = Namespace.getNamespace("hr", NAMESPACE_URI);
        XPathFactory xPathFactory = XPathFactory.instance();
        startDateExpression = xPathFactory.compile("//hr:StartDate", Filters.element(), null, namespace);
        endDateExpression = xPathFactory.compile("//hr:EndDate", Filters.element(), null, namespace);
        firstNameExpression = xPathFactory.compile("//hr:FirstName", Filters.element(), null, namespace);
        lastNameExpression = xPathFactory.compile("//hr:LastName", Filters.element(), null, namespace);
    }

    @PayloadRoot(namespace = NAMESPACE_URI, localPart = "HolidayRequest")                      (3)
    public void handleHolidayRequest(@RequestPayload Element holidayRequest) throws Exception {(4)
        Date startDate = parseDate(startDateExpression, holidayRequest);
        Date endDate = parseDate(endDateExpression, holidayRequest);
        String name = firstNameExpression.evaluateFirst(holidayRequest).getText() + " " + lastNameExpression.evaluateFirst(holidayRequest).getText();

        humanResourceService.bookHoliday(startDate, endDate, name);
    }

    private Date parseDate(XPathExpression<Element> expression, Element element) throws ParseException {
        Element result = expression.evaluateFirst(element);
        if (result != null) {
            SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
            return dateFormat.parse(result.getText());
        } else {
            throw new IllegalArgumentException("Could not evaluate [" + expression + "] on [" + element + "]");
        }
    }

}
1 HolidayEndpoint 使用 @Endpoint 注解。这会将此类标记为一种特殊的 @Component,适用于在 Spring-WS 中处理 XML 消息,并使其也适合进行组件扫描。
2 HolidayEndpoint 需要 HumanResourceService 业务服务才能运行,因此我们在构造函数中注入了依赖项并使用 @Autowired 注解它。接下来,我们使用 JDOM2 API 设置 XPath 表达式。有四个表达式://hr:StartDate 用于提取 <StartDate> 文本值,//hr:EndDate 用于提取结束日期,以及两个用于提取员工姓名。
3 @PayloadRoot 注解告诉 Spring-WS handleHolidayRequest 方法适用于处理 XML 消息。此方法可以处理的消息类型由注解值指示。在此示例中,它可以处理具有 HolidayRequest 本地部分和 http://mycompany.com/hr/schemas 命名空间的 XML 元素。有关将消息映射到端点的更多信息在下一节中提供。
4 handleHolidayRequest(..) 方法是主要的处理方法,它会传入来自传入 XML 消息的 <HolidayRequest/> 元素。@RequestPayload 注解表示 holidayRequest 参数应该映射到请求消息的有效负载。我们使用 XPath 表达式从 XML 消息中提取字符串值,并使用 SimpleDateFormatparseData 方法)将这些值转换为 Date 对象。有了这些值,我们就可以在业务服务上调用一个方法。通常,这会导致启动数据库事务并更改数据库中的一些记录。最后,我们定义了一个 void 返回类型,它告诉 Spring-WS 我们不想发送响应消息。如果我们需要响应消息,我们可以返回一个 JDOM 元素来表示响应消息的有效负载。

使用 JDOM 只是处理 XML 的选项之一。其他选项包括 DOM、dom4j、XOM、SAX 和 StAX,以及 JAXB、Castor、XMLBeans、JiBX 和 XStream 等编组技术,如下一章所述。我们选择 JDOM 是因为它允许我们访问原始 XML,并且它基于类(而不是像 W3C DOM 和 dom4j 那样基于接口和工厂方法),这使得代码更简洁。我们使用 XPath 是因为它比编组技术更健壮。只要我们能找到日期和名称,我们就不需要严格的模式一致性。

因为我们使用 JDOM,所以我们必须向项目根目录中的 Maven pom.xml 添加一些依赖项。以下是 POM 的相关部分

<dependencies>
    <dependency>
        <groupId>org.springframework.ws</groupId>
        <artifactId>spring-ws-core</artifactId>
        <version></version>
    </dependency>
    <dependency>
        <groupId>jdom</groupId>
        <artifactId>jdom</artifactId>
        <version>2.0.1</version>
    </dependency>
    <dependency>
        <groupId>jaxen</groupId>
        <artifactId>jaxen</artifactId>
        <version>1.1</version>
    </dependency>
</dependencies>

以下是我们在 spring-ws-servlet.xml Spring XML 配置文件中使用组件扫描配置这些类的方法。我们还指示 Spring-WS 使用注解驱动的端点,使用 <sws:annotation-driven> 元素。

<beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:context="http://www.springframework.org/schema/context"
  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
  http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd">

  <context:component-scan base-package="com.mycompany.hr"/>

  <sws:annotation-driven/>

</beans>

3.5.2. 将消息路由到端点

在编写端点时,我们还使用了 @PayloadRoot 注解来指示 handleHolidayRequest 方法可以处理哪种消息。在 Spring-WS 中,这个过程是 EndpointMapping 的职责。在这里,我们通过使用 PayloadRootAnnotationMethodEndpointMapping 根据消息内容路由消息。以下列表显示了我们之前使用的注解

@PayloadRoot(namespace = "http://mycompany.com/hr/schemas", localPart = "HolidayRequest")

上述示例中显示的注解基本上意味着,每当收到具有命名空间 http://mycompany.com/hr/schemas 和本地名称 HolidayRequest 的 XML 消息时,它都会路由到 handleHolidayRequest 方法。通过在配置中使用 <sws:annotation-driven> 元素,我们启用了 @PayloadRoot 注解的检测。在一个端点中拥有多个相关处理方法(每个方法处理不同的 XML 消息)是可能(并且很常见)的。

还有其他将端点映射到 XML 消息的方法,如下一章所述。

3.5.3. 提供服务和存根实现

现在我们有了端点,我们需要 HumanResourceService 及其实现供 HolidayEndpoint 使用。以下列表显示了 HumanResourceService 接口

package com.mycompany.hr.service;

import java.util.Date;

public interface HumanResourceService {
    void bookHoliday(Date startDate, Date endDate, String name);
}

为了教程目的,我们使用 HumanResourceService 的简单存根实现

package com.mycompany.hr.service;

import java.util.Date;

import org.springframework.stereotype.Service;

@Service                                                                 (1)
public class StubHumanResourceService implements HumanResourceService {
    public void bookHoliday(Date startDate, Date endDate, String name) {
        System.out.println("Booking holiday for [" + startDate + "-" + endDate + "] for [" + name + "] ");
    }
}
1 StubHumanResourceService 使用 @Service 注解。这会将此类标记为业务外观,使其成为 HolidayEndpoint@Autowired 注入的候选对象。

3.6. 发布 WSDL

最后,我们需要发布 WSDL。如服务契约中所述,我们不需要自己编写 WSDL。Spring-WS 可以根据一些约定生成一个。以下是我们定义生成方式的方法

<sws:dynamic-wsdl id="holiday"                                (1)
    portTypeName="HumanResource"                              (3)
    locationUri="/holidayService/"                            (4)
    targetNamespace="http://mycompany.com/hr/definitions">    (5)
  <sws:xsd location="/WEB-INF/hr.xsd"/>                       (2)
</sws:dynamic-wsdl>
1 id 确定可以检索 WSDL 的 URL。在此示例中,idholiday,这意味着 WSDL 可以作为 holiday.wsdl 在 servlet 上下文中检索。完整 URL 是 https://:8080/holidayService/holiday.wsdl
2 接下来,我们将 WSDL 端口类型设置为 HumanResource
3 我们设置了可以访问服务的 location:/holidayService/。我们使用相对 URI,并指示框架将其动态转换为绝对 URI。因此,如果服务部署到不同的上下文,我们不必手动更改 URI。有关更多信息,请参见名为“自动 WSDL 暴露”的章节。为了使 location 转换生效,我们需要在 web.xml 中向 spring-ws servlet 添加一个 init 参数(如下面的列表所示)。
4 我们为 WSDL 定义本身定义了目标命名空间。设置此属性并非必需。如果未设置,WSDL 将具有与 XSD 模式相同的命名空间。
5 xsd 元素引用了我们在数据契约中定义的人力资源模式。我们将模式放置在应用程序的 WEB-INF 目录中。

以下列表显示了如何添加 init 参数

<init-param>
  <param-name>transformWsdlLocations</param-name>
  <param-value>true</param-value>
</init-param>

您可以使用 mvn install 创建 WAR 文件。如果您部署应用程序(到 Tomcat、Jetty 等),并将浏览器指向此位置,您将看到生成的 WSDL。此 WSDL 已准备好供客户端使用,例如 soapUI 或其他 SOAP 框架。

本教程到此结束。教程代码可以在 Spring-WS 的完整发行版中找到。如果您想继续,请查看发行版中包含的 echo 示例应用程序。之后,查看 airline 示例,它稍微复杂一些,因为它使用了 JAXB、WS-Security、Hibernate 和事务性服务层。最后,您可以阅读参考文档的其余部分。

II. 参考

本参考文档部分详细介绍了构成 Spring Web Services 的各种组件。这包括讨论客户端和服务器端 WS 共享部分的一章、专门讨论编写服务器端 Web 服务细节的章节、关于在客户端使用 Web 服务的章节,以及关于使用WS-Security的章节。

4. 共享组件

本章探讨了客户端和服务器端 Spring-WS 开发之间共享的组件。这些接口和类代表了 Spring-WS 的构建块,因此您需要了解它们的功能,即使您不直接使用它们。

4.1. Web 服务消息

本节描述了 Spring-WS 使用的消息和消息工厂。

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

org.w3c.dom.Node

javax.xml.transform.dom.DOMResult

org.w3c.dom.Node

javax.xml.transform.sax.SAXSource

org.xml.sax.InputSourceorg.xml.sax.XMLReader

javax.xml.transform.sax.SAXResult

org.xml.sax.ContentHandler

javax.xml.transform.stream.StreamSource

java.io.File, java.io.InputStream, 或 java.io.Reader

javax.xml.transform.stream.StreamResult

java.io.File, java.io.OutputStream, 或 java.io.Writer

除了从有效负载读取和写入有效负载之外,Web 服务消息还可以将自身写入输出流。

4.1.2. SoapMessage

SoapMessageWebServiceMessage 的子类。它包含 SOAP 特定的方法,例如获取 SOAP 头、SOAP 错误等。通常,您的代码不应依赖于 SoapMessage,因为 SOAP 正文的内容(消息的有效负载)可以通过使用 WebServiceMessage 中的 getPayloadSource()getPayloadResult() 获取。只有在需要执行 SOAP 特定操作(例如添加头、获取附件等)时,才需要将 WebServiceMessage 转换为 SoapMessage

4.1.3. 消息工厂

具体消息实现由 WebServiceMessageFactory 创建。此工厂可以创建空消息或从输入流读取消息。WebServiceMessageFactory 有两个具体实现。一个基于 SAAJ,即 Java 的带附件 SOAP API。另一个基于 Axis 2 的 AXIOM (AXis Object Model)。

SaajSoapMessageFactory

SaajSoapMessageFactory 使用 Java 的带附件 SOAP API (SAAJ) 创建 SoapMessage 实现。SAAJ 是 J2EE 1.4 的一部分,因此它应该在大多数现代应用程序服务器下都受支持。以下是常见应用程序服务器提供的 SAAJ 版本概述

应用程序服务器 SAAJ 版本

BEA WebLogic 8

1.1

BEA WebLogic 9

1.1/1.21

IBM WebSphere 6

1.2

SUN Glassfish 1

1.3

1Weblogic 9 在 SAAJ 1.2 实现中存在一个已知错误:它实现了所有 1.2 接口,但在调用时会抛出 UnsupportedOperationException。Spring Web Services 有一个解决方法:它在 WebLogic 9 上运行时使用 SAAJ 1.1。

此外,Java SE 6 包含 SAAJ 1.3。您可以按如下方式配置 SaajSoapMessageFactory

<bean id="messageFactory" class="org.springframework.ws.soap.saaj.SaajSoapMessageFactory" />
SAAJ 基于 DOM,即文档对象模型。这意味着所有 SOAP 消息都存储在内存中。对于较大的 SOAP 消息,这可能无法提供高性能。在这种情况下,AxiomSoapMessageFactory 可能更适用。
AxiomSoapMessageFactory

AxiomSoapMessageFactory 使用 AXis 2 对象模型 (AXIOM) 创建 SoapMessage 实现。AXIOM 基于 StAX,即 XML 流 API。StAX 提供了一种基于拉取的机制来读取 XML 消息,这对于较大的消息可能更高效。

为了提高 AxiomSoapMessageFactory 上的读取性能,您可以将 payloadCaching 属性设置为 false(默认为 true)。这样做会导致 SOAP 主体的内容直接从套接字流读取。启用此设置后,有效负载只能读取一次。这意味着您必须确保消息的任何预处理(日志记录或其他工作)都不会消耗它。

您可以按如下方式使用 AxiomSoapMessageFactory

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

除了有效负载缓存之外,AXIOM 还支持完整的流式消息,如 StreamingWebServiceMessage 中定义的那样。这意味着您可以直接在响应消息上设置有效负载,而不是将其写入 DOM 树或缓冲区。

当处理程序方法返回 JAXB2 支持的对象时,将使用 AXIOM 的完整流式传输。它会自动将此编组对象设置为响应消息,并在响应传出时将其写入传出套接字流。

有关完整流式传输的更多信息,请参阅 StreamingWebServiceMessageStreamingPayload 的类级 Javadoc。

SOAP 1.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>

在前面的示例中,我们定义了一个 SaajSoapMessageFactory,它只接受 SOAP 1.2 消息。

尽管两个版本的 SOAP 格式非常相似,但 1.2 版本与 1.1 不向后兼容,因为它使用不同的 XML 命名空间。SOAP 1.1 和 1.2 之间的其他主要区别包括故障结构的不同以及 SOAPAction HTTP 标头实际上已弃用,尽管它们仍然有效。

关于 SOAP 版本号(或通常的 WS-* 规范版本号)需要注意的一点是,规范的最新版本通常不是最受欢迎的版本。对于 SOAP,这意味着(目前)最佳版本是 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());
           }
        });
      PlainText Section qName; // do something with the list of Contact objects
   }
}

与 Spring JDBC 的 RowMapper 中的行映射类似,每个结果节点都使用匿名内部类进行映射。在此示例中,我们创建一个 Contact 对象,我们稍后会使用它。

4.3.2. XPathTemplate

XPathExpression 仅允许您评估单个预编译表达式。一种更灵活(尽管更慢)的替代方案是 XpathTemplate。此类遵循 Spring 中常用的模板模式(JdbcTemplateJmsTemplate 等)。以下列表显示了一个示例

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 服务时,查看(SOAP)消息到达或发送之前的内容非常有用。Spring Web Services 通过标准 Commons Logging 接口提供此功能。

请务必使用 Commons Logging 1.1 或更高版本。早期版本存在类加载问题,并且不与 Log4J TRACE 级别集成。

要记录所有服务器端消息,请将 org.springframework.ws.server.MessageTracing 日志级别设置为 DEBUGTRACE。在 DEBUG 级别,仅记录有效负载根元素。在 TRACE 级别,记录整个消息内容。如果您只想记录已发送的消息,请使用 org.springframework.ws.server.MessageTracing.sent 记录器。同样,您可以使用 org.springframework.ws.server.MessageTracing.received 记录器来记录仅接收到的消息。

在客户端,存在类似的日志记录器: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] ...

5. 使用 Spring-WS 创建 Web 服务

Spring-WS 的服务器端支持围绕 MessageDispatcher 设计,该调度程序将传入消息分派到端点,具有可配置的端点映射、响应生成和端点拦截。端点通常使用 @Endpoint 注解,并具有一个或多个处理方法。这些方法通过检查消息的某些部分(通常是有效负载)来处理传入的 XML 请求消息,并创建某种响应。您可以使用另一个注解(通常是 @PayloadRoot)来注解方法,以指示它可以处理哪种类型的消息。

Spring-WS 的 XML 处理非常灵活。端点可以从 Spring-WS 支持的大量 XML 处理库中进行选择,包括

  • DOM 族:W3C DOM、JDOM、dom4j 和 XOM

  • SAX 或 StAX:为了更快的性能

  • XPath:从消息中提取信息

  • 编组技术(JAXB、Castor、XMLBeans、JiBX 或 XStream):将 XML 转换为对象,反之亦然

5.1. MessageDispatcher

Spring-WS 的服务器端围绕一个核心类设计,该类将传入的 XML 消息分派到端点。Spring-WS 的 MessageDispatcher 极其灵活,允许您使用任何类型的类作为端点,只要它可以在 Spring IoC 容器中配置。从某种意义上说,消息调度器类似于 Spring 的 DispatcherServlet,即 Spring Web MVC 中使用的“前端控制器”。

以下序列图显示了 MessageDispatcher 的处理和分发流程

sequence

MessageDispatcher 设置为可用并且针对该特定调度程序传入请求时,MessageDispatcher 开始处理请求。以下过程描述了 MessageDispatcher 如何处理请求

  1. 搜索配置的 EndpointMapping(s) 以查找适当的端点。如果找到端点,则调用与端点关联的调用链(预处理器、后处理器和端点)以创建响应。

  2. 为端点找到一个合适的适配器。MessageDispatcher 将委托给此适配器来调用端点。

  3. 如果返回响应,则将其发送出去。如果没有返回响应(这可能是由于预处理器或后处理器拦截请求,例如出于安全原因),则不发送响应。

在请求处理期间抛出的异常将被应用程序上下文中声明的任何端点异常解析器捕获。使用这些异常解析器允许您在抛出此类异常时定义自定义行为(例如返回 SOAP 错误)。

MessageDispatcher 有几个属性用于设置端点适配器、映射异常解析器。但是,设置这些属性不是必需的,因为调度程序会自动检测应用程序上下文中注册的所有类型。您只有在需要覆盖检测时才应设置这些属性。

消息调度器操作的是消息上下文,而不是传输特定的输入流和输出流。因此,传输特定的请求需要读入 MessageContext。对于 HTTP,这是通过 WebServiceMessageReceiverHandlerAdapter(一个 Spring Web HandlerInterceptor)完成的,以便可以将 MessageDispatcher 连接到标准的 DispatcherServlet 中。但是,有一种更方便的方法可以做到这一点,如MessageDispatcherServlet中所示。

5.2. 传输

Spring Web Services 支持多种传输协议。最常见的是 HTTP 传输,为此提供了一个自定义 servlet,但您也可以通过 JMS 甚至电子邮件发送消息。

5.2.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,例如端点、编组器等。

作为 web.xml 的替代方案,如果您在 Servlet 3+ 环境中运行,您可以以编程方式配置 Spring-WS。为此,Spring-WS 提供了许多抽象基类,它们扩展了 Spring Framework 中找到的 WebApplicationInitializer 接口。如果您还使用 @Configuration 类来定义 bean,则应该扩展 AbstractAnnotationConfigMessageDispatcherServletInitializer

public class MyServletInitializer
    extends AbstractAnnotationConfigMessageDispatcherServletInitializer {

    @Override
    protected Class<?>[] getRootConfigClasses() {
        return new Class[]{MyRootConfig.class};
    }

    @Override
    protected Class<?>[] getServletConfigClasses() {
        return new Class[]{MyEndpointConfig.class};
    }

}

在前面的示例中,我们告诉 Spring 端点 bean 定义可以在 MyEndpointConfig 类(这是一个 @Configuration 类)中找到。其他 bean 定义(通常是服务、存储库等)可以在 MyRootConfig 类中找到。默认情况下,AbstractAnnotationConfigMessageDispatcherServletInitializer 将 servlet 映射到两个模式:/services*.wsdl,尽管您可以通过覆盖 getServletMappings() 方法来更改此设置。有关 MessageDispatcherServlet 编程配置的更多详细信息,请参阅 AbstractMessageDispatcherServletInitializerAbstractAnnotationConfigMessageDispatcherServletInitializer 的 Javadoc。

自动 WSDL 暴露

MessageDispatcherServlet 会自动检测其 Spring 容器中定义的任何 WsdlDefinition bean。所有检测到的 WsdlDefinition bean 也通过 WsdlDefinitionHandlerAdapter 暴露。这是一种通过定义一些 bean 来向客户端暴露 WSDL 的便捷方式。

例如,考虑以下在 Spring-WS 配置文件 (/WEB-INF/[servlet-name]-servlet.xml) 中定义的 <static-wsdl> 定义。请注意 id 属性的值,因为它在暴露 WSDL 时使用。

<sws:static-wsdl id="orders" location="orders.wsdl"/>

或者,它也可以是 @Configuration 类中的 @Bean 方法

@Bean
public SimpleWsdl11Definition orders() {
	return new SimpleWsdl11Definition(new ClassPathResource("orders.wsdl"));
}

您可以通过以下形式的 URL 访问类路径中 orders.wsdl 文件中定义的 WSDL (请根据需要替换主机、端口和 servlet 上下文路径)

https://:8080/spring-ws/orders.wsdl
所有 WsdlDefinition bean 定义都由 MessageDispatcherServlet 在其 bean 名称下公开,后缀为 `.wsdl`。因此,如果 bean 名称为 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>

如果您使用 AbstractAnnotationConfigMessageDispatcherServletInitializer,启用转换就像覆盖 isTransformWsdlLocations() 方法以返回 true 一样简单。

请查阅 WsdlDefinitionHandlerAdapter 类的类级 Javadoc,以了解整个转换过程的更多信息。

作为手动编写 WSDL 并使用 <static-wsdl> 暴露它的替代方案,Spring Web Services 还可以从 XSD 模式生成 WSDL。这是发布 WSDL中显示的方法。以下应用程序上下文片段显示了如何创建此类动态 WSDL 文件

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

或者,您可以使用 Java @Bean 方法

@Bean
public DefaultWsdl11Definition orders() {
    DefaultWsdl11Definition definition = new DefaultWsdl11Definition();
    definition.setPortTypeName("Orders");
    definition.setLocationUri("https://:8080/ordersService/");
    definition.setSchema(new SimpleXsdSchema(new ClassPathResource("echo.xsd")));

    return definition;
}

<dynamic-wsdl> 元素依赖于 DefaultWsdl11Definition 类。这个定义类使用 org.springframework.ws.wsdl.wsdl11.provider 包中的 WSDL 提供程序和 ProviderBasedWsdl4jDefinition 类在首次请求时生成 WSDL。请参阅这些类的类级 Javadoc,以了解在必要时如何扩展此机制。

DefaultWsdl11Definition(因此,<dynamic-wsdl> 标签)通过约定从 XSD 模式构建 WSDL。它遍历模式中找到的所有 element 元素,并为所有元素创建 message。接下来,它为所有以定义的请求或响应后缀结尾的消息创建 WSDL operation。默认请求后缀为 Request。默认响应后缀为 Response,尽管可以通过分别设置 <dynamic-wsdl /> 上的 requestSuffixresponseSuffix 属性来更改这些后缀。它还根据操作构建 portTypebindingservice

例如,如果我们的 Orders.xsd 模式定义了 GetOrdersRequestGetOrdersResponse 元素,<dynamic-wsdl> 将创建 GetOrdersRequestGetOrdersResponse 消息以及 GetOrders 操作,该操作将放入 Orders 端口类型。

要使用多个模式,无论是通过包含还是导入,您都可以将 Commons XMLSchema 放在类路径上。如果 Commons XMLSchema 在类路径上,则 <dynamic-wsdl> 元素将遵循所有 XSD 导入和包含,并将它们作为单个 XSD 内联到 WSDL 中。这大大简化了模式的部署,同时仍然可以单独编辑它们。

尽管在运行时从 XSD 生成 WSDL 可能很方便,但这种方法存在一些缺点。首先,尽管我们努力保持 WSDL 生成过程在不同版本之间的一致性,但它仍然有可能(略微)发生变化。其次,生成速度有点慢,尽管一旦生成,WSDL 会被缓存以供以后引用。

因此,您应该仅在项目的开发阶段使用 <dynamic-wsdl>。我们建议使用浏览器下载生成的 WSDL,将其存储在项目中,并使用 <static-wsdl> 公开它。这是唯一能真正确保 WSDL 不随时间变化的方法。

5.2.2. 在 DispatcherServlet 中配置 Spring-WS

作为 MessageDispatcherServlet 的替代方案,您可以在标准 Spring-Web MVC DispatcherServlet 中配置 MessageDispatcher。默认情况下,DispatcherServlet 只能委托给 Controllers,但我们可以通过向 servlet 的 Web 应用程序上下文添加 WebServiceMessageReceiverHandlerAdapter 来指示它委托给 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.method.annotation.RequestMappingHandlerAdapter"/>

</beans>

请注意,通过显式添加 WebServiceMessageReceiverHandlerAdapter,调度程序 servlet 不会加载默认适配器,并且无法处理标准 Spring-MVC @Controllers。因此,我们将 RequestMappingHandlerAdapter 添加到末尾。

以类似的方式,您可以连接 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.2.3. JMS 传输

Spring Web Services 通过 Spring 框架提供的 JMS 功能支持服务器端 JMS 处理。Spring Web Services 提供了 WebServiceMessageListener,可插入到 MessageListenerContainer 中。此消息监听器需要 WebServiceMessageFactoryMessageDispatcher 才能运行。以下配置示例展示了这一点

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

5.2.4. 电子邮件传输

除了 HTTP 和 JMS,Spring Web Services 还提供服务器端电子邮件处理。此功能通过 MailMessageReceiver 类提供。该类监控 POP3 或 IMAP 文件夹,将电子邮件转换为 WebServiceMessage,并使用 SMTP 发送任何响应。您可以通过 storeUri 配置主机名,该属性指示要监控请求的邮件文件夹(通常是 POP3 或 IMAP 文件夹),并通过 transportUri 配置主机名,该属性指示用于发送响应的服务器(通常是 SMTP 服务器)。

您可以使用可插拔策略:MonitoringStrategy 来配置 MailMessageReceiver 如何监控传入消息。默认情况下,使用轮询策略,每五分钟轮询一次传入文件夹以查找新消息。您可以通过设置策略上的 pollingInterval 属性来更改此间隔。默认情况下,所有 MonitoringStrategy 实现都会删除已处理的消息。您可以通过设置 deleteMessages 属性来更改此设置。

作为效率较低的轮询方法的替代方案,有一种使用 IMAP IDLE 的监控策略。IDLE 命令是 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.2.5. 嵌入式 HTTP 服务器传输

Spring Web Services 提供了一个基于 Sun JRE 1.6 HTTP 服务器的传输。嵌入式 HTTP 服务器是一个独立服务器,配置简单。它为传统的 Servlet 容器提供了一种更轻量级的替代方案。

使用嵌入式 HTTP 服务器时,您不需要外部部署描述符(web.xml)。您只需定义一个服务器实例并将其配置为处理传入请求。Core 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.2.6. XMPP 传输

Spring Web Services 2.0 引入了对 XMPP(又称 Jabber)的支持。该支持基于 Smack 库。

Spring Web Services 对 XMPP 的支持与其它传输非常相似:WebServiceTemplate 有一个 XmppMessageSenderMessageDispatcher 有一个 XmppMessageReceiver

以下示例展示了如何设置服务器端 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.2.7. MTOM

MTOM 是用于向 Web 服务发送和接收二进制数据的机制。您可以通过 MTOM 示例了解如何使用 Spring WS 实现此功能。

5.3. 端点

端点是 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 返回类型,表示不发送响应消息。有关端点方法的更多信息,请参阅 @Endpoint 处理方法
4 getOrder 方法接受一个 OrderRequest(也用 @RequestPayload 注解)作为参数。此参数是 JAXB2 支持的对象(它用 @XmlRootElement 注解)。这意味着消息的有效载荷作为未解组的对象传递给此方法。SoapHeader 类型也作为参数给出。在调用时,此参数包含请求消息的 SOAP 头。该方法也用 @ResponsePayload 注解,表示返回值(Order)用作响应消息的有效载荷。有关端点方法的更多信息,请参阅 @Endpoint 处理方法
5 此端点的两个处理方法都使用 @PayloadRoot 标记,指示方法可以处理哪种请求消息:getOrder 方法针对有效载荷根元素的局部名称为 orderRequest 且命名空间 URI 为 http://samples 的请求调用。order 方法针对局部名称为 order 的请求调用。有关 @PayloadRoot 的更多信息,请参阅 端点映射

要启用对 @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.xsd
    http://www.springframework.org/schema/web-services
      http://www.springframework.org/schema/web-services/web-services.xsd">

  *<sws:annotation-driven />

</beans>

或者,如果您使用 @Configuration 类而不是 Spring XML,则可以使用 @EnableWs 注解您的配置类

@EnableWs
@Configuration
public class EchoConfig {

    // @Bean definitions go here

}

要自定义 @EnableWs 配置,您可以实现 WsConfigurer,或者更好地扩展 WsConfigurerAdapter

@Configuration
@EnableWs
@ComponentScan(basePackageClasses = { MyConfiguration.class })
public class MyConfiguration extends WsConfigurerAdapter {

  @Override
  public void addInterceptors(List<EndpointInterceptor> interceptors) {
    interceptors.add(new MyInterceptor());
  }

  @Override
  public void addArgumentResolvers(List<MethodArgumentResolver> argumentResolvers) {
    argumentResolvers.add(new MyArgumentResolver());
  }

  // More overridden methods ...
}

在接下来的几节中,将更详细地描述 @Endpoint 编程模型。

端点,像任何其他 Spring Bean 一样,默认情况下被作用域为单例。也就是说,每个容器只创建一个 bean 定义实例。作为单例意味着多个线程可以同时使用它,因此端点必须是线程安全的。如果您想使用不同的作用域,例如原型,请参阅 Spring 参考文档
Note that all abstract base classes provided in Spring-WS are thread safe, unless otherwise indicated in the class-level Javadoc.

5.3.1. @Endpoint 处理方法

为了让端点实际处理传入的 XML 消息,它需要有一个或多个处理方法。处理方法可以接受各种参数和返回类型。然而,它们通常有一个参数包含消息有效载荷,并返回响应消息的有效载荷(如果有的话)。本节介绍支持哪些参数和返回类型。

为了指示方法可以处理哪种消息,方法通常使用 @PayloadRoot@SoapAction 注解进行注解。您可以在 端点映射 中了解有关这些注解的更多信息。

以下示例显示了一个处理方法

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

order 方法接受一个 Element(用 @RequestPayload 注解)作为参数。这意味着消息的有效载荷作为 DOM 元素传递给此方法。该方法具有 void 返回类型,表示不发送响应消息。

处理方法参数

处理方法通常有一个或多个参数,这些参数引用传入 XML 消息的各个部分。最常见的是,处理方法有一个映射到消息有效载荷的单个参数,但它也可以映射到请求消息的其他部分,例如 SOAP 头。本节描述您可以在处理方法签名中使用的参数。

要将参数映射到请求消息的有效载荷,您需要使用 @RequestPayload 注解此参数。此注解告诉 Spring-WS 该参数需要绑定到请求有效载荷。

下表描述了支持的参数类型。它显示了支持的类型,参数是否应使用 @RequestPayload 注解,以及任何附加说明。

名称 支持的参数类型 @RequestPayload 是否必需? 附加说明

TrAX

javax.xml.transform.Source 及其子接口(DOMSourceSAXSourceStreamSourceStAXSource

默认启用。

W3C DOM

org.w3c.dom.Element

默认启用

dom4j

org.dom4j.Element

当 dom4j 位于类路径上时启用。

JDOM

org.jdom.Element

当 JDOM 位于类路径上时启用。

XOM

nu.xom.Element

当 XOM 位于类路径上时启用。

StAX

javax.xml.stream.XMLStreamReaderjavax.xml.stream.XMLEventReader

当 StAX 位于类路径上时启用。

XPath

任何 boolean、double、Stringorg.w3c.Nodeorg.w3c.dom.NodeList,或者可以由 Spring 转换服务String 转换的类型,并且用 @XPathParam 注解。

默认启用,请参阅名为 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 位于类路径上时启用。

OXM

Spring OXM Unmarshaller 支持的任何类型。

当指定 <sws:annotation-driven/>unmarshaller 属性时启用。

接下来的几个示例展示了可能的方法签名。以下方法使用请求消息的有效载荷作为 DOM org.w3c.dom.Element 调用

public void handle(@RequestPayload Element element)

以下方法使用请求消息的有效载荷作为 javax.xml.transform.dom.DOMSource 调用。header 参数绑定到请求消息的 SOAP 头。

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

以下方法使用请求消息的有效载荷解组到 MyJaxb2Object(用 @XmlRootElement 注解)中调用。消息的有效载荷也作为 DOM Element 给出。整个消息上下文作为第三个参数传递。

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

如您所见,在定义如何处理方法签名方面有许多可能性。您甚至可以扩展此机制以支持您自己的参数类型。请参阅 DefaultMethodEndpointAdapterMethodArgumentResolver 的 Javadoc 以了解如何操作。

@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

  • 字符串

  • 节点

  • NodeList

除了此列表之外,您还可以使用任何可以由 Spring 转换服务String 转换的类型。

处理方法返回类型

要发送响应消息,处理需要指定返回类型。如果不需要响应消息,方法可以声明 void 返回类型。最常见的是,返回类型用于创建响应消息的有效载荷。但是,您也可以映射到响应消息的其他部分。本节描述您可以在处理方法签名中使用的返回类型。

要将返回值映射到响应消息的有效载荷,您需要使用 @ResponsePayload 注解该方法。此注解告诉 Spring-WS 返回值需要绑定到响应有效载荷。

下表描述了支持的返回类型。它显示了支持的类型,参数是否应使用 @ResponsePayload 注解,以及任何附加说明。

名称 支持的返回类型 @ResponsePayload 是否必需? 附加说明

无响应

void

默认启用。

TrAX

javax.xml.transform.Source 及其子接口(DOMSourceSAXSourceStreamSourceStAXSource

默认启用。

W3C DOM

org.w3c.dom.Element

默认启用

dom4j

org.dom4j.Element

当 dom4j 位于类路径上时启用。

JDOM

org.jdom.Element

当 JDOM 位于类路径上时启用。

XOM

nu.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.4. 端点映射

端点映射负责将传入消息映射到适当的端点。一些端点映射默认启用,例如 PayloadRootAnnotationMethodEndpointMappingSoapActionAnnotationMethodEndpointMapping。但是,我们首先需要检查 EndpointMapping 的一般概念。

一个 EndpointMapping 交付一个 EndpointInvocationChain,其中包含与传入请求匹配的端点,并且可能还包含应用于请求和响应的端点拦截器列表。当请求到来时,MessageDispatcher 将其交给端点映射,让它检查请求并生成适当的 EndpointInvocationChain。然后 MessageDispatcher 调用链中的端点和任何拦截器。

可配置的端点映射概念非常强大,它可以选择性地包含拦截器(反过来,拦截器可以操作请求、响应或两者)。许多支持功能可以内置到自定义 EndpointMapping 实现中。例如,自定义端点映射不仅可以根据消息内容选择端点,还可以根据特定的 SOAP 头(或实际上是多个 SOAP 头)选择端点。

大多数端点映射继承自 AbstractEndpointMapping,它提供了一个“拦截器”属性,即要使用的拦截器列表。EndpointInterceptor拦截请求——EndpointInterceptor 接口中讨论。此外,还有 defaultEndpoint,它是当此端点映射未导致匹配端点时使用的默认端点。

端点中所述,@Endpoint 样式允许您在一个端点类中处理多个请求。这是 MethodEndpointMapping 的职责。此映射决定了应为传入请求消息调用哪个方法。

有两种端点映射可以将请求定向到方法:PayloadRootAnnotationMethodEndpointMappingSoapActionAnnotationMethodEndpointMapping。您可以通过在应用程序上下文中使用 <sws:annotation-driven/> 启用这两种方法。

PayloadRootAnnotationMethodEndpointMapping 使用 @PayloadRoot 注解,以及 localPartnamespace 元素,以特定限定名称标记方法。每当消息以该限定名称作为有效载荷根元素传入时,就会调用该方法。有关示例,请参阅上文

或者,SoapActionAnnotationMethodEndpointMapping 使用 @SoapAction 注解来标记具有特定 SOAP 动作的方法。每当消息带有此 SOAPAction 头传入时,就会调用该方法。

5.4.1. WS-Addressing

WS-Addressing 指定了一种传输无关的路由机制。它基于 ToAction SOAP 头,分别指示 SOAP 消息的目标和意图。此外,WS-Addressing 允许您定义返回地址(用于正常消息和故障)和唯一消息标识符,可用于关联。有关 WS-Addressing 的更多信息,请参阅 https://en.wikipedia.org/wiki/WS-Addressing。以下示例显示了一个 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

使用 AnnotationActionEndpointMapping

AnnotationActionEndpointMapping 类似于 SoapActionAnnotationMethodEndpointMapping,但使用 WS-Addressing 头而不是 SOAP Action 传输头。

要使用 AnnotationActionEndpointMapping,请使用 @Action 注解处理方法,类似于 @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 上一样。请参阅 URI 和传输

5.4.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 引用。

当您使用 @Configuration 类时,您可以从 WsConfigurerAdapter 扩展以添加拦截器

@Configuration
@EnableWs
public class MyWsConfiguration extends WsConfigurerAdapter {

  @Override
  public void addInterceptors(List<EndpointInterceptor> interceptors) {
    interceptors.add(new MyPayloadRootInterceptor());
  }

}

拦截器必须实现 org.springframework.ws.server 包中的 EndpointInterceptor 接口。此接口定义了三个方法,一个用于在实际端点处理之前处理请求消息,一个用于处理正常响应消息,一个用于处理故障消息。后两个方法在端点处理之后调用。这三个方法应提供足够的灵活性来执行各种预处理和后处理。

拦截器上的 handleRequest(..) 方法返回一个布尔值。您可以使用此方法中断或继续调用链的处理。当此方法返回 true 时,端点处理链将继续。当它返回 false 时,MessageDispatcher 将此解释为拦截器本身已处理好事情,并且不会继续处理调用链中的其他拦截器和实际端点。handleResponse(..)handleFault(..) 方法也具有布尔返回值。当这些方法返回 false 时,响应将不会发送回客户端。

有许多标准的 EndpointInterceptor 实现可用于您的 Web 服务。此外,还有 XwsSecurityInterceptor,在 XwsSecurityInterceptor 中描述。

PayloadLoggingInterceptorSoapEnvelopeLoggingInterceptor

开发 Web 服务时,记录传入和传出的 XML 消息会很有用。Spring WS 通过 PayloadLoggingInterceptorSoapEnvelopeLoggingInterceptor 类简化了此操作。前者仅将消息的有效负载记录到 Commons Logging Log。后者记录整个 SOAP 包络,包括 SOAP 头。以下示例显示了如何在端点映射中定义 PayloadLoggingInterceptor

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

这两个拦截器都有两个属性:logRequestlogResponse,可以设置为 false 以禁用请求或响应消息的日志记录。

您也可以像前面描述的那样,为 PayloadLoggingInterceptor 使用 WsConfigurerAdapter 方法。

PayloadValidatingInterceptor

使用契约优先开发风格的好处之一是我们可以使用模式来验证传入和传出的 XML 消息。Spring-WS 通过 PayloadValidatingInterceptor 促进了这一点。此拦截器需要引用一个或多个 W3C XML 或 RELAX NG 模式,并且可以设置为验证请求、响应或两者。

请注意,请求验证听起来是个好主意,但它会使最终的 Web 服务变得非常严格。通常,请求是否验证并不真正重要,重要的是端点是否能获得足够的信息来满足请求。验证响应是一个好主意,因为端点应该遵守其模式。记住波斯特尔定律:“对你所做的一切要保守,对你从别人那里接受的一切要开放。”

以下示例使用 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>

当然,您也可以像前面描述的那样,为 PayloadValidatingInterceptor 使用 WsConfigurerAdapter 方法。

使用 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 转换响应消息。请注意,由于端点拦截器在端点映射级别注册,您可以创建一个适用于“旧样式”消息的端点映射,并将拦截器添加到该映射中。因此,转换仅适用于这些“旧样式”消息。

您也可以像前面描述的那样,为 PayloadTransformingInterceptor 使用 WsConfigurerAdapter 方法。

5.5. 异常处理

Spring-WS 提供了 EndpointExceptionResolver 来缓解当消息被与请求匹配的端点处理时发生的意外异常的痛苦。端点异常解析器有点类似于可以在 Web 应用程序描述符 web.xml 中定义的异常映射。但是,它们提供了一种更灵活的异常处理方式。它们提供有关抛出异常时调用了哪个端点的信息。此外,以编程方式处理异常为您提供了更多选项来适当响应。您无需通过给出异常和堆栈跟踪来暴露应用程序内部,而是可以以您想要的任何方式处理异常——例如,通过返回具有特定故障代码和字符串的 SOAP 故障。

端点异常解析器由 MessageDispatcher 自动拾取,因此无需显式配置。

除了实现 EndpointExceptionResolver 接口(只需实现 resolveException(MessageContext, endpoint, Exception) 方法)之外,您还可以使用其中一个提供的实现。最简单的实现是 SimpleSoapExceptionResolver,它创建一个 SOAP 1.1 Server 或 SOAP 1.2 Receiver 故障,并使用异常消息作为故障字符串。SimpleSoapExceptionResolver 是默认值,但可以通过显式添加另一个解析器来覆盖它。

5.5.1. SoapFaultMappingExceptionResolver

SoapFaultMappingExceptionResolver 是一个更复杂的实现。此解析器允许您获取任何可能抛出的异常的类名并将其映射到 SOAP 故障

<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 类型的异常映射到客户端 SOAP 故障,其故障字符串为 Invalid request,如下所示

<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.5.2. 使用 SoapFaultAnnotationExceptionResolver

您还可以使用 @SoapFault 注解异常类,以指示在抛出该异常时应返回的 SOAP 故障。要使这些注解生效,您需要将 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.6. 服务器端测试

在测试您的 Web 服务端点时,您有两种可能的方法

  • 编写单元测试,您为端点提供(模拟)参数以供其使用。

    这种方法的优点是很容易实现(特别是对于用 @Endpoint 注解的类)。缺点是您没有真正测试通过网络发送的 XML 消息的确切内容。

  • 编写集成测试,它确实测试消息的内容。

第一种方法可以很容易地通过 EasyMock、JMock 等模拟框架实现。下一节将重点介绍如何使用 Spring Web Services 2.0 中引入的测试功能编写集成测试。

5.6.1. 编写服务器端集成测试

Spring Web Services 2.0 引入了对创建端点集成测试的支持。在这种上下文中,端点是处理(SOAP)消息的类(参见端点)。

集成测试支持位于 org.springframework.ws.test.server 包中。该包中的核心类是 MockWebServiceClient。其基本思想是,该客户端创建一个请求消息,然后将其发送到在标准 MessageDispatcherServlet 应用程序上下文(参见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 中的代码完成功能来指导您完成设置模拟服务器的过程。
另请注意,您可以在单元测试中依赖 Spring Web Services 中提供的标准日志记录功能。有时,检查请求或响应消息以找出特定测试失败的原因可能会很有用。有关更多信息,请参阅消息日志记录和跟踪

例如,考虑以下 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 注解。请参阅 端点
2 getCustomerCount() 方法以 CustomerCountRequest 作为参数,并返回 CustomerCountResponse。这两个类都是解组器支持的对象。例如,它们可以具有 @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 框架中提供的标准测试设施。这不是必需的,但通常是设置测试最简单的方法。
3 应用程序上下文是一个标准的 Spring-WS 应用程序上下文(请参阅 MessageDispatcherServlet),从 spring-ws-servlet.xml 读取。在这种情况下,应用程序上下文包含 CustomerEndpoint 的 Bean 定义(或者可能使用了 <context:component-scan />)。
4 @Before 方法中,我们使用 createClient 工厂方法创建一个 MockWebServiceClient
5 我们通过调用 sendRequest() 并使用静态导入的 RequestCreators 提供的 withPayload() RequestCreator(请参阅 使用 RequestCreatorRequestCreators)来发送请求。

我们还通过调用 andExpect() 并使用静态导入的 ResponseMatchers 提供的 payload() ResponseMatcher(请参阅 使用 ResponseMatcherResponseMatchers)来设置响应期望。

测试的这一部分可能看起来有点令人困惑,但 IDE 的代码完成功能提供了很大的帮助。在键入 sendRequest( 之后,只要您静态导入了 RequestCreators,您的 IDE 就可以为您提供可能的请求创建策略列表。对于 andExpect() 也是如此,只要您静态导入了 ResponseMatchers

5.6.2. 使用 RequestCreatorRequestCreators

最初,MockWebServiceClient 需要为端点创建要消费的请求消息。客户端为此目的使用 RequestCreator 策略接口

public interface RequestCreator {

  WebServiceMessage createRequest(WebServiceMessageFactory messageFactory)
    throws IOException;

}

您可以编写此接口的自己的实现,使用消息工厂创建请求消息,但您当然不必这样做。RequestCreators 类提供了一种基于给定有效载荷在 withPayload() 方法中创建 RequestCreator 的方法。您通常静态导入 RequestCreators

5.6.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 故障。

mustUnderstandFault()clientOrSenderFault()serverOrReceiverFault()versionMismatchFault()

期望响应消息包含特定的 SOAP 故障。

您可以通过链式调用 andExpect() 来设置多个响应期望

mockClient.sendRequest(...).
 andExpect(payload(expectedResponsePayload)).
 andExpect(validPayload(schemaResource));

有关 ResponseMatchers 提供的响应匹配器的更多信息,请参阅 Javadoc

6. 在客户端使用 Spring Web Services

Spring-WS 提供了一个客户端 Web 服务 API,允许对 Web 服务进行一致的、XML 驱动的访问。它还满足了对编组器和解组器的使用,以便您的服务层代码可以完全处理 Java 对象。

org.springframework.ws.client.core 包提供了使用客户端访问 API 的核心功能。它包含模板类,简化了 Web 服务的使用,就像核心 Spring JdbcTemplate 对 JDBC 所做的那样。Spring 模板类共同的设计原则是提供辅助方法来执行常见操作,对于更复杂的用法,则委托给用户实现的 callback 接口。Web 服务模板遵循相同的设计。这些类提供了各种便利方法,用于

  • 发送和接收 XML 消息

  • 在发送前将对象编组为 XML

  • 允许多种传输选项

6.1. 使用客户端 API

本节介绍如何使用客户端 API。有关如何使用服务器端 API,请参阅 使用 Spring-WS 创建 Web 服务

6.1.1. WebServiceTemplate

WebServiceTemplate 是 Spring-WS 中客户端 Web 服务访问的核心类。它包含用于发送 Source 对象和接收响应消息(可以是 SourceResult)的方法。此外,它还可以在通过传输发送之前将对象编组为 XML,并将任何响应 XML 解组回对象。

URI 和传输

WebServiceTemplate 类使用 URI 作为消息目的地。您可以在模板本身上设置 defaultUri 属性,或者在调用模板上的方法时显式提供 URI。URI 被解析为 WebServiceMessageSender,它负责通过传输层发送 XML 消息。您可以使用 WebServiceTemplate 类的 messageSendermessageSenders 属性设置一个或多个消息发送器。

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>
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>
电子邮件传输

Spring Web Services 还提供了一个电子邮件传输,您可以使用它通过 SMTP 发送 Web 服务消息,并通过 POP3 或 IMAP 检索它们。客户端电子邮件功能包含在 MailMessageSender 类中。此类从请求 WebServiceMessage 创建电子邮件消息,并通过 SMTP 发送。然后它等待响应消息到达传入的 POP3 或 IMAP 服务器。

要使用 MailMessageSender,请将 defaultUriuri 参数设置为 mailto 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>
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>
消息工厂

除了消息发送器,WebServiceTemplate 还需要一个 Web 服务消息工厂。SOAP 有两个消息工厂:SaajSoapMessageFactoryAxiomSoapMessageFactory。如果未指定消息工厂(通过设置 messageFactory 属性),Spring-WS 默认使用 SaajSoapMessageFactory

6.1.2. 发送和接收 WebServiceMessage

WebServiceTemplate 包含许多方便的方法来发送和接收 Web 服务消息。有些方法接受并返回 Source,有些方法返回 Result。此外,还有将对象编组和解组为 XML 的方法。以下示例将一个简单的 XML 消息发送到 Web 服务

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 将“Hello, World”消息发送到位于 https://:8080/WebService 的 Web 服务(对于 simpleSendAndReceive() 方法),并将结果写入控制台。WebServiceTemplate 注入了默认 URI,因为在 Java 代码中未显式提供 URI,所以使用了该 URI。

请注意,WebServiceTemplate 类一旦配置(假设其所有依赖项也是线程安全的,Spring-WS 附带的所有依赖项都是如此),就是线程安全的,因此多个对象可以使用相同的共享 WebServiceTemplate 实例。WebServiceTemplate 公开一个无参数构造函数以及 messageFactorymessageSender Bean 属性,您可以使用它们来构造实例(通过使用 Spring 容器或纯 Java 代码)。或者,考虑从 Spring-WS 的 WebServiceGatewaySupport 便利基类派生,它公开方便的 Bean 属性以实现轻松配置。(您不必扩展此基类。它仅作为便利类提供。)

6.1.3. 发送和接收 POJO——编组和解组

为了方便发送普通 Java 对象,WebServiceTemplate 具有许多 send(..) 方法,这些方法将 Object 作为消息数据内容的参数。WebServiceTemplate 类中的 marshalSendAndReceive(..) 方法将请求对象转换为 XML 的任务委托给 Marshaller,将响应 XML 转换为对象的任务委托给 Unmarshaller。(有关编组和解组的更多信息,请参阅 Spring 框架参考文档。)通过使用编组器,您的应用程序代码可以专注于正在发送或接收的业务对象,而不必关心它如何表示为 XML 的细节。要使用编组功能,您必须使用 WebServiceTemplate 类的 marshallerunmarshaller 属性设置一个编组器和一个解组器。

6.1.4. 使用 WebServiceMessageCallback

为了适应在消息上设置 SOAP 头和其他设置,WebServiceMessageCallback 接口允许您在消息创建后但在发送之前访问该消息。以下示例演示了如何在一个通过编组对象创建的消息上设置 SOAP 动作头

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 头。
WS-Addressing

除了服务器端 WS-Addressing支持,Spring Web Services 还支持客户端的此规范。

要在客户端设置 WS-Addressing 头,您可以使用 org.springframework.ws.soap.addressing.client.ActionCallback。此回调将所需的动作头作为参数。它还具有用于指定 WS-Addressing 版本和 To 头的构造函数。如果未指定,To 头默认为正在进行的连接的 URL。

以下示例将 Action 头设置为 http://samples/RequestOrder

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

6.1.5. 使用 WebServiceMessageExtractor

WebServiceMessageExtractor 接口是一个低级回调接口,您可以完全控制从收到的 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.2. 客户端测试

在测试您的 Web 服务客户端(即使用 WebServiceTemplate 访问 Web 服务的类)时,您有两种可能的方法

  • 编写单元测试,其中模拟 WebServiceTemplate 类、WebServiceOperations 接口或完整的客户端类。

    这种方法的优点是很容易实现。缺点是您没有真正测试通过网络发送的 XML 消息的确切内容,尤其是在模拟整个客户端类时。

  • 编写集成测试,它确实测试消息的内容。

第一种方法可以很容易地通过 EasyMock、JMock 等模拟框架实现。下一节将重点介绍如何使用 Spring Web Services 2.0 中引入的测试功能编写集成测试。

6.2.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 中的代码完成功能来指导您完成设置模拟服务器的过程。
另请注意,您可以在单元测试中依赖 Spring Web Services 中提供的标准日志记录功能。有时,检查请求或响应消息以找出特定测试失败的原因可能会很有用。有关更多信息,请参阅消息日志记录和跟踪

例如,考虑以下 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 是一个编组器支持的对象。例如,它可以使用 @XmlRootElement 注解来支持 JAXB2。
3 CustomerClient 使用 WebServiceGatewaySupport 提供的 WebServiceTemplate 将请求对象编组为 SOAP 消息并将其发送到 Web 服务。响应对象被解组为 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 框架中提供的标准测试设施。这不是必需的,但通常是设置测试最简单的方法。
3 CustomerClientintegration-test.xml 中配置,并使用 @Autowired 注入到此测试中。
4 @Before 方法中,我们使用 createServer 工厂方法创建一个 MockWebServiceServer
5 我们通过调用 expect() 并使用静态导入的 RequestMatchers 提供的 payload() RequestMatcher(请参阅 使用 RequestMatcherRequestMatchers)来定义期望。

我们还通过调用 andRespond() 并使用静态导入的 ResponseCreators 提供的 withPayload() ResponseCreator(请参阅 使用 ResponseCreatorResponseCreators)来设置响应。

测试的这一部分可能看起来有点令人困惑,但 IDE 的代码完成功能提供了很大的帮助。在您键入 expect( 后,只要您静态导入了 RequestMatchers,您的 IDE 就可以为您提供可能的请求匹配策略列表。对于 andRespond( 也是如此,只要您静态导入了 ResponseCreators

6 我们调用 CustomerClient 上的 getCustomerCount(),从而使用 WebServiceTemplate。模板此时已设置为“测试模式”,因此此方法调用不会建立实际的(HTTP)连接。我们还根据方法调用的结果进行了一些 JUnit 断言。
7 我们调用 MockWebServiceServer 上的 verify(),验证是否实际收到了预期的消息。

6.2.2. 使用 RequestMatcherRequestMatchers

为了验证请求消息是否符合某些期望,MockWebServiceServer 使用 RequestMatcher 策略接口。此接口定义的契约如下

public interface RequestMatcher {

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

您可以编写此接口的自己的实现,当消息不符合您的期望时抛出 AssertionError 异常,但您当然不必这样做。RequestMatchers 类提供了标准的 RequestMatcher 实现供您在测试中使用。您通常会静态导入此类。

RequestMatchers 类提供了以下请求匹配器

RequestMatchers 方法 描述

anything()

期望任何类型的请求。

payload()

期望给定的请求负载。

validPayload()

期望请求负载根据给定的 XSD 模式进行验证。

xpath()

期望给定的 XPath 表达式存在,不存在,或评估为给定值。

soapHeader()

期望请求消息中存在给定的 SOAP 头。

connectionTo()

期望连接到给定的 URL。

您可以通过链式调用 andExpect() 来设置多个请求期望。

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

有关 RequestMatchers 提供的请求匹配器的更多信息,请参阅 Javadoc

6.2.3. 使用 ResponseCreatorResponseCreators

当请求消息已验证并符合定义的期望时,MockWebServiceServer 会为 WebServiceTemplate 创建一个响应消息以供其消费。服务器为此目的使用 ResponseCreator 策略接口。

public interface ResponseCreator {

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

}

您可以再次编写此接口的自己的实现,通过使用消息工厂创建响应消息,但您当然不必这样做,因为 ResponseCreators 类为您在测试中提供了标准的 ResponseCreator 实现。您通常会静态导入此 P 类。

ResponseCreators 类提供以下响应:

ResponseCreators 方法 描述

withPayload()

创建一个带有给定负载的响应消息。

withError()

在响应连接中创建错误。此方法让您有机会测试错误处理。

withException()

从响应连接读取时抛出异常。此方法让您有机会测试异常处理。

withMustUnderstandFault()withClientOrSenderFault()withServerOrReceiverFault()withVersionMismatchFault()

创建一个带有给定 SOAP 错误的响应消息。此方法让您有机会测试错误处理。

有关 RequestMatchers 提供的请求匹配器的更多信息,请参阅 Javadoc

7. 使用 Spring-WS 保护您的 Web 服务

本章解释了如何将 WS-Security 方面添加到您的 Web 服务中。我们重点关注 WS-Security 的三个不同领域:

  • 身份验证:这是一个确定主体是否是其声称的身份的过程。在这种情况下,“主体”通常指用户、设备或应用程序中可以执行操作的某些其他系统。

  • 数字签名:消息的数字签名是一段基于文档和签名者私钥的信息。它是通过使用哈希函数和私有签名函数(使用签名者的私钥加密)创建的。

  • 加密和解密:加密是将数据转换为在没有适当密钥的情况下无法读取的形式的过程。它主要用于将信息隐藏起来,不让不应该看到它的人看到。解密是加密的逆过程。它是将加密数据转换回可读形式的过程。

这三个领域分别通过使用 XwsSecurityInterceptorWss4jSecurityInterceptor 来实现,我们分别在 XwsSecurityInterceptor使用 Wss4jSecurityInterceptor 中进行描述。

请注意,WS-Security(尤其是加密和签名)需要大量的内存,并且会降低性能。如果性能对您很重要,您可能需要考虑不使用 WS-Security 或使用基于 HTTP 的安全性。

7.1. XwsSecurityInterceptor

XwsSecurityInterceptor 是一个 EndpointInterceptor(请参阅 拦截请求 — EndpointInterceptor 接口),它基于 SUN 的 XML 和 Web 服务安全包 (XWSS)。此 WS-Security 实现是 Java Web Services Developer Pack (Java WSDP) 的一部分。

与其他任何端点拦截器一样,它在端点映射中定义(请参阅 端点映射)。这意味着您可以选择性地添加 WS-Security 支持。某些端点映射需要它,而另一些则不需要。

请注意,XWSS 需要 SUN 1.5 JDK 和 SUN SAAJ 参考实现。WSS4J 拦截器没有这些要求(请参阅 使用 Wss4jSecurityInterceptor)。

XwsSecurityInterceptor 需要一个安全策略文件才能运行。此 XML 文件告诉拦截器需要从传入的 SOAP 消息中获取哪些安全方面,以及要添加到传出消息中哪些方面。策略文件的基本格式在以下各节中进行解释,但您可以在此处找到更深入的教程。您可以使用 policyConfiguration 属性设置策略,该属性需要一个 Spring 资源。策略文件可以包含多个元素 — 例如,要求传入消息中包含用户名令牌并签名所有传出消息。它包含一个 SecurityConfiguration 元素(而不是 JAXRPCSecurity 元素)作为其根元素。

此外,安全拦截器需要一个或多个 CallbackHandler 实例才能运行。这些处理程序用于检索证书、私钥、验证用户凭据等。Spring-WS 为大多数常见的安全问题提供了处理程序 — 例如,针对 Spring Security 身份验证管理器进行身份验证,以及基于 X509 证书对传出消息进行签名。以下各节指示针对哪个安全问题使用哪个回调处理程序。您可以使用 callbackHandlercallbackHandlers 属性设置回调处理程序。

以下示例展示了如何连接 XwsSecurityInterceptor

<beans>
    <bean id="wsSecurityInterceptor"
        class="org.springframework.ws.soap.security.xwss.XwsSecurityInterceptor">
        <property name="policyConfiguration" value="classpath:securityPolicy.xml"/>
        <property name="callbackHandlers">
            <list>
                <ref bean="certificateHandler"/>
                <ref bean="authenticationHandler"/>
            </list>
        </property>
    </bean>
    ...
</beans>

此拦截器通过使用类路径上的 securityPolicy.xml 文件进行配置。它使用文件中稍后定义的两个回调处理程序。

7.1.1. 密钥库

对于大多数加密操作,您可以使用标准的 java.security.KeyStore 对象。这些操作包括证书验证、消息签名、签名验证和加密。它们不包括用户名和时间戳验证。本节旨在为您提供有关密钥库和可用于将密钥和证书存储在密钥库文件中的 Java 工具的一些背景知识。此信息主要与 Spring-WS 无关,而是与 Java 的通用加密功能有关。

java.security.KeyStore 类表示加密密钥和证书的存储设施。它可以包含三种不同类型的元素:

  • 私钥:这些密钥用于自身份验证。私钥附带相应公钥的证书链。在 WS-Security 领域中,这用于消息签名和消息解密。

  • 对称密钥:对称(或秘密)密钥也用于消息加密和解密 — 区别在于双方(发送方和接收方)共享相同的秘密密钥。

  • 受信任证书:这些 X509 证书被称为“受信任证书”,因为密钥库所有者信任证书中的公钥确实属于证书所有者。在 WS-Security 中,这些证书用于证书验证、签名验证和加密。

使用 keytool

keytool 程序(一个密钥和证书管理实用程序)随您的 Java 虚拟机提供。您可以使用此工具创建新的密钥库,向其中添加新的私钥和证书等。提供 keytool 命令的完整参考超出了本文档的范围,但您可以在此处或通过在命令行上使用 keytool -help 命令找到参考。

使用 KeyStoreFactoryBean

为了使用 Spring 配置轻松加载密钥库,您可以使用 KeyStoreFactoryBean。它具有一个资源位置属性,您可以将其设置为指向要加载的密钥库的路径。可以提供密码以检查密钥库数据的完整性。如果未提供密码,则不执行完整性检查。以下列表配置了一个 KeyStoreFactoryBean

<bean id="keyStore" class="org.springframework.ws.soap.security.support.KeyStoreFactoryBean">
    <property name="password" value="password"/>
    <property name="location" value="classpath:org/springframework/ws/soap/security/xwss/test-keystore.jks"/>
</bean>
如果您不指定 location 属性,则会创建一个新的空密钥库,这很可能不是您想要的。
KeyStoreCallbackHandler

要在 XwsSecurityInterceptor 中使用密钥库,您需要定义一个 KeyStoreCallbackHandler。此回调具有三个类型为 keystore 的属性:(keyStoretrustStoresymmetricStore)。处理程序使用的确切存储取决于此处理程序要执行的加密操作。对于私钥操作,使用 keyStore。对于对称密钥操作,使用 symmetricStore。为了确定信任关系,使用 trustStore。下表对此进行了说明:

加密操作 使用的密钥库

证书验证

首先 keyStore,然后 trustStore

基于私钥的解密

keyStore

基于对称密钥的解密

symmetricStore

基于公钥证书的加密

trustStore

基于对称密钥的加密

symmetricStore

签名

keyStore

签名验证

trustStore

此外,KeyStoreCallbackHandler 具有一个 privateKeyPassword 属性,应将其设置为解锁 keyStore 中包含的私钥。

如果未设置 symmetricStore,则默认为 keyStore。如果未设置密钥或信任存储,则回调处理程序使用标准 Java 机制加载或创建它。有关此机制如何工作的信息,请参阅 KeyStoreCallbackHandler 的 JavaDoc。

例如,如果您想使用 KeyStoreCallbackHandler 验证传入证书或签名,您可以使用信任存储:

<beans>
    <bean id="keyStoreHandler" class="org.springframework.ws.soap.security.xwss.callback.KeyStoreCallbackHandler">
        <property name="trustStore" ref="trustStore"/>
    </bean>

    <bean id="trustStore" class="org.springframework.ws.soap.security.support.KeyStoreFactoryBean">
        <property name="location" value="classpath:truststore.jks"/>
        <property name="password" value="changeit"/>
    </bean>
</beans>

如果您想使用它来解密传入证书或签名传出消息,您可以使用密钥存储:

<beans>
    <bean id="keyStoreHandler" class="org.springframework.ws.soap.security.xwss.callback.KeyStoreCallbackHandler">
        <property name="keyStore" ref="keyStore"/>
        <property name="privateKeyPassword" value="changeit"/>
    </bean>

    <bean id="keyStore" class="org.springframework.ws.soap.security.support.KeyStoreFactoryBean">
        <property name="location" value="classpath:keystore.jks"/>
        <property name="password" value="changeit"/>
    </bean>
</beans>

以下各节将说明 KeyStoreCallbackHandler 可以用于何处,以及要为特定加密操作设置哪些属性。

7.1.2. 身份验证

本章简介所述,身份验证是确定主体是否是其声称的身份的任务。在 WS-Security 中,身份验证可以采取两种形式:使用用户名和密码令牌(使用明文密码或密码摘要)或使用 X509 证书。

明文用户名身份验证

最简单的用户名身份验证形式使用明文密码。在这种情况下,SOAP 消息包含一个 UsernameToken 元素,其中包含一个 Username 元素和一个包含明文密码的 Password 元素。明文身份验证可以与 HTTP 服务器提供的基本身份验证进行比较。

请注意,明文密码不是很安全。因此,如果您使用它们,您应该始终为您的传输层添加额外的安全措施(例如,使用 HTTPS 而不是纯 HTTP)。

要要求每条传入消息都包含一个带有明文密码的 UsernameToken,安全策略文件应包含一个 RequireUsernameToken 元素,并将 passwordDigestRequired 属性设置为 false。您可以在此处找到可能的子元素的参考。以下列表显示了如何包含 RequireUsernameToken 元素:

<xwss:SecurityConfiguration xmlns:xwss="http://java.sun.com/xml/ns/xwss/config">
    ...
    <xwss:RequireUsernameToken passwordDigestRequired="false" nonceRequired="false"/>
    ...
</xwss:SecurityConfiguration>

如果用户名令牌不存在,XwsSecurityInterceptor 会向发送方返回一个 SOAP 错误。如果它存在,它会向注册的处理程序发出一个带有 PlainTextPasswordRequestPasswordValidationCallback。在 Spring-WS 中,有三个类处理此特定回调。

使用 SimplePasswordValidationCallbackHandler

最简单的密码验证处理程序是 SimplePasswordValidationCallbackHandler。此处理程序根据内存中的 Properties 对象验证密码,您可以使用 users 属性指定该对象:

<bean id="passwordValidationHandler"
    class="org.springframework.ws.soap.security.xwss.callback.SimplePasswordValidationCallbackHandler">
    <property name="users">
        <props>
            <prop key="Bert">Ernie</prop>
        </props>
    </property>
</bean>

在这种情况下,我们只允许用户“Bert”使用密码“Ernie”登录。

使用 SpringPlainTextPasswordValidationCallbackHandler

SpringPlainTextPasswordValidationCallbackHandler 使用 Spring Security 对用户进行身份验证。描述 Spring Security 超出了本文档的范围,但它是一个功能齐全的安全框架。您可以在 Spring Security 参考文档中阅读更多相关信息。

SpringPlainTextPasswordValidationCallbackHandler 需要一个 AuthenticationManager 才能运行。它使用此管理器根据它创建的 UsernamePasswordAuthenticationToken 进行身份验证。如果身份验证成功,令牌将存储在 SecurityContextHolder 中。您可以使用 authenticationManager 属性设置身份验证管理器:

<beans>
  <bean id="springSecurityHandler"
      class="org.springframework.ws.soap.security.xwss.callback.SpringPlainTextPasswordValidationCallbackHandler">
    <property name="authenticationManager" ref="authenticationManager"/>
  </bean>

  <bean id="authenticationManager" class="org.springframework.security.providers.ProviderManager">
      <property name="providers">
          <bean class="org.springframework.security.providers.dao.DaoAuthenticationProvider">
              <property name="userDetailsService" ref="userDetailsService"/>
          </bean>
      </property>
  </bean>

  <bean id="userDetailsService" class="com.mycompany.app.dao.UserDetailService" />
  ...
</beans>
使用 JaasPlainTextPasswordValidationCallbackHandler

JaasPlainTextPasswordValidationCallbackHandler 基于标准的 Java 身份验证和授权服务。提供 JAAS 的完整介绍超出了本文档的范围,但有一个不错的教程可用。

JaasPlainTextPasswordValidationCallbackHandler 只需要一个 loginContextName 即可运行。它使用此名称创建一个新的 JAAS LoginContext,并使用 SOAP 消息中提供的用户名和密码处理标准的 JAAS NameCallbackPasswordCallback。这意味着此回调处理程序与在 login() 阶段触发这些回调的任何 JAAS LoginModule 集成,这是标准行为。

您可以按如下方式连接 JaasPlainTextPasswordValidationCallbackHandler

<bean id="jaasValidationHandler"
    class="org.springframework.ws.soap.security.xwss.callback.jaas.JaasPlainTextPasswordValidationCallbackHandler">
    <property name="loginContextName" value="MyLoginModule" />
</bean>

在这种情况下,回调处理程序使用名为 MyLoginModuleLoginContext。此模块应在您的 jaas.config 文件中定义,如前面提到的教程中所述。

摘要用户名身份验证

当使用密码摘要时,SOAP 消息也包含一个 UsernameToken 元素,其中包含一个 Username 元素和一个 Password 元素。区别在于密码不是以明文形式发送,而是以摘要形式发送。接收方将此摘要与他根据用户已知密码计算出的摘要进行比较,如果它们相同,则用户通过身份验证。此方法可与 HTTP 服务器提供的摘要身份验证相媲美。

要要求每条传入消息都包含一个带有密码摘要的 UsernameToken 元素,安全策略文件应包含一个 RequireUsernameToken 元素,并将 passwordDigestRequired 属性设置为 true。此外,nonceRequired 属性应设置为 true:您可以在此处找到可能的子元素的参考。以下列表显示了如何定义 RequireUsernameToken 元素:

<xwss:SecurityConfiguration xmlns:xwss="http://java.sun.com/xml/ns/xwss/config">
    ...
    <xwss:RequireUsernameToken passwordDigestRequired="true" nonceRequired="true"/>
    ...
</xwss:SecurityConfiguration>

如果用户名令牌不存在,XwsSecurityInterceptor 会向发送方返回一个 SOAP 错误。如果它存在,它会向注册的处理程序发出一个带有 DigestPasswordRequestPasswordValidationCallback。在 Spring-WS 中,有两个类处理此特定回调:SimplePasswordValidationCallbackHandlerSpringDigestPasswordValidationCallbackHandler

使用 SimplePasswordValidationCallbackHandler

SimplePasswordValidationCallbackHandler 可以处理明文密码和密码摘要。它在使用 SimplePasswordValidationCallbackHandler 中进行了描述。

使用 SpringDigestPasswordValidationCallbackHandler

SpringDigestPasswordValidationCallbackHandler 需要一个 Spring Security UserDetailService 才能运行。它使用此服务检索令牌中指定用户的密码。然后将此详细信息对象中包含的密码摘要与消息中的摘要进行比较。如果它们相等,则用户已成功通过身份验证,并且 UsernamePasswordAuthenticationToken 存储在 SecurityContextHolder 中。您可以使用 userDetailsService 属性设置该服务。此外,您可以设置一个 userCache 属性来缓存加载的用户详细信息,如下所示:

<beans>
    <bean class="org.springframework.ws.soap.security.xwss.callback.SpringDigestPasswordValidationCallbackHandler">
        <property name="userDetailsService" ref="userDetailsService"/>
    </bean>

    <bean id="userDetailsService" class="com.mycompany.app.dao.UserDetailService" />
    ...
</beans>
证书身份验证

一种更安全的身份验证方法是使用 X509 证书。在这种情况下,SOAP 消息包含一个 BinarySecurityToken,其中包含 X509 证书的 Base 64 编码版本。接收方使用该证书进行身份验证。消息中存储的证书也用于签名消息(请参阅 验证签名)。

为了确保所有传入的 SOAP 消息都携带 BinarySecurityToken,安全策略文件应包含一个 RequireSignature 元素。此元素还可以携带其他元素,这些元素在 验证签名 中介绍。您可以在此处找到可能的子元素的参考。以下列表显示了如何定义 RequireSignature 元素:

<xwss:SecurityConfiguration xmlns:xwss="http://java.sun.com/xml/ns/xwss/config">
    ...
    <xwss:RequireSignature requireTimestamp="false">
    ...
</xwss:SecurityConfiguration>

当消息到达时没有携带证书,XwsSecurityInterceptor 会向发送方返回一个 SOAP 错误。如果存在证书,它会触发一个 CertificateValidationCallback。Spring-WS 中有三个处理程序为此回调提供身份验证目的:

在大多数情况下,证书身份验证应先于证书验证,因为您只想针对有效证书进行身份验证。应忽略无效证书,例如过期或不在您的受信任证书存储中的证书。

在 Spring-WS 术语中,这意味着 SpringCertificateValidationCallbackHandlerJaasCertificateValidationCallbackHandler 应该在 KeyStoreCallbackHandler 之前。这可以通过在 XwsSecurityInterceptor 的配置中设置 callbackHandlers 属性的顺序来实现:

<bean id="wsSecurityInterceptor"
    class="org.springframework.ws.soap.security.xwss.XwsSecurityInterceptor">
    <property name="policyConfiguration" value="classpath:securityPolicy.xml"/>
    <property name="callbackHandlers">
        <list>
            <ref bean="keyStoreHandler"/>
            <ref bean="springSecurityHandler"/>
        </list>
    </property>
</bean>

使用此设置,拦截器首先使用密钥库确定消息中的证书是否有效,然后对其进行身份验证。

使用 KeyStoreCallbackHandler

KeyStoreCallbackHandler 使用标准的 Java 密钥库来验证证书。此证书验证过程包括以下步骤:

  1. 处理程序检查证书是否在私有 keyStore 中。如果是,则它有效。

  2. 如果证书不在私有密钥库中,处理程序会检查当前日期和时间是否在证书中给定的有效期内。如果不是,则证书无效。如果是,则继续执行最后一步。

  3. 为证书创建认证路径。这基本上意味着处理程序确定证书是否已由 trustStore 中的任何证书颁发机构颁发。如果认证路径可以成功构建,则证书有效。否则,证书无效。

要将 KeyStoreCallbackHandler 用于证书验证目的,您很可能只需要设置 trustStore 属性:

<beans>
    <bean id="keyStoreHandler" class="org.springframework.ws.soap.security.xwss.callback.KeyStoreCallbackHandler">
        <property name="trustStore" ref="trustStore"/>
    </bean>

    <bean id="trustStore" class="org.springframework.ws.soap.security.support.KeyStoreFactoryBean">
        <property name="location" value="classpath:truststore.jks"/>
        <property name="password" value="changeit"/>
    </bean>
</beans>

使用前面示例中所示的设置,要验证的证书必须在信任存储本身中,或者信任存储必须包含颁发该证书的证书颁发机构。

使用 SpringCertificateValidationCallbackHandler

SpringCertificateValidationCallbackHandler 需要一个 Spring Security AuthenticationManager 才能运行。它使用此管理器根据它创建的 X509AuthenticationToken 进行身份验证。配置的身份验证管理器应提供一个可以处理此令牌的提供程序(通常是 X509AuthenticationProvider 的实例)。如果身份验证成功,令牌将存储在 SecurityContextHolder 中。您可以使用 authenticationManager 属性设置身份验证管理器:

<beans>
    <bean id="springSecurityCertificateHandler"
        class="org.springframework.ws.soap.security.xwss.callback.SpringCertificateValidationCallbackHandler">
        <property name="authenticationManager" ref="authenticationManager"/>
    </bean>

    <bean id="authenticationManager"
        class="org.springframework.security.providers.ProviderManager">
        <property name="providers">
            <bean class="org.springframework.ws.soap.security.x509.X509AuthenticationProvider">
                <property name="x509AuthoritiesPopulator">
                    <bean class="org.springframework.ws.soap.security.x509.populator.DaoX509AuthoritiesPopulator">
                        <property name="userDetailsService" ref="userDetailsService"/>
                    </bean>
                </property>
            </bean>
        </property>
    </bean>

  <bean id="userDetailsService" class="com.mycompany.app.dao.UserDetailService" />
  ...
</beans>

在这种情况下,我们使用自定义用户详细信息服务来获取基于证书的身份验证详细信息。有关 X509 证书身份验证的更多信息,请参阅 Spring Security 参考文档

使用 JaasCertificateValidationCallbackHandler

JaasCertificateValidationCallbackHandler 需要一个 loginContextName 才能运行。它使用此名称和证书的 X500Principal 创建一个新的 JAAS LoginContext。这意味着此回调处理程序与处理 X500 主体的任何 JAAS LoginModule 集成。

您可以按如下方式连接 JaasCertificateValidationCallbackHandler

<bean id="jaasValidationHandler"
    class="org.springframework.ws.soap.security.xwss.callback.jaas.JaasCertificateValidationCallbackHandler">
    <property name="loginContextName">MyLoginModule</property>
</bean>

在这种情况下,回调处理程序使用名为 MyLoginModuleLoginContext。此模块应在您的 jaas.config 文件中定义,并且应能够针对 X500 主体进行身份验证。

7.1.3. 数字签名

消息的数字签名是一段基于文档和签名者私钥的信息。WS-Security 中与签名相关的两个主要任务是:验证签名和签名消息。

验证签名

基于证书的身份验证一样,已签名消息包含一个 BinarySecurityToken,其中包含用于签名消息的证书。此外,它还包含一个 SignedInfo 块,指示消息的哪些部分已签名。

为了确保所有传入的 SOAP 消息都携带 BinarySecurityToken,安全策略文件应包含一个 RequireSignature 元素。它还可以包含一个 SignatureTarget 元素,该元素指定预期要签名的目标消息部分以及各种其他子元素。您还可以定义要使用的私钥别名、是否使用对称密钥而不是私钥以及许多其他属性。您可以在此处找到可能的子元素的参考。以下列表配置了一个 RequireSignature 元素:

<xwss:SecurityConfiguration xmlns:xwss="http://java.sun.com/xml/ns/xwss/config">
    <xwss:RequireSignature requireTimestamp="false"/>
</xwss:SecurityConfiguration>

如果签名不存在,XwsSecurityInterceptor 会向发送方返回一个 SOAP 错误。如果签名存在,它会向注册的处理程序发出一个 SignatureVerificationKeyCallback。在 Spring-WS 中,有一个类处理此特定回调:KeyStoreCallbackHandler

使用 KeyStoreCallbackHandler

KeyStoreCallbackHandler 中所述,KeyStoreCallbackHandler 使用 java.security.KeyStore 处理各种加密回调,包括签名验证。对于签名验证,处理程序使用 trustStore 属性:

<beans>
    <bean id="keyStoreHandler" class="org.springframework.ws.soap.security.xwss.callback.KeyStoreCallbackHandler">
        <property name="trustStore" ref="trustStore"/>
    </bean>

    <bean id="trustStore" class="org.springframework.ws.soap.security.support.KeyStoreFactoryBean">
        <property name="location" value="classpath:org/springframework/ws/soap/security/xwss/test-truststore.jks"/>
        <property name="password" value="changeit"/>
    </bean>
</beans>
签名消息

签名消息时,XwsSecurityInterceptorBinarySecurityToken 添加到消息中。它还添加了一个 SignedInfo 块,指示消息的哪些部分已签名。

要签名所有传出的 SOAP 消息,安全策略文件应包含一个 Sign 元素。它还可以包含一个 SignatureTarget 元素,该元素指定预期要签名的目标消息部分以及各种其他子元素。您还可以定义要使用的私钥别名、是否使用对称密钥而不是私钥以及许多其他属性。您可以在此处找到可能的子元素的参考。以下示例包含一个 Sign 元素:

<xwss:SecurityConfiguration xmlns:xwss="http://java.sun.com/xml/ns/xwss/config">
	<xwss:Sign includeTimestamp="false" />
</xwss:SecurityConfiguration>

XwsSecurityInterceptor 会向注册的处理程序发出一个 SignatureKeyCallback。在 Spring-WS 中,KeyStoreCallbackHandler 类处理此特定回调。

使用 KeyStoreCallbackHandler

KeyStoreCallbackHandler 中所述,KeyStoreCallbackHandler 使用 java.security.KeyStore 处理各种加密回调,包括签名消息。对于添加签名,处理程序使用 keyStore 属性。此外,您必须设置 privateKeyPassword 属性以解锁用于签名的私钥。以下示例使用 KeyStoreCallbackHandler

<beans>
    <bean id="keyStoreHandler" class="org.springframework.ws.soap.security.xwss.callback.KeyStoreCallbackHandler">
        <property name="keyStore" ref="keyStore"/>
        <property name="privateKeyPassword" value="changeit"/>
    </bean>

    <bean id="keyStore" class="org.springframework.ws.soap.security.support.KeyStoreFactoryBean">
        <property name="location" value="classpath:keystore.jks"/>
        <property name="password" value="changeit"/>
    </bean>
</beans>

7.1.4. 解密和加密

加密时,消息被转换为只能使用适当密钥读取的形式。消息可以解密以显示原始的可读消息。

解密

要解密传入的 SOAP 消息,安全策略文件应包含一个 RequireEncryption 元素。此元素可以进一步携带一个 EncryptionTarget 元素,指示消息的哪个部分应该被加密,以及一个 SymmetricKey 指示应该使用共享密钥而不是常规私钥来解密消息。您可以在此处阅读其他元素的描述。以下示例使用 RequireEncryption 元素:

<xwss:SecurityConfiguration xmlns:xwss="http://java.sun.com/xml/ns/xwss/config">
    <xwss:RequireEncryption />
</xwss:SecurityConfiguration>

如果传入消息未加密,XwsSecurityInterceptor 会向发送方返回一个 SOAP 错误。如果已加密,它会向注册的处理程序发出一个 DecryptionKeyCallback。在 Spring-WS 中,KeyStoreCallbackHandler 类处理此特定回调。

使用 KeyStoreCallbackHandler

KeyStoreCallbackHandler 中所述,KeyStoreCallbackHandler 使用 java.security.KeyStore 处理各种加密回调,包括解密。对于解密,处理程序使用 keyStore 属性。此外,您必须设置 privateKeyPassword 属性以解锁用于解密的私钥。对于基于对称密钥的解密,它使用 symmetricStore。以下示例使用 KeyStoreCallbackHandler

<beans>
    <bean id="keyStoreHandler" class="org.springframework.ws.soap.security.xwss.callback.KeyStoreCallbackHandler">
        <property name="keyStore" ref="keyStore"/>
        <property name="privateKeyPassword" value="changeit"/>
    </bean>

    <bean id="keyStore" class="org.springframework.ws.soap.security.support.KeyStoreFactoryBean">
        <property name="location" value="classpath:keystore.jks"/>
        <property name="password" value="changeit"/>
    </bean>
</beans>
加密

要加密传出的 SOAP 消息,安全策略文件应包含一个 Encrypt 元素。此元素可以进一步携带一个 EncryptionTarget 元素,指示消息的哪个部分应该被加密,以及一个 SymmetricKey 指示应该使用共享密钥而不是常规公钥来加密消息。您可以在此处阅读其他元素的描述。以下示例使用 Encrypt 元素:

<xwss:SecurityConfiguration xmlns:xwss="http://java.sun.com/xml/ns/xwss/config">
    <xwss:Encrypt />
</xwss:SecurityConfiguration>

XwsSecurityInterceptor 向注册的处理程序发出一个 EncryptionKeyCallback 以检索加密信息。在 Spring-WS 中,KeyStoreCallbackHandler 类处理此特定回调。

使用 KeyStoreCallbackHandler

KeyStoreCallbackHandler 中所述,KeyStoreCallbackHandler 使用 java.security.KeyStore 处理各种加密回调,包括加密。对于基于公钥的加密,处理程序使用 trustStore 属性。对于基于对称密钥的加密,它使用 symmetricStore。以下示例使用 KeyStoreCallbackHandler

<beans>
    <bean id="keyStoreHandler" class="org.springframework.ws.soap.security.xwss.callback.KeyStoreCallbackHandler">
        <property name="trustStore" ref="trustStore"/>
    </bean>

    <bean id="trustStore" class="org.springframework.ws.soap.security.support.KeyStoreFactoryBean">
        <property name="location" value="classpath:truststore.jks"/>
        <property name="password" value="changeit"/>
    </bean>
</beans>

7.1.5. 安全异常处理

当安全或验证操作失败时,XwsSecurityInterceptor 分别抛出 WsSecuritySecurementExceptionWsSecurityValidationException。这些异常绕过标准异常处理机制,但由拦截器本身处理。

WsSecuritySecurementException 异常由 XwsSecurityInterceptorhandleSecurementException 方法处理。默认情况下,此方法会记录错误并停止消息的进一步处理。

类似地,WsSecurityValidationException 异常由 XwsSecurityInterceptorhandleValidationException 方法处理。默认情况下,此方法会创建一个 SOAP 1.1 客户端或 SOAP 1.2 发送方错误并将其作为响应发送回去。

handleSecurementExceptionhandleValidationException 都是受保护的方法,您可以覆盖它们以更改其默认行为。

7.2. 使用 Wss4jSecurityInterceptor

Wss4jSecurityInterceptor 是一个 EndpointInterceptor(请参阅 拦截请求 — EndpointInterceptor 接口),它基于 Apache 的 WSS4J

WSS4J 实现以下标准:

  • OASIS Web 服务安全:SOAP 消息安全 1.0 标准 200401,2004 年 3 月

  • 用户名令牌配置文件 V1.0

  • X.509 令牌配置文件 V1.0

此拦截器支持由 AxiomSoapMessageFactorySaajSoapMessageFactory 创建的消息。

7.2.1. 配置 Wss4jSecurityInterceptor

WSS4J 不使用外部配置文件。拦截器完全由属性配置。此拦截器调用的验证和安全操作分别通过 validationActionssecurementActions 属性指定。操作作为空格分隔的字符串传递。以下列表显示了一个示例配置:

<bean class="org.springframework.ws.soap.security.wss4j.Wss4jSecurityInterceptor">
    <property name="validationActions" value="UsernameToken Encrypt"/>
    ...
    <property name="securementActions" value="Encrypt"/>
    ...
</bean>

下表显示了可用的验证操作:

验证操作 描述

UsernameToken

验证用户名令牌

Timestamp

验证时间戳

Encrypt

解密消息

Signature

验证签名

NoSecurity

不执行任何操作

下表显示了可用的安全操作:

安全操作 描述

UsernameToken

添加用户名令牌

UsernameTokenSignature

添加用户名令牌和签名用户名令牌密钥

Timestamp

添加时间戳

Encrypt

加密响应

Signature

签名响应

NoSecurity

不执行任何操作

操作的顺序很重要,并由拦截器强制执行。如果其安全操作的执行顺序与 validationActions 指定的顺序不同,则拦截器会拒绝传入的 SOAP 消息。

7.2.2. 处理数字证书

对于需要与密钥库或证书处理(签名、加密和解密操作)交互的加密操作,WSS4J 需要 org.apache.ws.security.components.crypto.Crypto 的实例。

Crypto 实例可以从 WSS4J 的 CryptoFactory 获取,或者更方便地使用 Spring-WS 的 CryptoFactoryBean 获取。

CryptoFactoryBean

Spring-WS 提供了一个方便的工厂 Bean CryptoFactoryBean,它通过强类型属性(首选)或通过 Properties 对象构造和配置 Crypto 实例。

默认情况下,CryptoFactoryBean 返回 org.apache.ws.security.components.crypto.Merlin 的实例。您可以通过设置 cryptoProvider 属性(或其等效的 org.apache.ws.security.crypto.provider 字符串属性)来更改此设置。

以下示例配置使用 CryptoFactoryBean

<bean class="org.springframework.ws.soap.security.wss4j.support.CryptoFactoryBean">
    <property name="keyStorePassword" value="mypassword"/>
    <property name="keyStoreLocation" value="file:/path_to_keystore/keystore.jks"/>
</bean>

7.2.3. 身份验证

本节讨论如何使用 Wss4jSecurityInterceptor 进行身份验证。

验证用户名令牌

Spring-WS 提供了一组回调处理程序以与 Spring Security 集成。此外,还提供了一个简单的回调处理程序 SimplePasswordValidationCallbackHandler,用于通过内存中的 Properties 对象配置用户和密码。

回调处理程序通过 Wss4jSecurityInterceptor 属性的 validationCallbackHandler 进行配置。

使用 SimplePasswordValidationCallbackHandler

SimplePasswordValidationCallbackHandler 根据内存中的 Properties 对象验证明文和摘要用户名令牌。您可以按如下方式配置它:

<bean id="callbackHandler"
    class="org.springframework.ws.soap.security.wss4j.callback.SimplePasswordValidationCallbackHandler">
    <property name="users">
        <props>
            <prop key="Bert">Ernie</prop>
        </props>
    </property>
</bean>
使用 SpringSecurityPasswordValidationCallbackHandler

SpringSecurityPasswordValidationCallbackHandler 通过使用 Spring Security UserDetailService 来验证明文和摘要密码。它使用此服务来检索令牌中指定用户的密码(或密码的摘要)。然后将此详细信息对象中包含的密码(或密码的摘要)与消息中的摘要进行比较。如果它们相等,则用户已成功通过身份验证,并且 UsernamePasswordAuthenticationToken 存储在 SecurityContextHolder 中。您可以使用 userDetailsService 设置服务。此外,您可以设置一个 userCache 属性来缓存加载的用户详细信息,如下所示:

<beans>
    <bean class="org.springframework.ws.soap.security.wss4j.callback.SpringDigestPasswordValidationCallbackHandler">
        <property name="userDetailsService" ref="userDetailsService"/>
    </bean>

    <bean id="userDetailsService" class="com.mycompany.app.dao.UserDetailService" />
    ...
</beans>
添加用户名令牌

向传出消息添加用户名令牌就像将 UsernameToken 添加到 Wss4jSecurityInterceptorsecurementActions 属性并指定 securementUsernamesecurementPassword 一样简单。

密码类型可以通过设置 securementPasswordType 属性来设置。可能的值是 PasswordText 用于明文密码,或 PasswordDigest 用于摘要密码,这是默认值。

以下示例生成一个带有摘要密码的用户名令牌:

<bean class="org.springframework.ws.soap.security.wss4j.Wss4jSecurityInterceptor">
    <property name="securementActions" value="UsernameToken"/>
    <property name="securementUsername" value="Ernie"/>
    <property name="securementPassword" value="Bert"/>
</bean>

如果选择明文密码类型,可以通过设置 securementUsernameTokenElements 属性来指示拦截器添加 NonceCreated 元素。该值必须是包含所需元素名称(区分大小写)的空格分隔列表。

以下示例生成一个带有明文密码、NonceCreated 元素的用户名令牌:

<bean class="org.springframework.ws.soap.security.wss4j.Wss4jSecurityInterceptor">
    <property name="securementActions" value="UsernameToken"/>
    <property name="securementUsername" value="Ernie"/>
    <property name="securementPassword" value="Bert"/>
    <property name="securementPasswordType" value="PasswordText"/>
    <property name="securementUsernameTokenElements" value="Nonce Created"/>
</bean>
证书身份验证

由于证书身份验证类似于数字签名,WSS4J 将其作为签名验证和安全的一部分进行处理。具体来说,securementSignatureKeyIdentifier 属性必须设置为 DirectReference,以指示 WSS4J 生成一个包含 X509 证书的 BinarySecurityToken 元素并将其包含在传出消息中。证书的名称和密码分别通过 securementUsernamesecurementPassword 属性传递,如以下示例所示:

<bean class="org.springframework.ws.soap.security.wss4j.Wss4jSecurityInterceptor">
    <property name="securementActions" value="Signature"/>
    <property name="securementSignatureKeyIdentifier" value="DirectReference"/>
    <property name="securementUsername" value="mycert"/>
    <property name="securementPassword" value="certpass"/>
    <property name="securementSignatureCrypto">
      <bean class="org.springframework.ws.soap.security.wss4j.support.CryptoFactoryBean">
        <property name="keyStorePassword" value="123456"/>
        <property name="keyStoreLocation" value="classpath:/keystore.jks"/>
      </bean>
    </property>
</bean>

对于证书验证,适用常规签名验证:

<bean class="org.springframework.ws.soap.security.wss4j.Wss4jSecurityInterceptor">
    <property name="validationActions" value="Signature"/>
    <property name="validationSignatureCrypto">
      <bean class="org.springframework.ws.soap.security.wss4j.support.CryptoFactoryBean">
        <property name="keyStorePassword" value="123456"/>
        <property name="keyStoreLocation" value="classpath:/keystore.jks"/>
      </bean>
    </property>
</bean>

在验证结束时,拦截器通过委托给默认的 WSS4J 实现自动验证证书的有效性。如果需要,您可以通过重新定义 verifyCertificateTrust 方法来更改此行为。

有关更多详细信息,请参阅 数字签名

7.2.4. 安全时间戳

本节描述了 Wss4jSecurityInterceptor 中可用的各种时间戳选项。

验证时间戳

要验证时间戳,请将 Timestamp 添加到 validationActions 属性中。您可以通过将 timestampStrict 设置为 true 并通过设置 timeToLive 属性来指定服务器端生存时间(默认为:300)来覆盖 SOAP 消息发起者指定的时间戳语义。无论 timeToLive 的值如何,拦截器始终拒绝已经过期的时间戳。

在以下示例中,拦截器将时间戳有效期窗口限制为 10 秒,拒绝该窗口之外的任何有效时间戳令牌:

<bean class="org.springframework.ws.soap.security.wss4j.Wss4jSecurityInterceptor">
    <property name="validationActions" value="Timestamp"/>
    <property name="timestampStrict" value="true"/>
    <property name="timeToLive" value="10"/>
</bean>
添加时间戳

Timestamp 添加到 securementActions 属性会在传出消息中生成时间戳头。timestampPrecisionInMilliseconds 属性指定生成的时间戳的精度是否以毫秒为单位。默认值为 true。以下列表添加了一个时间戳:

<bean class="org.springframework.ws.soap.security.wss4j.Wss4jSecurityInterceptor">
    <property name="securementActions" value="Timestamp"/>
    <property name="timestampPrecisionInMilliseconds" value="true"/>
</bean>

7.2.5. 数字签名

本节描述了 Wss4jSecurityInterceptor 中可用的各种签名选项。

验证签名

要指示 Wss4jSecurityInterceptorvalidationActions 必须包含 Signature 操作。此外,validationSignatureCrypto 属性必须指向包含发起者公共证书的密钥库:

<bean id="wsSecurityInterceptor" class="org.springframework.ws.soap.security.wss4j.Wss4jSecurityInterceptor">
    <property name="validationActions" value="Signature"/>
    <property name="validationSignatureCrypto">
        <bean class="org.springframework.ws.soap.security.wss4j.support.CryptoFactoryBean">
            <property name="keyStorePassword" value="123456"/>
            <property name="keyStoreLocation" value="classpath:/keystore.jks"/>
        </bean>
    </property>
</bean>
签名消息

通过将 Signature 操作添加到 securementActions 来启用传出消息的签名。要使用的私钥的别名和密码分别由 securementUsernamesecurementPassword 属性指定。securementSignatureCrypto 必须指向包含私钥的密钥库:

<bean class="org.springframework.ws.soap.security.wss4j.Wss4jSecurityInterceptor">
    <property name="securementActions" value="Signature"/>
    <property name="securementUsername" value="mykey"/>
    <property name="securementPassword" value="123456"/>
    <property name="securementSignatureCrypto">
        <bean class="org.springframework.ws.soap.security.wss4j.support.CryptoFactoryBean">
            <property name="keyStorePassword" value="123456"/>
            <property name="keyStoreLocation" value="classpath:/keystore.jks"/>
        </bean>
    </property>
</bean>

此外,您可以通过设置 securementSignatureAlgorithm 属性来定义签名算法。

您可以通过设置 securementSignatureKeyIdentifier 属性来自定义要使用的密钥标识符类型。只有 IssuerSerialDirectReference 对签名有效。

securementSignatureParts 属性控制消息的哪些部分被签名。此属性的值是一个由分号分隔的元素名称列表,用于标识要签名的元素。签名部分的一般形式是 {}{namespace}Element。请注意,第一个空括号仅用于加密部分。默认行为是签名 SOAP 正文。

以下示例显示了如何在 Spring Web Services echo 示例中签名 echoResponse 元素:

<property name="securementSignatureParts"
    value="{}{http://www.springframework.org/spring-ws/samples/echo}echoResponse"/>

要指定没有命名空间的元素,请使用字符串 Null(区分大小写)作为命名空间名称。

如果请求中没有其他元素的本地名称为 Body,则 SOAP 命名空间标识符可以为空 ({})。

签名确认

通过将 enableSignatureConfirmation 设置为 true 来启用签名确认。请注意,签名确认操作跨越请求和响应。这意味着即使没有相应的安全操作,也必须将 secureResponsevalidateRequest 设置为 true(这是默认值)。以下示例将 enableSignatureConfirmation 属性设置为 true

<bean class="org.springframework.ws.soap.security.wss4j.Wss4jSecurityInterceptor">
    <property name="validationActions" value="Signature"/>
    <property name="enableSignatureConfirmation" value="true"/>
    <property name="validationSignatureCrypto">
        <bean class="org.springframework.ws.soap.security.wss4j.support.CryptoFactoryBean">
            <property name="keyStorePassword" value="123456"/>
            <property name="keyStoreLocation" value="file:/keystore.jks"/>
        </bean>
    </property>
</bean>

7.2.6. 解密和加密

本节描述了 Wss4jSecurityInterceptor 中可用的各种解密和加密选项。

解密

传入 SOAP 消息的解密要求将 Encrypt 操作添加到 validationActions 属性中。(这是因为 WSS4J 只需要一个用于加密密钥的 Crypto,而嵌入式密钥名称验证则委托给回调处理程序。)

要解密具有嵌入式加密对称密钥(xenc:EncryptedKey 元素)的消息,validationDecryptionCrypto 需要指向包含解密私钥的密钥库。此外,必须向 validationCallbackHandler 注入一个 org.springframework.ws.soap.security.wss4j.callback.KeyStoreCallbackHandler,该处理程序指定密钥的密码:

<bean class="org.springframework.ws.soap.security.wss4j.Wss4jSecurityInterceptor">
    <property name="validationActions" value="Encrypt"/>
    <property name="validationDecryptionCrypto">
        <bean class="org.springframework.ws.soap.security.wss4j.support.CryptoFactoryBean">
            <property name="keyStorePassword" value="123456"/>
            <property name="keyStoreLocation" value="classpath:/keystore.jks"/>
        </bean>
    </property>
    <property name="validationCallbackHandler">
        <bean class="org.springframework.ws.soap.security.wss4j.callback.KeyStoreCallbackHandler">
            <property name="privateKeyPassword" value="mykeypass"/>
        </bean>
    </property>
</bean>

为了支持解密带有嵌入式密钥名称(ds:KeyName 元素)的消息,您可以配置一个 KeyStoreCallbackHandler,它指向带有对称密钥的密钥库。symmetricKeyPassword 属性指示密钥的密码,密钥名称是由 ds:KeyName 元素指定的名称:

<bean class="org.springframework.ws.soap.security.wss4j.Wss4jSecurityInterceptor">
    <property name="validationActions" value="Encrypt"/>
    <property name="validationCallbackHandler">
        <bean class="org.springframework.ws.soap.security.wss4j.callback.KeyStoreCallbackHandler">
            <property name="keyStore">
                <bean class="org.springframework.ws.soap.security.support.KeyStoreFactoryBean">
                    <property name="location" value="classpath:keystore.jks"/>
                    <property name="type" value="JCEKS"/>
                    <property name="password" value="123456"/>
                </bean>
            </property>
            <property name="symmetricKeyPassword" value="mykeypass"/>
        </bean>
    </property>
</bean>
加密

Encrypt 添加到 securementActions 可启用传出消息的加密。您可以通过设置 securementEncryptionUser 属性来设置用于加密的证书别名。包含证书的密钥库通过 securementEncryptionCrypto 属性访问。由于加密依赖于公共证书,因此无需传递密码。以下示例使用 securementEncryptionCrypto 属性:

<bean class="org.springframework.ws.soap.security.wss4j.Wss4jSecurityInterceptor">
    <property name="securementActions" value="Encrypt"/>
    <property name="securementEncryptionUser" value="mycert"/>
    <property name="securementEncryptionCrypto">
        <bean class="org.springframework.ws.soap.security.wss4j.support.CryptoFactoryBean">
            <property name="keyStorePassword" value="123456"/>
            <property name="keyStoreLocation" value="file:/keystore.jks"/>
        </bean>
    </property>
</bean>

您可以通过多种方式自定义加密:要使用的密钥标识符类型由 securementEncryptionKeyIdentifier 属性定义。可能的值是 IssuerSerialX509KeyIdentifierDirectReferenceThumbprintSKIKeyIdentifierEmbeddedKeyName

如果选择 EmbeddedKeyName 类型,您需要指定用于加密的密钥。密钥的别名在 securementEncryptionUser 属性中设置,与其他密钥标识符类型相同。但是,WSS4J 需要一个回调处理程序来获取密钥。因此,您必须向 securementCallbackHandler 提供一个指向适当密钥库的 KeyStoreCallbackHandler。默认情况下,结果 WS-Security 头中的 ds:KeyName 元素采用 securementEncryptionUser 属性的值。要指示不同的名称,您可以使用所需值设置 securementEncryptionEmbeddedKeyName。在下一个示例中,传出消息使用别名为 secretKey 的密钥进行加密,而 myKey 出现在 ds:KeyName 元素中:

<bean class="org.springframework.ws.soap.security.wss4j.Wss4jSecurityInterceptor">
    <property name="securementActions" value="Encrypt"/>
    <property name="securementEncryptionKeyIdentifier" value="EmbeddedKeyName"/>
    <property name="securementEncryptionUser" value="secretKey"/>
    <property name="securementEncryptionEmbeddedKeyName" value="myKey"/>
    <property name="securementCallbackHandler">
        <bean class="org.springframework.ws.soap.security.wss4j.callback.KeyStoreCallbackHandler">
            <property name="symmetricKeyPassword" value="keypass"/>
            <property name="keyStore">
                <bean class="org.springframework.ws.soap.security.support.KeyStoreFactoryBean">
                    <property name="location" value="file:/keystore.jks"/>
                    <property name="type" value="jceks"/>
                    <property name="password" value="123456"/>
                </bean>
            </property>
        </bean>
    </property>
</bean>

securementEncryptionKeyTransportAlgorithm 属性定义用于加密生成的对称密钥的算法。支持的值是 http://www.w3.org/2001/04/xmlenc#rsa-1_5(默认值)和 http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p

最后,您可以通过设置 securementEncryptionSymAlgorithm 属性来设置要使用的对称加密算法。支持的值是 http://www.w3.org/2001/04/xmlenc#aes128-cbc(默认)、http://www.w3.org/2001/04/xmlenc#tripledes-cbchttp://www.w3.org/2001/04/xmlenc#aes256-cbchttp://www.w3.org/2001/04/xmlenc#aes192-cbc

最后,securementEncryptionParts 属性定义消息的哪些部分被加密。此属性的值是一个由分号分隔的元素名称列表,用于标识要加密的元素。每个元素名称前面可以有一个加密模式说明符和一个命名空间标识符,每个都包含在一对花括号中。加密模式说明符可以是 {Content}{Element}。请参阅 W3C XML 加密规范了解元素加密和内容加密之间的差异。以下示例标识了 echo 示例中的 echoResponse

<property name="securementEncryptionParts"
    value="{Content}{http://www.springframework.org/spring-ws/samples/echo}echoResponse"/>

请注意,元素名称、命名空间标识符和加密修饰符是区分大小写的。您可以省略加密修饰符和命名空间标识符。如果省略,加密模式默认为 Content,命名空间设置为 SOAP 命名空间。

要指定没有命名空间的元素,请使用值 Null(区分大小写)作为命名空间名称。如果未指定列表,则处理程序默认以 Content 模式加密 SOAP 正文。

7.2.7. 安全异常处理

Wss4jSecurityInterceptor 的异常处理与 XwsSecurityInterceptor 的异常处理相同。有关更多信息,请参阅 安全异常处理

三、其他资源

除了本参考文档之外,还有许多其他资源可以帮助您学习如何使用 Spring Web Services。本节列出了这些额外的第三方资源。

参考书目

  • [waldo-94] Jim Waldo, Ann Wollrath, and Sam Kendall. 分布式计算札记. Springer Verlag. 1994

  • [alpine] Steve Loughran & Edmund Smith. 重新思考 Java SOAP 栈. 2005 年 5 月 17 日。© 2005 IEEE 电话实验室公司。

  • [effective-enterprise-java] Ted Neward. Scott Meyers. 高效企业 Java. Addison-Wesley. 2004

  • [effective-xml] Elliotte Rusty Harold. Scott Meyers. 高效 XML. Addison-Wesley. 2004

© . This site is unofficial and not affiliated with VMware.