# Spring Web 服务参考文档

# 序言

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

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

# 一.导言

这是参考文献是一个概述的第一部分 Spring Web 服务和底层概念。 Spring-WS 随后被介绍,并且概念契约优先 Web 服务开发的背后的概念被解释。

# 1.什么是 Spring Web 服务?

# 1.1.导言

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

人们使用 Spring-WS 的原因有很多,但在遵循 Web 服务最佳实践时,大多数人在发现其他 SOAP 堆栈缺乏之后都会被它所吸引。 Spring-WS 使最佳实践成为一种简单的实践。这包括一些实践,如 WS-I 基本概要文件、契约优先的开发,以及在契约和实现之间的松散耦合。 Spring Web 服务的其他关键特性是:

# 1.1.1.强大的映射

你可以将传入的 XML 请求分发到任何对象,这取决于消息有效负载、SOAP 动作报头或 XPath 表达式。

# 1.1.2.XML API 支持

传入的 XML 消息不仅可以通过标准的 JAXP API(如 DOM、SAX 和 STAX)来处理,还可以通过 JDOM、DOM4J、XOM 甚至编组技术来处理。

# 1.1.3.灵活的 XML 编组

Spring Web 服务构建在 Spring 框架中的对象/XML 映射模块上,该模块支持 JAXB1 和 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 安全性的集成

Spring Web 服务的 WS-Security 实现提供了与 Spring 安全性的集成。这意味着你也可以为你的 SOAP 服务使用你现有的 Spring 安全配置。

# 1.1.7. Apache 许可证

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

# 1.2.运行时环境

Spring Web 服务需要一个标准的 Java8 运行时环境。 Spring-WS 是基于 Spring Framework4.0.9 构建的,但支持更高的版本。

Spring-WS 由许多模块组成,这些模块在本节的其余部分中进行了描述。

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

  • 核心模块(spring-ws-core.jar)是 Spring 的 Web 服务功能的核心部分。它提供了中心[WebServiceMessage](#web-service-messages)和[SoapMessage](#SOAP-message)接口、服务器端框架(具有强大的消息调度功能)、用于实现 Web 服务端点的各种支持类,以及客户端WebServiceTemplate

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

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

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

spring deps

# 1.3.支持的标准

Spring Web 服务支持以下标准:

  • SOAP1.1 和 1.2

  • WSDL1.1 和 2.0(仅 WSDL1.1 支持基于 XSD 的生成)

  • WS-I 基本配置文件 1.0、1.1、1.2 和 2.0

  • WS-Addressing1.0 和 2004 年 8 月的草案

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

# 2.为什么要先签约?

在创建 Web 服务时,有两种开发风格:Contract-last 和 Contract-first。当你使用契约-最后一种方法时,首先从 Java 代码开始,然后让 Web 服务契约(在 WSDL 中——参见侧边栏)从中生成。当使用 Contract-First 时,你可以从 WSDL Contract 开始,并使用 Java 来实现该 Contract。

什么是 WSDL?

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

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

# 2.1.对象/XML 阻抗不匹配

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

这一部分的大部分内容都受到了[[alpine]和[[effective-Enterprise-java]](#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,再次是指Flight。类似的循环图在 Java 中非常常见。如果我们采用一种简单的方法将其转换为 XML,那么我们最终会得到这样的结果:

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

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

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

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

这解决了递归问题,但引入了新的问题。首先,你无法使用 XML 验证器来验证此结构。另一个问题是,在 SOAP(RPC/Encoded)中使用这些引用的标准方式已被弃用,而倾向于使用文档/文字(参见 WS-I)。

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

# 2.2.合同优先与合同优先

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

# 2.2.1.脆弱性

如前所述,Contract-Last 开发风格导致你的 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,我们可以在 Contract 和实现之间有一个更松散的耦合。这样的松散耦合使我们能够在一个类中实现契约的两个版本。例如,我们可以使用 XSLT 样式表将任何“旧式”消息转换为“新式”消息。

# 3.书面合同优先的 Web 服务

本教程向你展示了如何编写契约优先的 Web 服务——也就是说,如何开发首先从 XML 模式或 WSDL 契约开始,然后是 Java 代码的 Web 服务。 Spring-WS 专注于这种开发风格,本教程应该帮助你开始学习。请注意,本教程的第一部分几乎不包含 Spring-WS 特定的信息。它主要是关于 XML、XSD 和 WSDL 的。第二部分着重于用 Spring-WS 实现这个契约。

在进行契约优先的 Web 服务开发时,最重要的事情是考虑 XML。这意味着 Java 语言的概念不那么重要。跨线发送的是 XML,你应该关注这一点。使用 Java 来实现 Web 服务是一个实现细节。

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

# 3.1.信息

在这一节中,我们将重点关注发送到 Web 服务和从 Web 服务发送的实际 XML 消息。我们首先要确定这些信息是什么样子的。

# 3.1.1.假日

在该场景中,我们必须处理假期请求,因此确定 XML 中的假期是什么样子是有意义的:

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

假日由开始日期和结束日期组成。我们还决定对日期使用标准的ISO 8601 (opens new window)日期格式,因为这节省了大量的解析麻烦。我们还向元素添加了一个命名空间,以确保我们的元素可以在其他 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](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 服务。RELAXNG 和 Schematron 比 XML Schema 更简单。不幸的是,它们并没有得到跨平台的广泛支持。因此,我们使用了 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>

显然,我们必须确保开始日期和结束日期是真正的日期。XMLSchema 有一个很好的内置date类型,我们可以使用它。我们还将NCNames 更改为string实例。最后,我们将sequence中的<HolidayRequest/>修改为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 我们使用xs:date数据类型(包括一年、一个月和一天)表示<StartDate/><EndDate/>
3 xs:string用于名字和姓氏。

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

# 3.3.服务合同

服务契约通常表示为WSDL (opens new window)文件。请注意,在 Spring-WS 中,不需要手工编写 WSDL。基于 XSD 和一些约定, Spring-WS 可以为你创建 WSDL,如在标题为实现端点的部分中所解释的那样。本节的其余部分展示了如何手工编写 WSDL。你可能希望跳到下一节

我们从标准的前导码开始我们的 WSDL,并通过导入我们现有的 XSD。为了将模式从定义中分离出来,我们为 WSDL 定义使用了一个单独的名称空间:[http://mycompany.com/hr/definitions](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](http://mycompany.com/RequestHoliday),但任何 URI 都有效),并确定你希望请求到达的locationURL(在本例中,[http://mycompany.com/humanresources](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 我们定义HolidayRequest消息,它在portType中使用。
3 HolidayRequest类型在模式中定义。
4 我们定义HumanResource端口类型,它在binding中使用。
5 我们定义HumanResourceBinding绑定,它在port中使用。
6 我们使用文档/文字样式。
7 字面[http://schemas.xmlsoap.org/soap/http](http://schemas.xmlsoap.org/soap/http)表示 HTTP 传输。
8 soapAction属性表示每个请求都将发送的SOAPActionHTTP 头。
9 [http://localhost:8080/holidayService/](http://localhost:8080/holidayService/)地址是可以调用 Web 服务的 URL。

前面的清单显示了最终的 WSDL。在下一节中,我们将描述如何实现生成的模式和 WSDL。

# 3.4.创建项目

在本节中,我们使用Maven (opens new window)为我们创建初始项目结构。这样做并不是必需的,但大大减少了我们为设置 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-WSMessageDispatcherServlet,并将所有传入的请求映射到这个 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。因此,如果你定义一个名为MessageDispatcherServlet'dynamite',则 Spring-WS 特定配置文件的名称将变为WEB-INF/dynamite-servlet.xml

(你可以在[[tutorial.example.sws-conf-file]](#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 (opens new window)来处理 XML 消息。我们还使用XPath (opens new window),因为它允许我们选择 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对其进行注释。
接下来,我们使用 JDOM2API 设置 XPath 表达式。有四个表达式://hr:StartDate用于提取<StartDate>文本值,//hr:EndDate用于提取结束日期,以及两个用于提取员工的姓名。
3 @PayloadRoot注释告诉 Spring-WShandleHolidayRequest方法适合处理 XML 消息。该方法可以处理的消息类型由注释值指示。在这种情况下,它可以
处理具有HolidayRequest本地部分和[http://mycompany.com/hr/schemas](http://mycompany.com/hr/schemas)名称空间的 XML 元素。
关于将消息映射到端点的更多信息将在下一节中提供。
4 主要的处理方法是handleHolidayRequest(..)方法,它从传入的 XML 消息中传递<HolidayRequest/>元素。@RequestPayload注释表示holidayRequest参数应该映射到
请求消息的有效负载。我们使用 XPath 表达式从 XML 消息中提取字符串值,并通过使用SimpleDateFormatparseData方法)将这些值转换为Date对象。使用这些值,我们调用业务服务上的一个方法,
通常,这会导致启动一个数据库事务,并更改数据库中的一些记录,
最后,我们定义一个void返回类型,这表明我们不想向 Spring-WS 发送响应消息。
如果我们想要响应消息,我们可以返回一个 JDOM 元素来表示响应消息的有效负载。

使用 JDOM 只是处理 XML 的选项之一。其他选项包括 DOM、DOM4J、XOM、SAX 和 STAX,但也包括 JAXB、Castor、XMLBeans、JiBX 和 XStream 等编组技术,如下一章中所述。我们之所以选择 JDOM,是因为它让我们能够访问原始 XML,也因为它基于类(而不是 W3C DOM 和 DOM4J 那样的接口和工厂方法),这使得代码不那么冗长。我们使用 XPath 是因为它不像编组技术那么脆弱。只要能够找到日期和名称,就不需要严格的模式一致性。

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

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

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

<beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:context="http://www.springframework.org/schema/context"
  xmlns:sws="http://www.springframework.org/schema/web-services"
  xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
  http://www.springframework.org/schema/web-services http://www.springframework.org/schema/web-services/web-services-2.0.xsd
  http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd">

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

  <sws:annotation-driven/>

</beans>

# 3.5.2.将消息路由到端点

作为编写端点的一部分,我们还使用@PayloadRoot注释来指示哪种消息可以由handleHolidayRequest方法处理。在 Spring-WS 中,这个过程是EndpointMapping的职责。在这里,我们使用PayloadRootAnnotationMethodEndpointMapping基于内容路由消息。下面的清单显示了我们之前使用的注释:

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

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

还有其他将端点映射到 XML 消息的方法,这在下一章中进行了描述。

# 3.5.3.提供服务和存根实现

现在我们有了端点,我们需要HumanResourceService及其实现,供HolidayEndpoint使用。下面的清单显示了HumanResourceService接口:

package com.mycompany.hr.service;

import java.util.Date;

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

出于教程的目的,我们使用HumanResourceService的一个简单存根实现:

package com.mycompany.hr.service;

import java.util.Date;

import org.springframework.stereotype.Service;

@Service                                                                 (1)
public class StubHumanResourceService implements HumanResourceService {
    public void bookHoliday(Date startDate, Date endDate, String name) {
        System.out.println("Booking holiday for [" + startDate + "-" + endDate + "] for [" + name + "] ");
    }
}
1 StubHumanResourceService注释为@Service。这将类标记为一个业务外观,这使得它成为@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](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 等)并将浏览器指向这个位置 (opens new window),则会看到生成的 WSDL。此 WSDL 已准备好供客户机使用,例如soapUI (opens new window)或其他 SOAP 框架。

本教程到此结束。教程代码可以在 Spring-WS 的完整发行版中找到。如果你希望继续,请查看作为发行版一部分的 echo 示例应用程序。在此之后,看一下航空公司示例,它有点复杂,因为它使用了 JAXB、WS-Security、 Hibernate 和事务性服务层。最后,你可以阅读参考文档的其余部分。

# ii。参考文献

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

# 4.共享组件

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

# 4.1.Web 服务消息

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

# 4.1.1.WebServiceMessage

Spring Web 服务的核心接口之一是WebServiceMessage。该接口表示与协议无关的 XML 消息。接口包含以javax.xml.transform.Sourcejavax.xml.transform.Result的形式提供对消息有效负载的访问的方法。SourceResult是标记接口,表示对 XML 输入和输出的抽象。具体的实现封装了各种 XML 表示,如下表所示:

Source or Result implementation 打包的 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.InputStream,或java.io.Reader
javax.xml.transform.stream.StreamResult java.io.Filejava.io.OutputStream,或java.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 的,SAAJ 是一种带有 Java 附件 API 的 SOAP。另一种是基于 Axis2 的公理(Axis Object Model)。

# SaajSoapMessageFactory

SaajSoapMessageFactory使用 SOAP with Attachments API for Java 来创建SoapMessage实现。SAAJ 是 J2EE1.4 的一部分,因此大多数现代应用程序服务器都应该支持它。以下是常见应用程序服务器提供的 SAAJ 版本的概述:

应用程序服务器 SAAJ Version
BEA WebLogic8 1.1
BEA WebLogic9 1.1/1.21
IBMWebSphere6 1.2
Sun Glassfish 鱼 1 1.3
1WebLogic9 在 SAAJ1.2 实现中有一个已知的错误:它实现了所有的 1.2 接口,但在调用时抛出一个UnsupportedOperationException。 Spring Web 服务有一个解决方法:当在 WebLogic9 上操作时,它使用 SAAJ1.1.

此外,Java SE6 还包括 SAAJ1.3.你可以按以下方式连接SaajSoapMessageFactory:

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

AxiomSoapMessageFactory使用 Axis2 对象模型来创建SoapMessage实现。AXIOM 是基于 STAX 的,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。

# SOAP1.1 或 1.2

SaajSoapMessageFactoryAxiomSoapMessageFactory都具有soapVersion属性,可以在其中注入一个SoapVersion常数。默认情况下,该版本是 1.1,但你可以将其设置为 1.2:

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:util="http://www.springframework.org/schema/util"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans-2.0.xsd
       http://www.springframework.org/schema/util
       http://www.springframework.org/schema/util/spring-util-2.0.xsd">

    <bean id="messageFactory" class="org.springframework.ws.soap.saaj.SaajSoapMessageFactory">
        <property name="soapVersion">
            <util:constant static-field="org.springframework.ws.soap.SoapVersion.SOAP_12"/>
        </property>
    </bean>

</beans>

在前面的示例中,我们定义了只接受 SOAP1.2 消息的SaajSoapMessageFactory

尽管两个版本的 SOAP 在格式上非常相似,但 1.2 版本并不向后兼容 1.1,因为它使用了不同的 XML 命名空间。SOAP1.1 和 1.2 之间的其他主要区别包括错误的不同结构,以及SOAPActionHTTP 头实际上已被弃用,尽管它们仍然有效。

使用 SOAP 版本号(或一般的 WS-* 规范版本号)需要注意的一件重要事情是,规范的最新版本通常不是最流行的版本。对于 SOAP,这意味着(目前)使用的最佳版本是 1.1.版本 1.2 可能会在未来变得更受欢迎,但 1.1 是目前最安全的选择。

# 4.1.4.MessageContext

通常,消息是成对出现的:请求和响应。在客户端创建一个请求,该请求通过一些传输发送到服务器端,在服务器端生成响应。这个响应被发送回客户机,在那里进行读取。

在 Spring Web 服务中,这样的会话包含在MessageContext中,其具有获取请求和响应消息的属性。在客户端,消息上下文由[WebServiceTemplate](#client-web-service-template)创建。在服务器端,消息上下文是从特定于传输的输入流中读取的。例如,在 HTTP 中,从HttpServletRequest读取,然后将响应写回HttpServletResponse

# 4.2.TransportContext

SOAP 协议的一个关键属性是它试图与传输无关。这就是为什么 Spring-WS 不支持通过 HTTP 请求 URL 而是通过消息内容将消息映射到端点的原因。

然而,有时需要获得对底层传输的访问,无论是在客户端还是服务器端。为此, Spring Web 服务具有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 服务有两种在应用程序中使用 XPath 的方式:更快的XPathExpression或更灵活的XPathTemplate

# 4.3.1.XPathExpression

XPathExpression是对已编译的 XPath 表达式的抽象,例如 Java5javax.xml.xpath.XPathExpression接口或 JaxenXPath类。要在应用程序上下文中构造表达式,可以使用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 服务时,在消息到达或发送之前查看消息的内容可能非常有用。 Spring Web 服务通过标准的 Commons 日志记录接口提供了这种功能。

确保使用 Commons 日志记录版本 1.1 或更高版本。较早的版本存在类加载问题,并且不与 log4j 跟踪级别集成。

要记录所有服务器端消息,请将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.theMessageDispatcher

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具有用于设置端点适配器的几个属性,mappings异常解析器。但是,不需要设置这些属性,因为 Dispatcher 会自动检测在应用程序上下文中注册的所有类型。你应该仅在需要重写检测时才设置这些属性。

消息调度器对消息上下文进行操作,而不是对特定于传输的输入流和输出流进行操作。因此,传输特定的请求需要读入MessageContext。对于 HTTP,这是用WebServiceMessageReceiverHandlerAdapter(这是 Spring webHandlerInterceptor)完成的,以便MessageDispatcher可以连接到标准DispatcherServlet中。然而,还有一种更方便的方法可以做到这一点,它显示在[MessageDispatcherServlet](#message-dispatcher- Servlet)中。

# 5.2.运输

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

# 5.2.1.MessageDispatcherServlet

MessageDispatcherServlet是一个标准Servlet,它方便地从标准 Spring webDispatcherServlet扩展并包装一个MessageDispatcher。因此,它将这些属性合并为一个。作为MessageDispatcher,它遵循上一节中描述的相同的请求处理流程。作为 Servlet,在 Web 应用程序的MessageDispatcherServlet中配置了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 服务的第一步,因为 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 服务 bean,比如端点、编组器等等。

作为web.xml的替代方案,如果你在 Servlet 3+ 环境上运行,则可以通过编程方式配置 Spring-WS。为此, Spring-WS 提供了许多抽象基类,它们扩展了 Spring 框架中的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的编程配置的更多详细信息,请参阅[AbstractMessageDispatcherServletInitializer](https://DOCS. Spring.io/ Spring-ws/DOCS/current/org/springframework/ws/transport/http/support/AbstractMessageDispatcherServletInitializer.html)和[<<gtr="530"/>](https://DOCS. Spring..io//// Spring-Messessframews/currumblessframews/curractork/sprintionframews/support/initializer.html)

# 自动 WSDL 曝光

MessageDispatcherServlet自动检测在其 Spring 容器中定义的任何WsdlDefinitionbean。所有检测到的WsdlDefinitionbean 也都通过WsdlDefinitionHandlerAdapter公开。这是一种方便的方式,可以通过定义一些 bean 向客户机公开你的 WSDL。

通过示例的方式,考虑下面的<static-wsdl>定义,在 Spring-WS 配置文件(/WEB-INF/[servlet-name]-servlet.xml)中定义。请注意id属性的值,因为它是在公开 WSDL 时使用的。

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

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

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

你可以通过GET请求访问在 Classpath 上的orders.wsdl文件中定义的 WSDL,该请求具有以下形式的 URL(酌情替换主机、端口和 Servlet 上下文路径):

http://localhost:8080/spring-ws/orders.wsdl
所有WsdlDefinition Bean 定义都由MessageDispatcherServlet在其 Bean 名称下以.wsdl为后缀的MessageDispatcherServlet公开。因此,如果 Bean 名称是echo,主机名称是server,而 Servlet 上下文(WAR 名称)是spring-ws,则可以在[http://server/spring-ws/echo.wsdl](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](https://DOCS. Spring.io/ Spring-ws/DOCS/current/org/springframework/ws/transport/http/wsdldefinitionhandleradapter.html)类上查看类级 Javadoc,以了解整个转换过程的更多信息。

Spring 作为手工编写 WSDL 并用<static-wsdl>公开它的一种替代方法,Web 服务还可以从 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](https://DOCS. Spring.io/ Spring-ws/sites/1.5/apDOCS/org/springframework/ws/wsdl/wsdl11/provider/package-summary.html)包和[ProviderBasedWsdl4jDefinition](https://DOCS. Spring.io/ Spring-ws/docs/current/org/springframews/wsdlframews/wsdl/wsdl/wsdl11/providwsdl4jsdl)类中使用 WSDL 提供程序,以生成首次如果有必要,请查看这些类的类级 Javadoc,以了解如何扩展此机制。

DefaultWsdl11Definition(因此,<dynamic-wsdl>标记)通过使用约定从 XSD 模式构建 WSDL。它对模式中的所有element元素进行迭代,并为所有元素创建一个message。接下来,它为所有以定义的请求或响应后缀结尾的消息创建一个 WSDL。默认的请求后缀是Request。默认的响应后缀是Response,但是可以通过分别在<dynamic-wsdl />上设置requestSuffixresponseSuffix属性来更改这些后缀。它还基于操作构建了portTypebindingservice

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

要使用多个模式(通过包含或导入),你可以将 CommonsXMLSchema 放在类路径上。如果 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 MVCDispatcherServlet中连接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,Dispatcher 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 服务通过在 Spring 框架中提供的 JMS 功能支持服务器端 JMS 处理。 Spring Web 服务提供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.电子邮件传输

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

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

作为轮询方法的一种替代方法(轮询方法非常低效),有一种使用 IMAP IDLE 的监视策略。IDLE 命令是 IMAP 电子邮件协议的可选扩展,它允许邮件服务器异步向MailMessageReceiver发送新的消息更新。如果使用支持 IDLE 命令的 IMAP 服务器,则可以将ImapIdleMonitoringStrategy插入到monitoringStrategy属性中。除了支持的服务器外,还需要使用 JavaMail1.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 服务提供了基于 Sun 的 JRE1.6的传输。嵌入式 HTTP 服务器是一个独立的服务器,配置起来很简单。它提供了一种比传统 Servlet 容器更轻的替代品。

使用嵌入式 HTTP 服务器时,不需要外部部署描述符(web.xml)。你只需要定义服务器的一个实例,并将其配置为处理传入的请求。核心 Spring 框架中的远程处理模块包含一个用于 HTTP 服务器的方便的工厂 Bean:SimpleHttpServerFactoryBean。最重要的属性是contexts,它将上下文路径映射到相应的HttpHandler实例。

Spring Web 服务提供了HttpHandler接口的两种实现方式:[WsdlDefinitionHttpHandler](https://DOCS. Spring.io/ Spring-ws/DOCS/current/api/org/springframework/ws/transport/http/wsdldefinitionhttphandler.html)和[<<<<gtr="631"/>](https:///DOCS.io/ Spring-ws/DOCS/current/api/org/api/servicemessler.html)。前者将传入的 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 (opens new window)

# 5.2.6.XMPP 运输

Spring Web 服务 2.0 引入了对 XMPP 的支持,也称为 Jabber。该支持基于Smack (opens new window)库。

Spring 用于 XMPP 的 Web 服务支持非常类似于其它传输:有一个用于XmppMessageSenderWebServiceTemplate和一个用于与XmppMessageReceiver一起使用的MessageDispatcher

下面的示例展示了如何设置服务器端 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 (opens new window)是用于向 Web 服务发送和从 Web 服务发送二进制数据的机制。你可以通过MTOM 样本 (opens new window)查看如何使用 Spring WS 实现这一点。

# 5.3.端点

端点是 Spring-WS 服务器端支持的核心概念。端点提供对应用程序行为的访问,该行为通常由业务服务接口定义。端点解释 XML 请求消息,并使用该输入(通常)调用业务服务上的方法。该服务调用的结果表示为响应消息。 Spring-WS 具有各种各样的端点,并且使用各种方式来处理 XML 消息并创建响应。

你可以通过使用@Endpoint注释对类进行注释来创建端点。在这个类中,你可以通过使用各种各样的参数类型(例如 DOM 元素、JAXB2 对象等)来定义一个或多个处理传入 XML 请求的方法。你可以通过使用另一个注释(通常是@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处理方法](#server-atendpoint-methods)。
4 getOrder方法将OrderRequest(也用@RequestPayload注释)作为参数。这个参数是一个 JAXB2 支持的对象(用@XmlRootElement注释)。这意味着消息的有效负载将作为未编组对象传递给该方法。SoapHeader类型也作为参数给出。在调用时,此参数包含请求消息的 SOAP 头。该方法还用@ResponsePayload注释,表示返回值(theOrder)用作响应消息的有效负载。
有关端点方法的更多信息,请参见[@Endpoint处理方法](#server-atendpoint-methods)。
5 这个端点的两个处理方法被标记为@PayloadRoot,指示该方法可以处理哪种类型的请求消息:对于具有orderRequest本地名称和[http://samples](http://samples)命名空间 URI 的请求,调用getOrder方法。对于具有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 Reference documentation (opens new window)
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注释参数,以及任何其他注释。

Name 支持的参数类型 @RequestPayload required? Additional notes
TrAX javax.xml.transform.Source和子接口(DOMSourceSAXSourceStreamSource,和StAXSource Yes Enabled by default.
W3C DOM org.w3c.dom.Element Yes Enabled by default
dom4j org.dom4j.Element Yes Enabled when dom4j is on the classpath.
JDOM org.jdom.Element Yes Enabled when JDOM is on the classpath.
XOM nu.xom.Element Yes Enabled when XOM is on the classpath.
StAX javax.xml.stream.XMLStreamReaderjavax.xml.stream.XMLEventReader Yes Enabled when StAX is on the classpath.
XPath 任何布尔的,double 的,Stringorg.w3c.Nodeorg.w3c.dom.NodeList的,或者可以从String转换为 Spring 转换服务 (opens new window)的类型,并用@XPathParam注释。 No Enabled by default, see the section called XPathParam.
Message context org.springframework.ws.context.MessageContext No Enabled by default.
SOAP org.springframework.ws.soap.SoapMessageorg.springframework.ws.soap.SoapBodyorg.springframework.ws.soap.SoapEnvelopeorg.springframework.ws.soap.SoapHeader,以及org.springframework.ws.soap.SoapHeaderElements 与@SoapHeader注释结合使用。 No Enabled by default.
JAXB2 javax.xml.bind.annotation.XmlRootElementjavax.xml.bind.JAXBElement注释的任何类型。 Yes Enabled when JAXB2 is on the classpath.
OXM Spring OXM[Unmarshaller](https://DOCS. Spring.io/ Spring/DOCS/current/ Spring-framework-reference/data-access.html#OXM-marshaller-unmarshaller)支持的任何类型。 Yes Enabled when the unmarshaller attribute of <sws:annotation-driven/> is specified.

下面的几个示例展示了可能的方法签名。将请求消息的有效负载作为 DOMorg.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)时调用以下方法。消息的有效负载也被指定为 DOMElement。整个消息上下文作为第三个参数传递。

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

正如你所看到的,在定义如何处理方法签名时,有很多可能性。你甚至可以扩展此机制以支持你自己的参数类型。参见[DefaultMethodEndpointAdapter](https://DOCS. Spring.io/ Spring-ws/DOCS/current/api/org/springframework/ws/server/endpoint/adapter/defaultMethodPointadapter.html)和[<gtr="736"/>](<gtr://DOCS. Spring.io/ Spring-ws/DOCS/current/api/api/org/org/apframews/ws/server/endpoint/endpoint/dargumentver.html/metter/method/dapter.html)的 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](http://samples)名称空间。这是通过@Namespace注释完成的。或者,我们可以将此注释放置在类型级别上,以便对所有处理程序方法使用相同的名称空间映射,或者甚至在包级别(在package-info.java中)将其用于多个端点。

通过使用@XPathParam,你可以绑定到 XPath 支持的所有数据类型:

  • booleanBoolean

  • doubleDouble

  • String

  • Node

  • NodeList

除了这个列表之外,你还可以使用可以从String转换为 Spring 转换服务 (opens new window)的任何类型。

# 处理方法返回类型

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

要将返回值映射到响应消息的有效负载,你需要使用@ResponsePayload注释对方法进行注释。这个注释告诉 Spring-WS 返回值需要绑定到响应有效负载。

下表描述了受支持的返回类型。它显示了支持的类型,是否应该用@ResponsePayload注释参数,以及任何其他注释。

Name 支持的返回类型 @ResponsePayload required? Additional notes
No response void No Enabled by default.
TrAX javax.xml.transform.Source和子接口(DOMSourceSAXSourceStreamSource,和StAXSource Yes Enabled by default.
W3C DOM org.w3c.dom.Element Yes Enabled by default
dom4j org.dom4j.Element Yes Enabled when dom4j is on the classpath.
JDOM org.jdom.Element Yes Enabled when JDOM is on the classpath.
XOM nu.xom.Element Yes Enabled when XOM is on the classpath.
JAXB2 javax.xml.bind.annotation.XmlRootElementjavax.xml.bind.JAXBElement注释的任何类型。 Yes Enabled when JAXB2 is on the classpath.
OXM Spring OXM[Marshaller](https://DOCS. Spring.io/ Spring/DOCS/current/ Spring-framework-reference/data-access.html#OXM-marshaller-unmarshaller)支持的任何类型。 Yes Enabled when the marshaller attribute of <sws:annotation-driven/> is specified.

当涉及到定义处理方法签名时,有很多可能性。甚至可以扩展此机制以支持你自己的参数类型。参见[DefaultMethodEndpointAdapter](https://DOCS. Spring.io/ Spring-ws/DOCS/current/api/org/springframework/ws/server/endpoint/adapter/defaultMethodPointAdapter.html)和[<MethodReturnValueHandler](https://DOCS. Spring.io/ Spring-ws/DOCS/current/api/org/springframework/ws/server/endpoint/endpoint/adapter/methandler.html)的类级 Javadoc,以查看如何。

# 5.4.端点映射

端点映射负责将传入消息映射到适当的端点。默认情况下,一些端点映射是启用的——例如,PayloadRootAnnotationMethodEndpointMappingSoapActionAnnotationMethodEndpointMapping。然而,我们首先需要研究EndpointMapping的一般概念。

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

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

大多数端点映射继承自AbstractEndpointMapping,它提供了一个“拦截器”属性,这是要使用的拦截器列表。EndpointInterceptors在[拦截请求—EndpointInterceptor接口](#server-endpoint-interceptor)中进行了讨论。此外,还有defaultEndpoint,这是当此端点映射不会导致匹配的端点时使用的默认端点。

正如Endpoints中所解释的,@Endpoint样式允许你在一个端点类中处理多个请求。这是MethodEndpointMapping的责任。此映射决定了将为传入的请求消息调用哪个方法。

有两个端点映射可以指示对方法的请求:PayloadRootAnnotationMethodEndpointMappingSoapActionAnnotationMethodEndpointMapping你可以通过在应用程序上下文中使用<sws:annotation-driven/>来启用这两个方法。

PayloadRootAnnotationMethodEndpointMapping使用@PayloadRoot注释,使用localPartnamespace元素,用特定的限定名称标记方法。每当出现带有有效负载根元素的限定名称的消息时,都会调用该方法。有关示例,请参见above

或者,SoapActionAnnotationMethodEndpointMapping使用@SoapAction注释来标记具有特定 SOAP 操作的方法。每当带有这个SOAPAction头的消息出现时,都会调用该方法。

# 5.4.1.WS-Addressing

WS-Addressing 指定了一种与传输无关的路由机制。它基于ToActionSOAP 头,它们分别指示 SOAP 消息的目的和意图。此外,WS-Addressing 允许你定义一个返回地址(用于正常消息和错误)和一个唯一的消息标识符,该标识符可用于相关性。有关 WS-Addressing 的更多信息,请参见https://en.wikipedia.org/wiki/WS-Addressing (opens new window)。下面的示例显示了一个 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),而操作设置为[http://example.com/fabrikam/mail/Delete](http://example.com/fabrikam/mail/Delete)。此外,还有一个消息标识符和一个回复地址。默认情况下,该地址是“匿名”地址,这表明应该使用与请求相同的通道(即 HTTP 响应)发送响应,但它也可以是另一个地址,如本例中所示。

Spring 在 Web 服务中,WS-Addressing 被实现为端点映射。通过使用此映射,可以将 WS-Addressing 操作与端点关联起来,类似于前面描述的SoapActionAnnotationMethodEndpointMapping

# 使用AnnotationActionEndpointMapping

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

要使用AnnotationActionEndpointMapping,请使用@Action注释来注释处理方法,类似于[@PayloadRoot@SoapAction处理方法](#server-atendpoint-methods)和端点映射中描述的注释。下面的示例展示了如何做到这一点:

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);
    }

}

前面的映射路由请求具有Action[http://samples/RequestOrder](http://samples/RequestOrder)getOrder方法的 WS-Addressing。带有[http://samples/CreateOrder](http://samples/CreateOrder)的请求被路由到order方法。

默认情况下,AnnotationActionEndpointMapping既支持 1.0(2006 年 5 月),也支持 2004 年 8 月版本的 WS-Addressing。这两个版本最受欢迎,可以与 Axis1 和 2、JAX-WS、Xfire、Windows 通信基础和 Windows 服务增强 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](http://www.example.com)作为有效负载根名称空间的 XML 消息。除了namespaceUri之外,我们还可以定义一个localPart属性,以进一步限制拦截器所应用的消息。最后,我们定义了两个拦截器,当消息具有[http://www.example.com/SoapAction](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](#security-xws-security-interceptor)中进行了描述。

# PayloadLoggingInterceptorSoapEnvelopeLoggingInterceptor

在开发 Web 服务时,记录传入和传出的 XML 消息可能很有用。 Spring WS 通过PayloadLoggingInterceptorSoapEnvelopeLoggingInterceptor类来促进这一点。前者只将消息的有效负载记录到 Commons 日志中。后者记录整个 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促进了这一点。此拦截器需要对一个或多个 W3CXML 或 RELAXNG 模式的引用,并且可以设置为验证请求、响应或两者。

请注意,请求验证听起来可能是个好主意,但它会使生成的 Web 服务变得非常严格。通常,只有在端点能够获得足够的信息来满足请求的情况下,请求是否有效才真正重要。验证响应是一个好主意,因为端点应该遵循其模式。记住波斯特定律:
“在你做的事情上要保守,在你接受别人的东西上要自由。”

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

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

当然,你也可以使用WsConfigurerAdapter方法,如前面所述,用于PayloadValidatingInterceptor

# 使用PayloadTransformingInterceptor

Spring 为了将有效负载转换为另一种 XML 格式,Web 服务提供了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,它创建一个 SOAP1.1 服务器或 SOAP1.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的异常映射到具有Invalid request的故障字符串的客户端 SOAP 故障,如下所示:

<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);
    }
}

每当在端点调用过程中使用构造函数字符串MyBusinessException抛出"Oops!"时,都会产生以下响应:

<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 服务 2.0 中介绍的测试特性编写集成测试。

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

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

集成测试支持位于org.springframework.ws.test.server包中。包中的核心类是MockWebServiceClient。其基本思想是,该客户机创建一个请求消息,然后将其发送到在标准MessageDispatcherServlet应用程序上下文中配置的端点(参见[MessageDispatcherServlet](#message-dispatcher- Servlet))。这些端点处理消息并创建响应。然后,客户机接收到此响应,并根据已注册的期望对其进行验证。

MockWebServiceClient的典型用法是:。

  1. 通过调用MockWebServiceClient.createClient(ApplicationContext)MockWebServiceClient.createClient(WebServiceMessageReceiver, WebServiceMessageFactory)来创建MockWebServiceClient实例。

  2. 通过调用sendRequest(RequestCreator)发送请求消息,可能是通过使用RequestCreator中提供的默认RequestCreator实现(可以静态导入)。

  3. 通过调用andExpect(ResponseMatcher)来设置响应期望,可能使用ResponseMatcher中提供的默认ResponseMatcher实现(可以静态导入)。可以通过链接andExpect(ResponseMatcher)调用来设置多个期望。

请注意,MockWebServiceClient(以及相关的类)提供了一个“fluent”API,因此你通常可以使用 IDE 中的代码完成功能来指导你完成设置模拟服务器的过程。
还请注意,你可以在单元测试中依赖于 Spring Web 服务中可用的标准日志记录功能。有时,检查请求或响应消息以找出特定测试失败的原因可能是有用的。有关更多信息,请参见消息日志记录和跟踪

例如,考虑以下 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 @Endpoint注释的CustomerEndpoint。见Endpoints
2 getCustomerCount()方法以CustomerCountRequest为参数,并返回CustomerCountResponse。这两个类都是 Marshaller 支持的对象。例如,JAXB2 可以支持@XmlRootElement注释。

下面的示例显示了CustomerEndpoint的典型测试:

import javax.xml.transform.Source;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.xml.transform.StringSource;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;

import org.springframework.ws.test.server.MockWebServiceClient;                       (1)
import static org.springframework.ws.test.server.RequestCreators.*;                   (1)
import static org.springframework.ws.test.server.ResponseMatchers.*;                  (1)

@RunWith(SpringJUnit4ClassRunner.class)                                               (2)
@ContextConfiguration("spring-ws-servlet.xml")                                        (2)
public class CustomerEndpointIntegrationTest {

  @Autowired
  private ApplicationContext applicationContext;                                      (3)

  private MockWebServiceClient mockClient;

  @Before
  public void createClient() {
    mockClient = MockWebServiceClient.createClient(applicationContext);               (4)
  }

  @Test
  public void customerEndpoint() throws Exception {
    Source requestPayload = new StringSource(
      "<customerCountRequest xmlns='http://springframework.org/spring-ws'>" +
        "<customerName>John Doe</customerName>" +
      "</customerCountRequest>");
    Source responsePayload = new StringSource(
      "<customerCountResponse xmlns='http://springframework.org/spring-ws'>" +
        "<customerCount>10</customerCount>" +
      "</customerCountResponse>");

    mockClient.sendRequest(withPayload(requestPayload)).                              (5)
      andExpect(payload(responsePayload));                                            (5)
  }
}
1 CustomerEndpointIntegrationTest导入MockWebServiceClient,静态导入RequestCreatorsResponseMatchers
2 该测试使用在 Spring 框架中提供的标准测试设施。这不是必需的,但通常是设置测试的最简单方法。
3 应用程序上下文是标准的 Spring-WS 应用程序上下文(参见[MessageDispatcherServlet](#message-dispatcher- Servlet)),从spring-ws-servlet.xml读取。在这种情况下,应用程序上下文包含对CustomerEndpoint的 Bean 定义(或者可能使用<context:component-scan />)。
4 @Before方法中,我们使用createClient工厂方法创建MockWebServiceClient
5 我们通过调用sendRequest()并使用静态导入的RequestCreators提供的RequestCreator来发送请求(请参见[usingRequestCreator)和RequestCreators(#server-test-request-creator))。

我们还通过调用静态导入的payload()提供的ResponseMatcher(参见[usingResponseMatcherResponseMatchers(#server-test-response-response-matcher-matcher=“967”))来设置响应期望。但是 IDE 的代码完成功能非常有帮助。在输入sendRequest(之后,你的 IDE 可以为你提供一个可能的请求创建策略的列表,前提是静态导入RequestCreators。这同样适用于andExpect(),前提是静态导入ResponseMatchers

# 5.6.2.使用RequestCreatorRequestCreators

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

public interface RequestCreator {

  WebServiceMessage createRequest(WebServiceMessageFactory messageFactory)
    throws IOException;

}

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

# 5.6.3.使用ResponseMatcherResponseMatchers

当端点处理了请求消息并且接收到了响应时,MockWebServiceClient可以验证此响应消息是否满足某些期望。客户机为此目的使用ResponseMatcher策略接口:

public interface ResponseMatcher {

    void match(WebServiceMessage request,
               WebServiceMessage response)
      throws IOException, AssertionError;

}

再次,你可以编写你自己的这个接口的实现,在消息不满足你的期望时抛出AssertionError实例,但是你当然不必这样做,因为ResponseMatchers类提供了标准的ResponseMatcher实现,供你在测试中使用。你通常静态地导入这个类。

ResponseMatchers类提供以下响应匹配器:

ResponseMatchers方法 Description
payload() Expects a given response payload.
validPayload() Expects the response payload to validate against given XSD schemas.
xpath() Expects a given XPath expression to exist, not exist, or evaluate to a given value.
soapHeader() Expects a given SOAP header to exist in the response message.
noFault() Expects that the response message does not contain a SOAP Fault.
mustUnderstandFault()clientOrSenderFault()serverOrReceiverFault(),和versionMismatchFault() Expects the response message to contain a specific SOAP Fault.

你可以通过链接andExpect()调用来设置多个响应期望:

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

有关ResponseMatchers提供的响应匹配器的更多信息,请参见Javadoc (opens new window)

# 6.在客户端上使用 Spring Web 服务

Spring-WS 提供了一个客户端 Web 服务 API,该 API 允许对 Web 服务进行一致的、XML 驱动的访问。它还迎合了 Marshaller 和 Unmarshaller 的使用,这样你的服务层代码就可以专门处理 Java 对象了。

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

  • XML 消息的发送和接收

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

  • 允许多种运输方式

# 6.1.使用客户端 API

本节描述如何使用客户端 API。有关如何使用服务器端 API,请参见Creating a Web service with Spring-WS

# 服务器端 WS-Addressing6.1.1.WebServiceTemplate

WebServiceTemplate是 Spring-WS 中用于客户端 Web 服务访问的核心类。它包含用于发送Source对象和以SourceResult接收响应消息的方法。此外,它还可以在跨传输发送对象之前将对象封送到 XML,并将任何响应 XML 重新封送到对象中。

# URI 和 Transports

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

# HTTP 传输

用于通过 HTTP 发送消息的WebServiceMessageSender接口有两种实现方式。默认的实现是HttpUrlConnectionMessageSender,它使用 Java 本身提供的功能。另一种选择是HttpComponentsMessageSender,它使用Apache HttpComponents HttpClient (opens new window)。如果你需要更高级、更易用的功能(如身份验证、HTTP 连接池等),请使用后者。

要使用 HTTP 传输,可以将defaultUri设置为[http://example.com/services](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 服务提供JmsMessageSender。这个类使用 Spring 框架的功能将WebServiceMessage转换为 JMSMessage,在QueueTopic上发送它,并接收响应(如果有的话)。

要使用JmsMessageSender,你需要将defaultUriSecurityContextHolder参数设置为一个 JMS URI,该 URI 至少由jms:前缀和一个目标名称组成。JMS URI 的一些示例是:jms:SomeQueuejms:SomeTopic?priority=3&deliveryMode=NON_PERSISTENTjms:RequestQueue?replyToName=ResponseName。有关此 URI 语法的更多信息,请参见[Javadoc forJmsMessageSender](https://DOCS. Spring.io/ Spring-ws/DOCS/current/api/org/springframework/ws/transport/jms/jmssagesender.html)。

默认情况下,JmsMessageSender发送 JMSWebServiceMessageSender,但是你可以通过在 JMS URI 上使用messageType参数(例如,jms:Queue?messageType=TEXT_MESSAGE)来覆盖此内容以使用TextMessages。请注意,BytesMessages是首选类型,因为TextMessages不可靠地支持附件和字符编码。

下面的示例展示了如何结合 ActiveMQ 连接工厂使用 JMS 传输:

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

要使用MailMessageSender,将defaultUriuri参数设置为mailtoURI——例如,mailto:[[email protected]](/cdn-cgi/l/email-protection)mailto:[[email protected]](/cdn-cgi/l/email-protection)?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 服务 2.0 引入了一种 XMPP 传输,你可以使用它通过 XMPP 发送和接收 Web 服务消息。客户端 XMPP 功能包含在XmppMessageSender类中。该类从请求WebServiceMessage创建一个 XMPP 消息,并通过 XMPP 发送它。然后,它会监听收到的响应消息。

要使用XmppMessageSender,请将defaultUriuri参数设置为XmppMessageSenderURI——例如,KeyStoreCallbackHandler。发送方还需要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>
# messageFactory消息工厂

除了消息发送者之外,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](http://localhost:8080/WebService)的 Web 服务发送“你好,世界”消息(在simpleSendAndReceive()方法的情况下),并将结果写到控制台。将WebServiceTemplate注入默认的 URI,因为 Java 代码中没有显式地提供 URI,所以使用该 URI。

注意,WebServiceTemplate类在配置后是线程安全的(假设它的所有依赖项也是线程安全的,这是 Spring-WS 附带的所有依赖项的情况),因此多个对象可以使用相同的共享MailMessageSender实例。WebServiceTemplate公开了一个零参数构造函数和messageFactorymessageSender Bean 属性,你可以使用这些属性来构造实例(通过使用 Spring 容器或普通 Java 代码)。或者,可以考虑从 Spring-WS 的messageSender便利基类派生,它公开了方便的 Bean 属性以实现简单的配置。(你不必扩展这个基类。它仅作为一种便利类提供。

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

为了方便普通 Java 对象的发送,WebServiceTemplate具有许多send(..)方法,这些方法将Object作为消息数据内容的参数。在WebServiceTemplate类中的方法send(..)将请求对象到 XML 的转换委托给Marshaller,并将响应 XML 到对象的转换委托给Unmarshaller。(有关编组和解组器的更多信息,请参见the Spring Framework reference documentation (opens new window)。)通过使用编组器,你的应用程序代码可以专注于正在发送或接收的业务对象,而不必关注它如何表示为 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

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

为了在客户机上设置 WS-Addressing 头,你可以使用RequestMatchers。此回调以所需的动作标头作为参数。它还具有用于指定 WS-Addressing 版本的构造函数和To头。如果没有指定,To头默认为正在建立的连接的 URL。

下面的示例将Action标头设置为[http://samples/RequestOrder](http://samples/RequestOrder):

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

# 6.1.5.使用WebServiceMessageExtractor

WebServiceMessageExtractor接口是一个低级的回调接口,你可以完全控制从接收到的Object中提取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 服务的类)时,你有两种可能的方法:

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

    这种方法的优点是容易实现。缺点是,你实际上并不是在测试通过连接发送的 XML 消息的确切内容,尤其是在模拟整个客户机类时。

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

第一种方法可以通过模拟框架(如 EasyMock、JMock 等)轻松实现。下一节将重点讨论如何使用 Spring Web 服务 2.0 中介绍的测试特性编写集成测试。

# 6.2.1.编写客户端集成测试

Spring Web 服务 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)来设置请求期望,可能需要使用RequestMatcher中提供的默认RequestMatcher实现(可以静态导入)。可以通过链接andExpect(RequestMatcher)调用来设置多个期望。

  3. 通过调用andRespond(ResponseCreator)创建适当的响应消息,可能需要使用ResponseCreator中提供的默认ResponseCreator实现(可以静态导入)。

  4. 正常地使用WebServiceTemplate,或者直接通过客户端代码。

  5. 请拨打MockWebServiceServer.verify(),以确保所有的期望都得到了满足。

请注意,MockWebServiceServer(以及相关的类)提供了一个“fluent”API,因此你通常可以使用 IDE 中的代码完成功能来指导你完成设置模拟服务器的过程。
还请注意,你可以在单元测试中依赖于 Spring Web 服务中可用的标准日志记录功能。有时,检查请求或响应消息以找出特定测试失败的原因可能是有用的。有关更多信息,请参见消息日志记录和跟踪

例如,考虑以下 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是一个由编组器支持的对象。例如,JAXB2 可以支持@XmlRootElement注释。
3 CustomerClient使用WebServiceTemplate提供的WebServiceTemplate将请求对象封送到 SOAP 消息中,并将其发送到 Web 服务。响应对象被解组为CustomerCountResponse

下面的示例显示了CustomerClient的典型测试:

import javax.xml.transform.Source;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.xml.transform.StringSource;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;

import static org.junit.Assert.assertEquals;

import org.springframework.ws.test.client.MockWebServiceServer;                         (1)
import static org.springframework.ws.test.client.RequestMatchers.*;                     (1)
import static org.springframework.ws.test.client.ResponseCreators.*;                    (1)

@RunWith(SpringJUnit4ClassRunner.class)                                                 (2)
@ContextConfiguration("integration-test.xml")                                           (2)
public class CustomerClientIntegrationTest {

  @Autowired
  private CustomerClient client;                                                        (3)

  private MockWebServiceServer mockServer;                                              (4)

  @Before
  public void createServer() throws Exception {
    mockServer = MockWebServiceServer.createServer(client);
  }

  @Test
  public void customerClient() throws Exception {
    Source requestPayload = new StringSource(
      "<customerCountRequest xmlns='http://springframework.org/spring-ws'>" +
        "<customerName>John Doe</customerName>" +
      "</customerCountRequest>");
    Source responsePayload = new StringSource(
      "<customerCountResponse xmlns='http://springframework.org/spring-ws'>" +
        "<customerCount>10</customerCount>" +
      "</customerCountResponse>");

    mockServer.expect(payload(requestPayload)).andRespond(withPayload(responsePayload));(5)

    int result = client.getCustomerCount();                                             (6)
    assertEquals(10, result);                                                           (6)

    mockServer.verify();                                                                (7)
  }

}
1 CustomerClientIntegrationTest导入MockWebServiceServer,静态导入RequestMatchersResponseCreators
2 该测试使用 Spring 框架中提供的标准测试设施。这不是必需的,但通常是设置测试的最简单方法。
3 CustomerClientintegration-test.xml中配置,并使用@Autowired连接到此测试中。
4 @Before方法中,我们使用createServer工厂方法创建MockWebServiceServer方法。
5 我们通过使用静态导入的RequestMatchers提供的payload()调用RequestMatcher来定义期望(参见[使用RequestMatcher)和RequestMatchers])。

我们还通过调用andRespond()与静态导入的withPayload()``ResponseCreator(参见[使用ResponseCreatorResponseCreator](client-test-responsion-#)来设置响应)。但是 IDE 的代码完成功能非常有帮助。在键入expect(之后,IDE 可以为你提供一个可能的请求匹配策略列表,前提是静态导入RequestMatchers。如果以静态方式导入ResponseCreators,则适用于andRespond(
6 我们在CustomerClient上调用getCustomerCount(),从而使用WebServiceTemplate。到目前为止,模板已经为“测试模式”设置好了,因此这个方法调用不会产生真正的连接。我们还根据方法调用的结果进行了一些 JUnit 断言。
7 我们在MockWebServiceServer上调用verify(),以验证是否实际收到了预期的消息。

# 6.2.2.使用RequestMatcherRequestMatchers

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

public interface RequestMatcher {

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

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

RequestMatcher类提供了以下请求匹配器:

RequestMatchers method 说明
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 (opens new window)

# 6.2.3.使用ResponseCreatorResponseCreators

当请求消息经过验证并满足已定义的期望时,MockWebServiceServer将为WebServiceTemplate创建一个要使用的响应消息。服务器为此目的使用ResponseCreator策略接口:

public interface ResponseCreator {

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

}

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

ResponseCreators类提供以下响应:

ResponseCreators method 说明
withPayload() 使用给定的有效负载创建响应消息。
withError() 在响应连接中创建错误。这个方法为你提供了测试错误处理的机会。
withException() 从响应连接读取时抛出异常。这个方法为你提供了测试异常处理的机会。
withMustUnderstandFault(), withClientOrSenderFault(), withServerOrReceiverFault(), or withVersionMismatchFault() 创建带有给定 SOAP 错误的响应消息。这个方法为你提供了测试错误处理的机会。

有关RequestMatchers提供的请求匹配器的更多信息,请参见Javadoc (opens new window)

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

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

  • 认证:这是确定委托人是否是他们所声称的人的过程。在这种情况下,“主体”通常是指可以在应用程序中执行操作的用户、设备或其他系统。

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

  • 加密和解密:加密是将数据转换为没有适当密钥就无法读取的形式的过程。它主要是用来隐藏信息,不让任何人知道它不是为谁准备的。解密是加密的反面。它是将加密数据转换回可读形式的过程。

这三个区域是通过使用XwsSecurityInterceptorWss4jSecurityInterceptor来实现的,我们在[XwsSecurityInterceptor](#security-xws-security-interceptor)和[usingWss4jSecurityInterceptor](#security-wss4j-security-interceptor)中分别进行了描述

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

#

XwsSecurityInterceptor是一个EndpointInterceptor(参见[拦截请求-EndpointInterceptor接口](#server-endpoint-interceptor)),它基于 Sun 的 XML 和 Web 服务安全包。这个 WS-Security 实现是 Java Web 服务开发人员包(Java WSDP (opens new window))的一部分。

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

请注意,XWSS 既需要 Sun1.5JDK,也需要 Sun Saaj 参考实现。WSS4J 拦截器没有这些需求(参见[usingWss4jSecurityInterceptor](#security-wss4j-security-interceptor))。

XwsSecurityInterceptor需要一个安全策略文件来操作。这个 XML 文件告诉拦截器从传入的 SOAP 消息中需要哪些安全方面,以及向传出的消息中添加哪些方面。策略文件的基本格式在下面的小节中进行了说明,但是你可以找到更深入的教程here (opens new window)。你可以使用policyConfiguration属性设置策略,这需要 Spring 资源。策略文件可以包含多个元素——例如,需要对传入消息使用用户名令牌,并对所有传出消息进行签名。它包含一个SecurityConfiguration元素(而不是XwsSecurityInterceptor元素)作为它的根。

此外,安全拦截器需要一个或多个CallbackHandler实例来操作。这些处理程序用于检索证书、私钥、验证用户凭据,等等。 Spring-WS 为最常见的安全问题提供了处理程序——例如,针对 Spring 安全身份验证管理器进行身份验证,并基于 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>

这个拦截器是通过使用 Classpath 上的securityPolicy.xml文件来配置的。它使用了稍后在文件中定义的两个回调处理程序。

# 7.1.1.密钥存储库

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

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

  • 私钥:这些密钥用于自我验证。私钥伴随着相应的公钥的证书链。在 WS-Security 字段中,这是消息签名和消息解密的帐户。

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

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

# 使用keytool

keytool程序是一个密钥和证书管理实用程序,它与你的 Java 虚拟机一起提供。你可以使用此工具创建新的密钥库,向它们添加新的私钥和证书,等等。提供keytool命令的完整引用超出了本文档的范围,但是你可以在命令行中找到here (opens new window)或使用@XmlRootElement命令的引用。

# 使用KeyStoreFactoryBean

要使用 Spring 配置轻松加载密钥库,可以使用KeyStoreFactoryBean。它有一个资源位置属性,你可以将其设置为指向要加载的密钥库的路径。可以给出一个密码来检查密钥存储库数据的 Integrity。如果没有给出密码,则不执行 Integrity 检查。下面的清单配置了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的属性:(keyStoretrustStore,和symmetricStore)。处理程序所使用的确切存储取决于该处理程序要执行的加密操作。对于私钥操作,使用keyStore。对于对称的键操作,使用symmetricStore。为了确定信任关系,使用了trustStore。下表表明了这一点:

加密操作 Keystore used
证书验证 First keyStore, then 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。你可以找到可能的子元素here (opens new window)的引用。下面的清单显示了如何包含RequireUsernameToken元素:

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

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

  • [SimplePasswordValidationCallbackHandler](#security-simple-password-validation-callback-handler)

  • [XwsSecurityInterceptor](#using-springplaintextpasswordvalidationcallbackhandler)

  • [JaasPlainTextPasswordValidationCallbackHandler](#using-jaasplaintextpasswordvalidationcallbackhandler)

# 使用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 (opens new window)对用户进行身份验证。描述 Spring 安全性超出了本文的范围,但它是一个成熟的安全框架。你可以在Spring Security reference documentation (opens new window)中阅读有关它的更多信息。

AuthenticationManager需要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 身份验证和授权服务 (opens new window)。提供对 JAAS 的完整介绍超出了本文的范围,但是好的教程 (opens new window)是可用的。

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

你可以按以下方式连接JaasPlainTextPasswordValidationCallbackHandler:

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

在这种情况下,回调处理程序使用名为LoginContextMyLoginModule。这个模块应该在jaas.config文件中定义,如前面提到的教程 (opens new window)中所解释的那样。

# 摘要用户名身份验证

在使用密码摘要时,SOAP 消息还包含一个UsernameToken元素,该元素本身包含一个Username元素和一个Password元素。不同之处在于,密码不是以纯文本的形式发送的,而是以摘要的形式发送的。收件人将此摘要与他根据用户的已知密码计算出的摘要进行比较,如果它们是相同的,则对用户进行身份验证。这种方法类似于 HTTP 服务器提供的摘要身份验证。

要要求每个传入的消息都包含带有密码摘要的UsernameToken元素,安全策略文件应该包含一个securityPolicy.xml元素,并将passwordDigestRequired属性设置为true。另外,nonceRequired属性应该设置为true:你可以找到可能的子元素here (opens new window)的引用。下面的清单展示了如何定义RequireUsernameToken元素:

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

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

# 使用SimplePasswordValidationCallbackHandler

SimplePasswordValidationCallbackHandler既可以处理纯文本密码,也可以处理密码摘要。在[usingSimplePasswordValidationCallbackHandler](#security-simple-password-validation-callback-handler)中进行了描述。

# 使用SpringDigestPasswordValidationCallbackHandler

SpringDigestPasswordValidationCallbackHandler需要 Spring 证券UserDetailService才能操作。它使用此服务检索令牌中指定的用户的密码。然后将包含在此 Details 对象中的密码摘要与消息中的摘要进行比较。如果它们相等,则用户已成功地进行了身份验证,并且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 证书的 base64 编码版本。该证书由收件人使用以进行身份验证。存储在消息中的证书也用于对消息进行签名(参见验证签名)。

为了确保所有传入的 SOAP 消息都带有BinarySecurityToken,安全策略文件应该包含RequireSignature元素。这个元素还可以携带其他元素,这些元素在验证签名中被覆盖。你可以找到可能的子元素here (opens new window)的引用。下面的清单展示了如何定义RequireSignature元素:

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

当一条没有证书的消息到达时,XwsSecurityInterceptor将向发送方返回一个 SOAP 错误。如果它存在,它将触发CertificateValidationCallback。 Spring-WS 中的三个处理程序处理此回调以用于身份验证:

  • [KeyStoreCallbackHandler](#using-keystoRecallbackhandler)

  • [SpringCertificateValidationCallbackHandler](#using-springcertificateValidationCallbackHandler)

  • [JaasCertificateValidationCallbackHandler](#using-jaascertificateValidationCallbackHandler)

在大多数情况下,证书身份验证之前应该有证书验证,因为你希望仅针对有效的证书进行身份验证。无效的证书,例如过期日期已经过的证书或不在受信任证书存储区中的证书,应该被忽略。

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

<br/><bean id="wsSecurityInterceptor"<br/> class="org.springframework.ws.soap.security.xwss.XwsSecurityInterceptor"><br/> <property name="policyConfiguration" value="classpath:securityPolicy.xml"/><br/> <property name="callbackHandlers"><br/> <list><br/> <ref bean="keyStoreHandler"/><br/> <ref bean="springSecurityHandler"/><br/> </list><br/> </property><br/></bean><br/>
拦截器首先使用密钥存储库确定消息中的证书是否有效,然后对其进行身份验证。
# 使用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 证券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 reference documentation (opens new window)

# 使用JaasCertificateValidationCallbackHandler

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

你可以按以下方式连接JaasCertificateValidationCallbackHandler:

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

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

# 7.1.3.数字签名

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

# 验证签名

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

为了确保所有传入的 SOAP 消息都带有BinarySecurityToken,安全策略文件应该包含RequireSignature元素。它还可以包含SignatureTarget元素,该元素指定预期要签名的目标消息部分和各种其他子元素。你还可以定义要使用的私钥别名、是否使用对称密钥而不是私钥以及许多其他属性。你可以找到可能的子元素here (opens new window)的引用。下面的清单配置了RequireSignature元素:

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

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

# 使用KeyStoreCallbackHandler

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

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

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

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

要对所有传出的 SOAP 消息进行签名,安全策略文件应该包含一个Sign元素。它还可以包含SignatureTarget元素,该元素指定预期要签名的目标消息部分和各种其他子元素。你还可以定义要使用的私钥别名、是否使用对称密钥而不是私钥以及许多其他属性。你可以找到可能的子元素here (opens new window)的引用。下面的示例包含一个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,指示应该使用共享秘密而不是常规私钥来解密消息。你可以读取对其他元素here (opens new window)的描述。下面的示例使用RequireEncryption元素:

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

如果传入消息未加密,XwsSecurityInterceptor将向发送方返回一个 SOAP ault。如果存在,它将向已注册的处理程序触发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表示应该使用共享秘密而不是常规公钥来加密消息。你可以读取对其他元素here (opens new window)的描述。下面的示例使用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.安全异常处理

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

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

类似地,WsSecurityValidationException异常由handleValidationException方法处理。默认情况下,该方法创建一个 SOAP1.1 客户机或 SOAP1.2Sender Fault,并将其作为响应发送回去。

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

# 7.2.使用Wss4jSecurityInterceptor

Wss4jSecurityInterceptor是基于Apache’s WSS4J (opens new window)EndpointInterceptor(参见[拦截请求-EndpointInterceptor接口](# 服务器-端点-拦截器))。

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>

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

Validation action 说明
UsernameToken 验证用户名令牌
Timestamp 验证时间戳
Encrypt 解密消息
Signature 验证签名
NoSecurity 未执行动作

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

Securement action 说明
UsernameToken 添加用户名令牌
UsernameTokenSignature 添加用户名令牌和签名用户名令牌密钥
Timestamp 添加时间戳
Encrypt 对响应进行加密
Signature 签署回应
NoSecurity 未执行动作

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

# 7.2.2.处理数码证书

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

Crypto实例可以从 wss4j 的CryptoFactory获得,或者更方便地使用 Spring-wsCryptoFactoryBean

# 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 安全性集成。此外,还提供了一个简单的回调处理程序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 安全性UserDetailService来操作,从而验证纯文本和摘要密码。它使用此服务检索令牌中指定的用户的密码(或密码摘要)。然后将这个 Details 对象中包含的密码(或密码摘要)与消息中的摘要进行比较。如果它们相等,则用户已成功地进行了身份验证,并且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>
# 添加用户名令牌

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

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

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

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

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

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

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

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

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

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

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

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

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

# 7.2.4.安全时间戳

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

# 验证时间戳

要验证时间戳,将Timestamp添加到validationActions属性。通过将timestampStrict设置为true,并通过设置timeToLive属性,指定服务器端的实时(默认:300),可以覆盖由 SOAP 消息的发起者指定的时间戳语义。拦截器总是拒绝已经过期的时间戳,无论timeToLive的值是多少。

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

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

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

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

# 7.2.5.数字签名

本节介绍Wss4jSecurityInterceptor中可用的各种签名选项。

# 验证签名

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

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

通过将Signature动作添加到securementActions中,可以对发出的消息进行签名。要使用的私钥的别名和密码分别由securementUsernamesecurementPassword属性指定。securementSignatureCrypto必须指向包含私钥的密钥库:

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

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

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

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

下面的示例展示了如何在 Spring Web 服务 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需要指向包含解密私钥的密钥库。此外,validationCallbackHandler必须注入一个org.springframework.ws.soap.security.wss4j.callback.KeyStoreCallbackHandler,该密码指定了密钥的密码:

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

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

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

Encrypt添加到securementActions中,可以对传出的消息进行加密。通过设置securementEncryptionUser属性,你可以设置用于加密的证书的别名。通过securementEncryptionCrypto属性访问证书所在的密钥存储库。由于加密依赖于公共证书,因此不需要传递密码。下面的示例使用securementEncryptionCrypto属性:

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

你可以通过几种方式定制加密:要使用的密钥标识符类型由securementEncryptionKeyIdentifier属性定义。可能的值是IssuerSerialX509KeyIdentifierDirectReferenceThumbprintSKIKeyIdentifier,和EmbeddedKeyName

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

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

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

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

最后,securementEncryptionParts属性定义了消息的哪些部分是加密的。此属性的值是用分号分隔的元素名称的列表,这些名称标识要加密的元素。一个加密模式说明符和一个命名空间标识(分别位于一对花括号内)可以放在每个元素名称之前。加密模式说明符是{Content}{Element}参见 W3CXML 加密规范中关于元素和内容加密的区别。下面的示例从 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

  • [高山]Steve Loughran&Edmund Smith。重新思考 Java SOAP 堆栈。2005 年 5 月 17 日。2005 年 IEEE Telephone Laboratories,Inc.

  • [effective-Enterprise-java]Ted Neward。Scott Meyers。有效的 EnterpriseJava。Addison-Wesley。2004

  • [Effective-xml]Elliotte Rusty Harold。Scott Meyers。有效 XML。Addison-Wesley。2004