© 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 Framework 中的对象/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、电子邮件等)。

  • Security 包 (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(仅支持基于 XSD 的 WSDL 1.1 生成)

  • WS-I 基本配置文件 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/编码)中使用这些引用的标准方式已被弃用,取而代之的是文档/文字(请参见 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,从而导致响应时间变慢。

使用 contract-first 时,可以明确描述在何处发送 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,以及该接口的(新)实现。当然,必须保留旧服务,因为可能有一些客户端尚未迁移。

如果使用 contract-first,则可以在合同和实现之间实现更松散的耦合。这种松散耦合使我们能够在一个类中实现合同的两个版本。例如,我们可以使用 XSLT 样式表将任何“旧式”消息转换为“新式”消息。

3. 编写 Contract-First Web 服务

本教程演示如何编写 contract-first web 服务,即如何先开发 XML Schema 或 WSDL 合同,然后再开发 Java 代码。Spring-WS 专注于这种开发风格,本教程应有助于你入门。请注意,本教程的第一部分几乎不包含任何 Spring-WS 特定信息。它主要涉及 XML、XSD 和 WSDL。 第二部分重点介绍如何使用 Spring-WS 实现此合同。

在进行合同优先 Web 服务开发时,最重要的就是以 XML 的形式进行思考。这意味着 Java 语言概念的重要性较低。通过网络发送的是 XML,您应该专注于此。用于实现 Web 服务的 Java 是一种实现细节。

在本教程中,我们定义了由人力资源部门创建的 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 定义此类协定

DTD 的命名空间支持有限,因此不适用于 Web 服务。Relax NG 和 Schematron 比 XML 模式简单。遗憾的是,它们在各个平台上的支持度不是很高。因此,我们使用 XML 模式。

到目前为止,创建 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 模式有一个出色的内置 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/literalrpc/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="http://localhost:8080/holidayService/"/>     (9)
        </wsdl:port>
    </wsdl:service>
</wsdl:definitions>
1 我们导入在 数据协定中定义的架构。
2 我们定义在 portType 中使用的 HolidayRequest 消息。
3 HolidayRequest 类型在架构中定义。
4 我们定义在 binding 中使用的 HumanResource 端口类型。
5 我们定义在 port 中使用的 HumanResourceBinding 绑定。
6 我们使用文档/文字样式。
7 文字 http://schemas.xmlsoap.org/soap/http 表示 HTTP 传输。
8 soapAction 属性表示将随每个请求一起发送的 SOAPAction HTTP 标头。
9 http://localhost: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/schemasHolidayRequest 本地名称的 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 可以作为 servlet 上下文中的 holiday.wsdl 检索。完整 URL 为 http://localhost:8080/holidayService/holiday.wsdl
2 接下来,我们将 WSDL 端口类型设置为 HumanResource
3 我们将服务可以访问的位置设置为:/holidayService/。我们使用相对 URI,并指示框架将其动态转换为绝对 URI。因此,如果服务部署到不同的上下文中,我们不必手动更改 URI。有关更多信息,请参阅 名为“自动 WSDL 曝光” 的部分。为了使位置转换正常工作,我们需要在 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 示例应用程序。之后,查看航空公司示例,它稍微复杂一些,因为它使用 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.Filejava.io.InputStreamjava.io.Reader

javax.xml.transform.stream.StreamResult

java.io.Filejava.io.OutputStreamjava.io.Writer

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

4.1.2. SoapMessage

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

4.1.3. 消息工厂

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

SaajSoapMessageFactory

SaajSoapMessageFactory 使用 SOAP with Attachments API for Java (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 Object Model (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>

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

尽管两个版本的 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接口。如果您还对 Bean 定义使用@Configuration类,则应扩展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 发出 GET 请求来访问类路径上 orders.wsdl 文件中定义的 WSDL(根据需要替换主机、端口和 servlet 上下文路径)

http://localhost:8080/spring-ws/orders.wsdl
所有 WsdlDefinition bean 定义都由 MessageDispatcherServlet 以其 bean 名称和后缀`.wsdl` 曝光。因此,如果 bean 名称是 echo,主机名是 server,Servlet 上下文(war 名称)是 spring-ws,则可以在 http://server/spring-ws/echo.wsdl 找到 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="http://localhost: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("http://localhost:8080/ordersService/");
    definition.setSchema(new SimpleXsdSchema(new ClassPathResource("echo.xsd")));

    return definition;
}

<dynamic-wsdl> 元素依赖于 DefaultWsdl11Definition 类。此定义类在 org.springframework/ws/wsdl/wsdl11/provider 包和 ProviderBasedWsdl4jDefinition 类中使用 WSDL 提供程序,以便在第一次请求时生成 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://localhost?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 发送新消息更新。如果您使用支持 IDLE 命令的 IMAP 服务器,则可以将 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 Framework 中的远程处理模块包含一个方便的 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 提供了一个 XmppMessageSender,为 MessageDispatcher 提供了一个 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 本地名称和 http://samples 命名空间 URI 的请求调用。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

任何布尔值、双精度、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.SoapHeaderorg.springframework.ws.soap.SoapHeaderElement(与 @SoapHeader 注释结合使用时)。

默认情况下启用。

JAXB2

使用 javax.xml.bind.annotation.XmlRootElementjavax.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

  • String

  • Node

  • 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.XmlRootElementjavax.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,它提供“拦截器”属性,即要使用的拦截器列表。EndpointInterceptors拦截请求 - 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 操作传输标头。

要使用 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 时,响应将不会发送回客户端。

Web 服务中可以使用许多标准的 EndpointInterceptor 实现。此外,还有 XwsSecurityInterceptor,在 XwsSecurityInterceptor 中进行了描述。

PayloadLoggingInterceptorSoapEnvelopeLoggingInterceptor

在开发 Web 服务时,记录传入和传出的 XML 消息可能很有用。Spring WS 通过 PayloadLoggingInterceptorSoapEnvelopeLoggingInterceptor 类来实现此功能。前者仅将消息的有效负载记录到 Commons Logging 日志中。后者记录整个 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 服务非常严格。通常,请求是否验证并不重要,只要端点能够获取足够的信息来满足请求即可。验证响应是个好主意,因为端点应遵守其模式。记住 Postel 定律:“对自己的行为保守;对接受他人的行为宽容。”

以下示例使用 PayloadValidatingInterceptor。在此示例中,我们使用 /WEB-INF/orders.xsd 中的模式来验证响应,但不验证请求。请注意,PayloadValidatingInterceptor 还可以通过设置 schemas 属性来接受多个模式。

<bean id="validatingInterceptor"
        class="org.springframework.ws.soap.server.endpoint.interceptor.PayloadValidatingInterceptor">
    <property name="schema" value="/WEB-INF/orders.xsd"/>
    <property name="validateRequest" value="false"/>
    <property name="validateResponse" value="true"/>
</bean>

当然,您也可以对 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 提供了 EndpointExceptionResolvers,以便在消息被与请求匹配的端点处理时发生意外异常时减轻痛苦。端点异常解析器有点类似于可以在 Web 应用程序描述符 web.xml 中定义的异常映射。但是,它们提供了一种更灵活的方式来处理异常。它们提供有关在引发异常时调用了哪个端点的信息。此外,以编程方式处理异常为您提供了更多选择,以便如何做出适当的响应。您可以通过返回带有特定错误代码和字符串的 SOAP 错误来处理异常,而不是通过给出异常和堆栈跟踪来公开应用程序的内部结构。

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

除了实现 EndpointExceptionResolver 接口(这仅仅是实现 resolveException(MessageContext, endpoint, Exception) 方法),您还可以使用其中一个提供的实现。最简单的实现是 SimpleSoapExceptionResolver,它创建一个 SOAP 1.1 服务器或 SOAP 1.2 接收器错误,并将异常消息用作错误字符串。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 Framework 中提供的标准测试工具。这不是必需的,但通常是设置测试的最简单方法。
3 应用程序上下文是标准 Spring-WS 应用程序上下文(请参阅 MessageDispatcherServlet),从 spring-ws-servlet.xml 读取。在这种情况下,应用程序上下文包含 CustomerEndpoint 的 Bean 定义(或者可能使用了 <context:component-scan />)。
4 @Before 方法中,我们使用 createClient 工厂方法创建 MockWebServiceClient
5 我们通过调用 sendRequest() 发送请求,其中包含静态导入的 RequestCreators 提供的 withPayload() RequestCreator(请参阅 使用 RequestCreatorRequestCreators)。

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

测试的这一部分可能看起来有点令人困惑,但 IDE 的代码完成功能非常有帮助。在键入 sendRequest( 后,IDE 可以为你提供可能的请求创建策略列表,前提是你已静态导入 RequestCreators。如果你已静态导入 ResponseMatchers,则 andExpect() 也适用。

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,允许一致、基于 XML 地访问 Web 服务。它还满足了对编组器和解组器的使用,以便您的服务层代码可以专门处理 Java 对象。

org.springframework.ws.client.core 包提供了使用客户端访问 API 的核心功能。它包含模板类,简化了 Web 服务的使用,就像核心 Spring JdbcTemplate 对 JDBC 所做的那样。Spring 模板类通用的设计原则是提供帮助程序方法来执行常见操作,对于更复杂的使用,则委托给用户实现的回调接口。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,它至少包含 jms: 前缀和目标名称。JMS URI 的一些示例包括:jms:SomeQueuejms:SomeTopic?priority=3&deliveryMode=NON_PERSISTENTjms:RequestQueue?replyToName=ResponseName。有关此 URI 语法的更多信息,请参阅 JmsMessageSenderJavadoc

默认情况下,JmsMessageSender 发送 JMS BytesMessage,但您可以使用 JMS URI 上的 messageType 参数覆盖此设置以使用 TextMessages,例如 jms:Queue?messageType=TEXT_MESSAGE。请注意,BytesMessages 是首选类型,因为 TextMessages 不可靠地支持附件和字符编码。

以下示例展示了如何将 JMS 传输与 ActiveMQ 连接工厂结合使用

<beans>

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

    <bean id="connectionFactory" class="org.apache.activemq.ActiveMQConnectionFactory">
        <property name="brokerURL" value="vm://localhost?broker.persistent=false"/>
    </bean>

    <bean id="webServiceTemplate" class="org.springframework.ws.client.core.WebServiceTemplate">
        <constructor-arg ref="messageFactory"/>
        <property name="messageSender">
            <bean class="org.springframework.ws.transport.jms.JmsMessageSender">
                <property name="connectionFactory" ref="connectionFactory"/>
            </bean>
        </property>
        <property name="defaultUri" value="jms:RequestQueue?deliveryMode=NON_PERSISTENT"/>
    </bean>

</beans>
电子邮件传输

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。以下示例向 Web 服务发送一条简单的 XML 消息

import java.io.StringReader;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;

import org.springframework.ws.WebServiceMessageFactory;
import org.springframework.ws.client.core.WebServiceTemplate;
import org.springframework.ws.transport.WebServiceMessageSender;

public class WebServiceClient {

    private static final String MESSAGE =
        "<message xmlns=\"http://tempuri.org\">Hello, Web Service World</message>";

    private final WebServiceTemplate webServiceTemplate = new WebServiceTemplate();

    public void setDefaultUri(String defaultUri) {
        webServiceTemplate.setDefaultUri(defaultUri);
    }

    // send to the configured default URI
    public void simpleSendAndReceive() {
        StreamSource source = new StreamSource(new StringReader(MESSAGE));
        StreamResult result = new StreamResult(System.out);
        webServiceTemplate.sendSourceAndReceiveToResult(source, result);
    }

    // send to an explicit URI
    public void customSendAndReceive() {
        StreamSource source = new StreamSource(new StringReader(MESSAGE));
        StreamResult result = new StreamResult(System.out);
        webServiceTemplate.sendSourceAndReceiveToResult("http://localhost:8080/AnotherWebService",
            source, result);
    }

}
<beans xmlns="http://www.springframework.org/schema/beans">

    <bean id="webServiceClient" class="WebServiceClient">
        <property name="defaultUri" value="http://localhost:8080/WebService"/>
    </bean>

</beans>

前面的示例使用 WebServiceTemplate 向位于 http://localhost:8080/WebService 的 Web 服务发送一条“Hello, World”消息(在 simpleSendAndReceive() 方法的情况下),并将结果写入控制台。WebServiceTemplate 注入默认 URI,这是因为在 Java 代码中没有明确提供 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 Framework 参考文档。)通过使用编码器,你的应用程序代码可以专注于正在发送或接收的业务对象,而无需关注它如何表示为 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 操作头。
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 Framework 中提供的标准测试工具。这不是必需的,但通常是设置测试的最简单方法。
3 CustomerClientintegration-test.xml 中配置,并使用 @Autowired 绑定到此测试中。
4 @Before 方法中,我们通过使用 createServer 工厂方法来创建一个 MockWebServiceServer
5 我们通过调用 expect() 来定义期望,其中带有由静态导入的 RequestMatchers 提供的 payload() RequestMatcher(请参阅 使用 RequestMatcherRequestMatchers)。

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

测试的这一部分可能看起来有点令人困惑,但 IDE 的代码完成功能非常有帮助。在键入 expect( 之后,IDE 可以为你提供可能的请求匹配策略列表,前提是你静态导入了 RequestMatchers。如果你静态导入了 ResponseCreators,则同样适用于 andRespond(

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 实现,供你在测试中使用。你通常会静态导入此类。

ResponseCreators 类提供以下响应

ResponseCreators 方法 说明

withPayload()

使用给定的有效负载创建响应消息。

withError()

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

withException()

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

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

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

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

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

本章说明如何向您的 Web 服务添加 WS-Security 方面。我们重点关注 WS-Security 的三个不同领域

  • 身份验证:这是确定主体是否为其声称的身份的过程。在此上下文中,“主体”通常指用户、设备或其他可以在您的应用程序中执行操作的系统。

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

  • 加密和解密:加密是将数据转换为无法在没有适当密钥的情况下读取的表单的过程。它主要用于对无权访问的人员隐藏信息。解密是加密的逆过程。它是将加密数据转换回可读表单的过程。

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

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

7.1. XwsSecurityInterceptor

XwsSecurityInterceptor 是一个 EndpointInterceptor(请参阅 拦截请求 — EndpointInterceptor 接口),它基于 SUN 的 XML 和 Web 服务安全包 (XWSS)。此 WS-Security 实现是 Java Web 服务开发人员包 (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>
签名消息

在对消息进行签名时,XwsSecurityInterceptor 会将 BinarySecurityToken 添加到消息中。它还会添加一个 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 属性并指定 securementUsername 和`securementPassword` 一样简单。

可以通过设置 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 只需要一个加密密钥,而嵌入式密钥名称验证则委托给回调处理程序。)

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

<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 元素)的消息,您可以配置一个指向存储对称密钥的密钥库的 KeyStoreCallbackHandlersymmetricKeyPassword 属性指示密钥的密码,密钥名称由 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 需要一个回调处理程序来获取密钥。因此,您必须使用指向适当密钥库的 KeyStoreCallbackHandler 提供 securementCallbackHandler。默认情况下,生成的 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 相同。有关更多信息,请参阅 安全异常处理

III. 其他资源

除了此参考文档之外,许多其他资源可能有助于您了解如何使用 Spring Web 服务。本节列举了这些额外的第三方资源。

参考书目

  • [waldo-94] Jim Waldo、Ann Wollrath 和 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 年