前言
在当前面向服务的架构时代,越来越多的人使用 Web 服务来连接以前未连接的系统。 最初,Web 服务被认为是执行远程过程调用 (RPC) 的另一种方式。 然而,随着时间的推移,人们发现 RPC 和 Web 服务之间存在很大差异。 特别是当与其他平台的互作性很重要时,通常最好发送包含处理请求所需的所有数据的封装 XML 文档。 从概念上讲,与消息队列相比,基于 XML 的 Web 服务比远程解决方案更好。 总的来说,XML 应该被视为数据的平台中立表示,是 SOA 的通用语言。 在开发或使用 Web 服务时,重点应该放在这个 XML 上,而不是 Java。
Spring-WS 专注于创建这些文档驱动的 Web 服务。 Spring-WS 促进了契约优先的 SOAP 服务开发,允许通过使用作 XML 有效负载的多种方法之一来创建灵活的 Web 服务。 Spring-WS 提供了一个强大的消息调度框架、一个与现有应用程序安全解决方案集成的 WS-Security 解决方案,以及一个遵循熟悉的 Spring 模板模式的客户端 API。
一、引言
1. 什么是 Spring Web Services?
1.1. 简介
Spring Web Services (Spring-WS) 是 Spring 社区的产品,专注于创建文档驱动的 Web 服务。 Spring Web Services 旨在促进契约优先的 SOAP 服务开发,允许通过使用作 XML 有效负载的多种方法之一来创建灵活的 Web 服务。 该产品基于 Spring 本身,这意味着您可以使用 Spring 概念(例如依赖注入)作为 Web 服务的组成部分。
人们使用 Spring-WS 的原因有很多,但大多数人在发现在遵循 Web 服务最佳实践时缺乏替代 SOAP 堆栈后被它所吸引。 Spring-WS 使最佳实践成为一种简单的实践。 这包括诸如 WS-I 基本配置文件、契约优先开发以及契约和实现之间的松散耦合等实践。 Spring-WS 的其他主要特性是:
1.1.2. XML API 支持
传入的 XML 消息不仅可以使用标准 JAXP API(如 DOM、SAX 和 StAX)进行处理,还可以使用 JDOM、dom4j、XOM 甚至编组技术进行处理。
1.1.3. 灵活的 XML 封送
Spring-WS 构建在 Spring Framework 中的 Object/XML Mapping 模块之上,该模块支持 JAXB 1 和 2、Castor、XMLBeans、JiBX 和 XStream。
1.1.4. 重用 Spring 专业知识
Spring-WS 将 Spring 应用程序上下文用于所有配置,这应该有助于 Spring 开发人员快速上手。 此外,Spring-WS 的架构类似于 Spring-MVC 的架构。
1.2. 运行时环境
Spring-WS 需要一个标准的 Java 17 运行时环境。 Spring-WS 基于 Spring Framework 6.x 构建。
Spring-WS 由许多模块组成,这些模块将在本节的其余部分进行描述。
-
XML 模块 (
spring-xml
)包含Spring-WS的各种XML支持类。 本模块主要面向 Spring-WS 框架本身,而不是 Web 服务开发人员。 -
核心模块 (
spring-ws-core
)是 Spring Web 服务功能的核心部分。 它提供了中央WebServiceMessage
和SoapMessage
接口、服务器端框架(具有强大的消息调度)、用于实现 Web 服务端点的各种支持类以及客户端WebServiceTemplate
. -
支持模块 (
spring-ws-support
) 包含其他传输(JMS、电子邮件等)。 -
安全模块 (
spring-ws-security
) 提供了一个与核心 Web 服务包集成的 WS-Security 实现。 它允许您对 SOAP 消息进行签名、解密和加密,以及将主体Tokens添加到 SOAP 消息。 此外,它还允许您使用现有的 Spring Security 实现进行身份验证和授权。
下图显示了 Spring-WS 模块之间的依赖关系。 箭头表示依赖关系(即 Spring-WS Core 依赖于 Spring-XML 和 Spring OXM)。

2. 为什么要先签约?
创建 Web 服务时,有两种开发风格:最后契约和契约优先。当您使用最后契约方法时,您从 Java 代码开始,然后从中生成 Web 服务契约(在 WSDL 中 — 请参阅侧边栏)。当使用契约优先时,您从 WSDL 契约开始,并使用 Java 实现契约。
Spring-WS 仅支持契约优先的开发风格,本节解释了原因。
2.1. 对象/XML 阻抗不匹配
与 ORM 领域类似,其中我们存在对象/关系阻抗不匹配,将 Java 对象转换为 XML 也有类似的问题。 乍一看,O/X 映射问题似乎很简单:为每个 Java 对象创建一个 XML 元素,将所有 Java 属性和字段转换为子元素或属性。 然而,事情并不像看起来那么简单,因为分层语言(例如 XML(尤其是 XSD))与 Java 的图模型之间存在根本区别。
本节的大部分内容都受到 [alpine] 和 [effective-enterprise-java] 的启发。 |
2.1.1. XSD 扩展
在 Java 中,更改类行为的唯一方法是对其进行子类化以将新行为添加到该子类。 在 XSD 中,可以通过限制数据类型来扩展数据类型,即约束元素和属性的有效值。 例如,考虑以下示例:
<simpleType name="AirportCode">
<restriction base="string">
<pattern value="[A-Z][A-Z][A-Z]"/>
</restriction>
</simpleType>
此类型通过正则表达式限制 XSD 字符串,仅允许三个大写字母。
如果将这种类型转换为 Java,我们最终会得到一个普通的java.lang.String
.
正则表达式在转换过程中丢失,因为 Java 不允许这些类型的扩展。
2.1.2. 不可移植类型
Web 服务最重要的目标之一是可互作性:支持 Java、.NET、Python 等多个平台。 由于所有这些语言都有不同的类库,因此您必须使用一些通用的跨语言格式在它们之间进行通信。 该格式是 XML,所有这些语言都支持它。
由于这种转换,您必须确保在服务实现中使用可移植类型。
例如,考虑一个返回java.util.TreeMap
:
public Map getFlights() {
// use a tree map, to make sure it's sorted
TreeMap map = new TreeMap();
map.put("KL1117", "Stockholm");
...
return map;
}
毫无疑问,该地图的内容可以转换为某种 XML,但由于没有标准方法可以用 XML 描述地图,因此它将是专有的。
此外,即使它可以转换为 XML,许多平台也没有类似于TreeMap
.
因此,当 .NET 客户端访问您的 Web 服务时,它最终可能会出现System.Collections.Hashtable
,具有不同的语义。
在客户端工作时也会出现此问题。请考虑以下 XSD 代码段,它描述了服务协定:
<element name="GetFlightsRequest">
<complexType>
<all>
<element name="departureDate" type="date"/>
<element name="from" type="string"/>
<element name="to" type="string"/>
</all>
</complexType>
</element>
该合约定义了一个请求,该请求接受date
,这是一个 XSD 数据类型,表示年、月和日。如果我们从 Java 调用此服务,我们可能会使用java.time.LocalDateTime
或java.time.Instant
. 但是,这两个类实际上都描述时间,而不是日期。因此,我们实际上最终发送了代表 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/编码)中使用这些引用的标准方法已被弃用,取而代之的是文档/文字(参见 WS-I 基本配置文件)。
这些只是处理 O/X 映射时的一些问题。 在编写 Web 服务时,尊重这些问题非常重要。 尊重它们的最好方法是完全专注于 XML,同时使用 Java 作为实现语言。 这就是合同优先的意义所在。
2.2. 合约优先与合约最后
除了上一节中提到的对象/XML 映射问题外,还有其他原因更喜欢契约优先的开发风格。
2.2.1. 脆弱性
如前所述,最后一个契约的开发风格会导致您的 Web 服务契约(WSDL 和 XSD)从您的 Java 契约(通常是一个接口)生成。 如果使用此方法,则无法保证合同在一段时间内保持不变。 每次更改 Java 契约并重新部署它时,可能会对 Web 服务契约进行后续更改。
此外,并非所有 SOAP 堆栈都从 Java 契约生成相同的 Web 服务契约。 这意味着将当前 SOAP 堆栈更改为不同的 SOAP 堆栈(无论出于何种原因)也可能更改您的 Web 服务契约。
当 Web 服务契约发生变化时,必须指示契约的用户获取新契约,并可能更改其代码以适应契约中的任何更改。
为了使合同有用,它必须尽可能长时间地保持不变。 如果合同发生变化,您必须联系服务的所有用户并指示他们获取新版本的合同。
2.2.2. 性能
当 Java 对象自动转换为 XML 时,无法确定通过网络发送的内容。 一个对象可能引用另一个对象,该对象引用另一个对象,依此类推。 最后,虚拟机中堆上的一半对象可能会转换为 XML,这会导致响应时间变慢。
使用契约优先时,您可以明确描述将哪些 XML 发送到哪里,从而确保它正是您想要的。
3. 编写契约优先的 Web 服务
本教程向您展示如何编写契约优先的 Web 服务,即如何开发先以 XML Schema 或 WSDL 约定开头,然后是 Java 代码的 Web 服务。 Spring-WS 专注于这种开发风格,本教程应该可以帮助您入门。 请注意,本教程的第一部分几乎不包含任何特定于 Spring-WS 的信息。 它主要是关于 XML、XSD 和 WSDL 的。 第二部分侧重于实现与 Spring-WS 的这个契约。
在进行契约优先的 Web 服务开发时,最重要的是从 XML 的角度进行思考。 这意味着 Java 语言概念不太重要。 它是通过网络发送的 XML,您应该关注这一点。 用于实现 Web 服务的 Java 是一个实现细节。
在本教程中,我们将定义由人力资源部门创建的 Web 服务。 客户可以向此服务发送假期申请表以预订假期。
3.1. 消息
在本节中,我们将重点介绍与 Web 服务之间发送和发送的实际 XML 消息。 我们首先确定这些消息是什么样子。
3.1.1. 假期
在这个场景中,我们必须处理假期请求,因此确定假期在 XML 中的样子是有意义的:
<Holiday xmlns="http://mycompany.com/hr/schemas">
<StartDate>2006-07-03</StartDate>
<EndDate>2006-07-07</EndDate>
</Holiday>
假期由开始日期和结束日期组成。 我们还决定使用标准的 ISO 8601 日期格式作为日期,因为这可以节省很多解析麻烦。 我们还为元素添加了一个命名空间,以确保我们的元素可以在其他 XML 文档中使用。
3.1.2. 员工
场景中还有员工的概念。下面是它在 XML 中的样子:
<Employee xmlns="http://mycompany.com/hr/schemas">
<Number>42</Number>
<FirstName>Arjen</FirstName>
<LastName>Poutsma</LastName>
</Employee>
我们使用了与以前相同的命名空间。如果这个<Employee/>
元素可以在其他场景中使用,使用不同的命名空间可能是有意义的,例如http://example.com/employees/schemas
.
3.1.3. 假期请求
两个holiday
元素和employee
元素可以放在一个<HolidayRequest/>
:
<HolidayRequest xmlns="http://mycompany.com/hr/schemas">
<Holiday>
<StartDate>2006-07-03</StartDate>
<EndDate>2006-07-07</EndDate>
</Holiday>
<Employee>
<Number>42</Number>
<FirstName>Arjen</FirstName>
<LastName>Poutsma</LastName>
</Employee>
</HolidayRequest>
两个元素的顺序无关紧要:<Employee/>
可能是第一个元素。重要的是所有数据都在那里。事实上,数据是唯一重要的东西:我们采用数据驱动的方法。
3.2. 数据合同
现在我们已经看到了一些可以使用的 XML 数据示例,将其形式化为模式是有意义的。此数据协定定义了我们接受的消息格式。有四种不同的方法可以为 XML 定义此类协定:
-
DTD。
DTD 的命名空间支持有限,因此它们不适合 Web 服务。 Relax NG 和 Schematron 比 XML Schema 更容易。 不幸的是,它们并没有得到跨平台的广泛支持。 因此,我们使用 XML Schema。
到目前为止,创建 XSD 的最简单方法是从示例文档中推断它。任何好的 XML 编辑器或 Java IDE 都提供此功能。基本上,这些工具使用一些示例 XML 文档来生成验证所有文档的模式。最终结果当然需要完善,但这是一个很好的起点。
使用前面描述的示例,我们最终会得到以下生成的架构:
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
elementFormDefault="qualified"
targetNamespace="http://mycompany.com/hr/schemas"
xmlns:hr="http://mycompany.com/hr/schemas">
<xs:element name="HolidayRequest">
<xs:complexType>
<xs:sequence>
<xs:element ref="hr:Holiday"/>
<xs:element ref="hr:Employee"/>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="Holiday">
<xs:complexType>
<xs:sequence>
<xs:element ref="hr:StartDate"/>
<xs:element ref="hr:EndDate"/>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="StartDate" type="xs:NMTOKEN"/>
<xs:element name="EndDate" type="xs:NMTOKEN"/>
<xs:element name="Employee">
<xs:complexType>
<xs:sequence>
<xs:element ref="hr:Number"/>
<xs:element ref="hr:FirstName"/>
<xs:element ref="hr:LastName"/>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="Number" type="xs:integer"/>
<xs:element name="FirstName" type="xs:NCName"/>
<xs:element name="LastName" type="xs:NCName"/>
</xs:schema>
这个生成的模式可以改进。首先要注意的是,每个类型都有一个根级元素声明。这意味着 Web 服务应该能够接受所有这些元素作为数据。这是不可取的:我们只想接受一个<HolidayRequest/>
. 通过删除包装元素标签(从而保留类型)并内联结果,我们可以完成此作,如下所示:
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:hr="http://mycompany.com/hr/schemas"
elementFormDefault="qualified"
targetNamespace="http://mycompany.com/hr/schemas">
<xs:element name="HolidayRequest">
<xs:complexType>
<xs:sequence>
<xs:element name="Holiday" type="hr:HolidayType"/>
<xs:element name="Employee" type="hr:EmployeeType"/>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:complexType name="HolidayType">
<xs:sequence>
<xs:element name="StartDate" type="xs:NMTOKEN"/>
<xs:element name="EndDate" type="xs:NMTOKEN"/>
</xs:sequence>
</xs:complexType>
<xs:complexType name="EmployeeType">
<xs:sequence>
<xs:element name="Number" type="xs:integer"/>
<xs:element name="FirstName" type="xs:NCName"/>
<xs:element name="LastName" type="xs:NCName"/>
</xs:sequence>
</xs:complexType>
</xs:schema>
该模式仍然存在一个问题:使用这样的模式,您可以期望以下消息进行验证:
<HolidayRequest xmlns="http://mycompany.com/hr/schemas">
<Holiday>
<StartDate>this is not a date</StartDate>
<EndDate>neither is this</EndDate>
</Holiday>
PlainText Section qName:lineannotation level:4, chunks:[<, !-- ... --, >] attrs:[:]
</HolidayRequest>
显然,我们必须确保开始日期和结束日期确实是日期。XML Schema 有一个出色的内置功能date
类型。我们还将NCName
s 设置为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 文件。 请注意,在 Spring-WS 中,不需要手动编写 WSDL。 基于 XSD 和一些约定,Spring-WS 可以为您创建 WSDL,如标题为 实现端点 的部分中所述。 本节的其余部分将展示如何手动编写 WSDL。 您可能想跳到下一节。
我们从标准前导码开始 WSDL 并导入现有的 XSD。
为了将模式与定义分开,我们为 WSDL 定义使用单独的命名空间:http://mycompany.com/hr/definitions
.
以下列表显示了序言:
<wsdl:definitions xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
xmlns:schema="http://mycompany.com/hr/schemas"
xmlns:tns="http://mycompany.com/hr/definitions"
targetNamespace="http://mycompany.com/hr/definitions">
<wsdl:types>
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<xsd:import namespace="http://mycompany.com/hr/schemas" schemaLocation="hr.xsd"/>
</xsd:schema>
</wsdl:types>
接下来,我们根据编写的模式类型添加消息。
我们只有一条消息,即<HolidayRequest/>
我们将架构放入:
<wsdl:message name="HolidayRequest">
<wsdl:part element="schema:HolidayRequest" name="HolidayRequest"/>
</wsdl:message>
我们将消息作为作添加到端口类型:
<wsdl:portType name="HumanResource">
<wsdl:operation name="Holiday">
<wsdl:input message="tns:HolidayRequest" name="HolidayRequest"/>
</wsdl:operation>
</wsdl:portType>
该消息完成了 WSDL 的抽象部分(可以说是接口),并留下了具体部分。
混凝土部分由一个binding
(告诉客户端如何调用您刚刚定义的作)和一个service
(告诉客户端在何处调用它)。
添加混凝土部分是相当标准的。
为此,请参考您之前定义的抽象部分,确保使用document/literal
对于soap:binding
元素 (rpc/encoded
已弃用),选择一个soapAction
对于作(在本例中,http://mycompany.com/RequestHoliday
,但任何 URI 都有效),并确定location
您希望请求到达的 URL(在本例中,http://mycompany.com/humanresources
):
<wsdl:definitions xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
xmlns:schema="http://mycompany.com/hr/schemas"
xmlns:tns="http://mycompany.com/hr/definitions"
targetNamespace="http://mycompany.com/hr/definitions">
<wsdl:types>
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<xsd:import namespace="http://mycompany.com/hr/schemas" (1)
schemaLocation="hr.xsd"/>
</xsd:schema>
</wsdl:types>
<wsdl:message name="HolidayRequest"> (2)
<wsdl:part element="schema:HolidayRequest" name="HolidayRequest"/> (3)
</wsdl:message>
<wsdl:portType name="HumanResource"> (4)
<wsdl:operation name="Holiday">
<wsdl:input message="tns:HolidayRequest" name="HolidayRequest"/> (2)
</wsdl:operation>
</wsdl:portType>
<wsdl:binding name="HumanResourceBinding" type="tns:HumanResource"> (4)(5)
<soap:binding style="document" (6)
transport="http://schemas.xmlsoap.org/soap/http"/> (7)
<wsdl:operation name="Holiday">
<soap:operation soapAction="http://mycompany.com/RequestHoliday"/> (8)
<wsdl:input name="HolidayRequest">
<soap:body use="literal"/> (6)
</wsdl:input>
</wsdl:operation>
</wsdl:binding>
<wsdl:service name="HumanResourceService">
<wsdl:port binding="tns:HumanResourceBinding" name="HumanResourcePort"> (5)
<soap:address location="http://localhost:8080/holidayService/"/> (9)
</wsdl:port>
</wsdl:service>
</wsdl:definitions>
1 | 我们导入数据协定中定义的架构。 |
2 | 我们将HolidayRequest 消息,用于portType . |
3 | 这HolidayRequest type 在架构中定义。 |
4 | 我们将HumanResource 端口类型,用于binding . |
5 | 我们将HumanResourceBinding binding,用于port . |
6 | 我们使用文档/文字样式。 |
7 | 字面意思http://schemas.xmlsoap.org/soap/http 表示 HTTP 传输。 |
8 | 这soapAction 属性表示SOAPAction HTTP 标头,将随每个请求一起发送。 |
9 | 这http://localhost:8080/holidayService/ address 是可以调用 Web 服务的 URL。 |
前面的列表显示了最终的 WSDL。 我们将在下一节中介绍如何实现生成的模式和 WSDL。
3.4. 创建项目
在本节中,我们使用 Maven 为我们创建初始项目结构。 这样做不是必需的,但大大减少了我们为设置 HolidayService 而必须编写的代码量。
以下命令使用 Spring-WS 原型(即项目模板)为我们创建一个 Maven Web 应用程序项目:
mvn archetype:create -DarchetypeGroupId=org.springframework.ws \ -DarchetypeArtifactId=spring-ws-archetype \ -DarchetypeVersion= \ -DgroupId=com.mycompany.hr \ -DartifactId=holidayService
前面的命令创建一个名为holidayService
.
在此目录中是一个src/main/webapp
目录,其中包含 WAR 文件的根目录。
您可以找到标准 Web 应用程序部署描述符 (WEB-INF/web.xml
)中,它定义了一个 Spring-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,例如EndPoints
和WebServiceMessageReceivers
并用于创建新的 Spring 容器。
此文件的名称派生自随用 servlet 的名称(在本例中'spring-ws'
) 替换为-servlet.xml
附加到它。
因此,如果您定义MessageDispatcherServlet
与名称dynamite
,特定于 Spring-WS 的配置文件的名称将变为WEB-INF/dynamite-servlet.xml
.
创建项目结构后,可以将上一节中的模式和 WSDL 放入WEB-INF/
文件夹。
3.5. 实现端点
在 Spring-WS 中,您实现端点来处理传入的 XML 消息。
端点通常是通过使用@Endpoint
注解。
在此端点类中,您可以创建一个或多个处理传入请求的方法。
方法签名可以非常灵活。
您可以包含与传入 XML 消息相关的几乎任何类型的参数类型,正如我们在本章后面所解释的那样。
3.5.1. 处理 XML 消息
以下列表显示了定义假日终结点的类:
package com.mycompany.hr.ws;
@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 business service 来作,所以我们在构造函数中注入依赖,并用@Autowired .
接下来,我们使用 JDOM2 API 设置 XPath 表达式。
有四种表达式://hr:StartDate 用于提取<StartDate> text 值,//hr:EndDate 用于提取结束日期,两个用于提取员工姓名。 |
3 | 这@PayloadRoot 注释告诉 Spring-WShandleHolidayRequest 方法适用于处理 XML 消息。
此方法可以处理的消息类型由注释值指示。
在这种情况下,它可以处理具有HolidayRequest local 部分和http://mycompany.com/hr/schemas Namespace。
下一节提供了有关将消息映射到终结点的详细信息。 |
4 | 这handleHolidayRequest(..) method 是主要的处理方法,它传递了<HolidayRequest/> 元素。
这@RequestPayload 注释表示holidayRequest 参数应映射到请求消息的有效负载。
我们使用 XPath 表达式从 XML 消息中提取字符串值,并将这些值转换为Date 对象,使用SimpleDateFormat (这parseData 方法)。
使用这些值,我们在业务服务上调用一个方法。
通常,这会导致启动数据库事务并更改数据库中的某些记录。
最后,我们定义一个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")
前面示例中显示的注释基本上意味着每当收到带有命名空间的 XML 消息时http://mycompany.com/hr/schemas
和HolidayRequest
local name,则路由到handleHolidayRequest
方法。
通过使用<sws:annotation-driven>
元素,我们启用了@PayloadRoot
附注。
在一个端点中可能(并且很常见)有多个相关的处理方法,每个方法处理不同的 XML 消息。
还有其他方法可以将端点映射到 XML 消息,这将在下一章中介绍。
3.5.3. 提供服务和存根实现
现在我们有了端点,我们需要HumanResourceService
及其实现供HolidayEndpoint
.
以下列表显示了HumanResourceService
接口:
package com.mycompany.hr.service;
public interface HumanResourceService {
void bookHoliday(Date startDate, Date endDate, String name);
}
出于教程目的,我们使用HumanResourceService
:
package com.mycompany.hr.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 在HolidayEndpoint . |
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。
在这种情况下,id 是holiday ,这意味着 WSDL 可以作为holiday.wsdl 在 servlet 上下文中。
完整网址是http://localhost:8080/holidayService/holiday.wsdl . |
2 | 接下来,我们将 WSDL 端口类型设置为HumanResource . |
3 | 我们设置可以到达服务的位置:/holidayService/ .
我们使用相对 URI,并指示框架将其动态转换为绝对 URI。
因此,如果服务部署到不同的上下文,则不必手动更改 URI。
有关更多信息,请参阅名为“自动 WSDL 公开”的部分。
为了使位置转换正常工作,我们需要将 init 参数添加到spring-ws servlet 放入web.xml (如下一个列表所示)。 |
4 | 我们为 WSDL 定义本身定义目标命名空间。不需要设置此属性。如果未设置,则 WSDL 具有与 XSD 模式相同的命名空间。 |
5 | 这xsd 元素是指我们在数据协定中定义的人力资源模式。我们将模式放在WEB-INF 应用程序的目录。 |
以下列表显示了如何添加 init 参数:
<init-param>
<param-name>transformWsdlLocations</param-name>
<param-value>true</param-value>
</init-param>
您可以使用以下命令创建 WAR 文件mvn install
. 如果将应用程序部署到(到 Tomcat、Jetty 等)并将浏览器指向此位置,则会看到生成的 WSDL。此 WSDL 已准备好供客户端使用,例如 soapUI 或其他 SOAP 框架。
本教程到此结束。 教程代码可以在 Spring-WS 的完整发行版中找到。 如果您想继续,请查看作为发行版一部分的回声示例应用程序。 之后,看看 airline 示例,它稍微复杂一些,因为它使用 JAXB、WS-Security、Hibernate 和事务服务层。 最后,您可以阅读参考文档的其余部分。