前言
在当前面向服务架构(SOA)的时代,越来越多的人使用Web服务来连接以前未连接的系统。最初,Web服务被认为只是进行远程过程调用(RPC)的另一种方式。然而,随着时间的推移,人们发现RPC和Web服务之间存在很大的差异。特别是当与其他平台的互操作性很重要时,发送包含处理请求所需的所有数据的封装XML文档通常更为合适。从概念上讲,基于XML的Web服务更像消息队列,而不是远程解决方案。总的来说,XML应被视为数据的平台中立表示形式,即SOA的通用语言。在开发或使用Web服务时,重点应该放在XML上,而不是Java。
Spring Web Services 专注于创建这些以文档驱动的 Web 服务。Spring Web Services 促进了基于契约优先的 SOAP 服务开发,允许通过多种方式之一操作 XML 负载来创建灵活的 Web 服务。Spring-WS 提供了一个强大的消息分发框架,一个与您现有应用程序安全解决方案集成的WS-Security解决方案,以及一个遵循熟悉的 Spring 模板模式的客户端 API。
I. 简介
1. 什么是 Spring Web Services?
1.1. 概述
Spring Web Services(Spring-WS)是Spring社区的产品,专注于创建文档驱动的Web服务。Spring Web Services旨在促进基于契约优先的SOAP服务开发,允许通过多种方式之一操作XML有效负载来创建灵活的Web服务。该产品基于Spring本身,这意味着您可以将Spring概念(例如依赖注入)作为Web服务的组成部分。
人们使用 Spring-WS 的原因有很多,但大多数人在发现其他 SOAP 栈在遵循 Web 服务最佳实践方面有所欠缺后,都会被它吸引。Spring-WS 让最佳实践变得易于实施。这包括诸如 WS-I 基本规范、契约优先开发以及在契约与实现之间保持松散耦合等实践。Spring Web Services 的其他关键特性还包括:
1.1.2. XML API 支持
传入的 XML 消息不仅可以使用标准的 JAXP API(如 DOM、SAX 和 StAX)来处理,还可以使用 JDOM、dom4j、XOM,甚至是编组技术来处理。
1.1.3. 灵活的 XML 编组
Spring Web Services 基于 Spring 框架中的对象/XML映射模块,该模块支持 JAXB 1 和 2、Castor、XMLBeans、JiBX 以及 XStream。
1.1.4. 重用您的Spring专业知识
Spring-WS 使用 Spring 应用程序上下文进行所有配置,这应该有助于 Spring 开发人员快速上手。此外,Spring-WS 的架构与 Spring-MVC 类似。
1.2. 运行时环境
Spring Web Services 需要标准的 Java 8 运行时环境。Spring-WS 基于 Spring Framework 4.0.9 构建,但支持更高版本。
Spring-WS 包含多个模块,这些模块将在本节的其余部分中进行描述。
-
XML模块(
spring-xml.jar)包含对Spring Web Services的各种XML支持类。该模块主要用于Spring-WS框架本身,而不是供Web服务开发人员使用。 -
核心模块 (
spring-ws-core.jar) 是 Spring 的 Web 服务功能的中心部分。它提供了核心的WebServiceMessage和SoapMessage接口、服务器端 框架(具有强大的消息分发功能)、用于实现 Web 服务端点的各种支持类,以及 客户端 的WebServiceTemplate。 -
支持模块(
spring-ws-support.jar)包含额外的传输方式(JMS、电子邮件等)。 -
The 安全 包 (
spring-ws-security.jar) 提供了一个 WS-Security 实现,它与核心 Web 服务包集成。它允许您对 SOAP 消息进行签名、解密和加密,并添加主体Tokens。此外,它还允许您使用现有的 Spring Security 安全实现来进行身份验证和授权。
下图显示了 Spring-WS 模块之间的依赖关系。箭头表示依赖关系(即 Spring-WS 核心依赖于 Spring-XML 和 Spring 3 及更高版本中的 OXM 模块)。
2. 为什么选择契约优先?
当创建Web服务时,有两种开发方式:contract-last(后契约)和contract-first(先契约)。当使用contract-last方法时,您从Java代码开始,然后根据它生成Web服务契约(以WSDL形式——请参见侧边栏)。而使用contract-first时,您从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.util.Date或java.util.Calendar。然而,这些类实际上描述的是时间,而不是日期。因此,我们最终发送的数据实际上代表了2007年4月4日午夜(2007-04-04T00:00:00),这与2007-04-04并不相同。
2.1.3. 环形图
假设我们有以下的类结构:
public class Flight {
private String number;
private List<Passenger> passengers;
// getters and setters omitted
}
public class Passenger {
private String name;
private Flight flight;
// getters and setters omitted
}
这是一个循环图:Flight引用Passenger,而Passenger又引用回Flight。像这样的循环图在 Java 中十分常见。如果我们采用一种简单的方式来将其转换为 XML,最终会得到类似如下的内容:
<flight number="KL1117">
<passengers>
<passenger>
<name>Arjen Poutsma</name>
<flight number="KL1117">
<passengers>
<passenger>
<name>Arjen Poutsma</name>
<flight number="KL1117">
<passengers>
<passenger>
<name>Arjen Poutsma</name>
...
处理这样的结构可能需要很长时间才能完成,因为此循环没有停止条件。
解决此问题的一种方法是使用对已编组对象的引用:
<flight number="KL1117">
<passengers>
<passenger>
<name>Arjen Poutsma</name>
<flight href="KL1117" />
</passenger>
...
</passengers>
</flight>
这解决了递归问题,但引入了新的问题。首先,您无法使用 XML 验证器来验证此结构。另一个问题是,在 SOAP 中使用这些引用的标准方式(RPC/encoded)已被弃用,取而代之的是 document/literal(参见 WS-I 基本配置文件)。
这些都是在处理 O/X 映射时的一些问题。在编写 Web 服务时,尊重这些问题非常重要。最好的方式是完全专注于 XML,同时使用 Java 作为实现语言。这正是“契约优先”所关注的内容。
2.2. 合同优先与合同滞后
除了上一节提到的对象/XML映射问题之外,还有其他原因使得我们更倾向于选择契约优先的开发方式。
2.2.1. 脆弱性
正如前面提到的,采用“先实现后合约”的开发风格会导致您的Web服务合约(WSDL和XSD)根据您的Java合约(通常是一个接口)生成。如果您使用这种方法,则无法保证合约会随着时间的推移保持不变。每次更改并重新部署Java合约时,Web服务合约可能都会随之发生更改。
此外,并非所有的 SOAP 协议栈都能从 Java 合同生成相同的 Web 服务合同。这意味着,将当前的 SOAP 协议栈更改为另一个(无论出于何种原因)也可能导致您的 Web 服务合同发生变化。
当 Web 服务合约发生更改时,必须指示该合约的用户获取新的合约,并可能更改其代码以适应合约中的任何更改。
要使契约有用,它必须尽可能长时间保持不变。如果契约发生更改,则需要联系服务的所有用户,并指示他们获取契约的新版本。
2.2.2. 性能
当一个 Java 对象被自动转换为 XML 时,无法确定通过网络发送了什么内容。一个对象可能引用另一个对象,而该对象又引用另一个对象,依此类推。最终,虚拟机堆中可能有一半的对象会被转换成 XML,从而导致响应时间变慢。
当使用契约优先时,您明确地描述了发送什么XML到何处,从而确保它正是您想要的内容。
3. 编写基于契约优先的Web服务
本教程展示了如何编写基于契约优先的Web服务——即如何开发以XML Schema或WSDL契约为起点,而将Java代码作为后续步骤的Web服务。Spring-WS专注于这种开发方式,本教程将帮助您入门。需要注意的是,本教程的第一部分几乎不包含特定于Spring-WS的信息,内容主要涉及XML、XSD和WSDL。教程的第二部分则重点介绍如何使用Spring-WS实现该契约。
The most important thing when doing contract-first web service development is tothink in terms of XML. This means that Java language concepts are of lesser importance. It is the XML that is sent across the wire, and you should focus on that. Java being used to implement the web service is an implementation detail.
在本教程中,我们定义了一个由人力资源部门创建的网络服务。客户端可以将请假申请表单发送到该服务以预订假期。
3.1. 消息
在本节中,我们重点关注发送到 Web 服务以及从 Web 服务接收的实际 XML 消息。我们首先确定这些消息的外观。
3.1.1. 假期
在该场景中,我们需要处理假期请求,因此确定假期在XML中的样子是有意义的:
<Holiday xmlns="http://mycompany.com/hr/schemas">
<StartDate>2006-07-03</StartDate>
<EndDate>2006-07-07</EndDate>
</Holiday>
假期由开始日期和结束日期组成。我们还决定对日期使用标准的ISO 8601日期格式,因为这样可以省去很多解析的麻烦。我们还为元素添加了命名空间,以确保我们的元素可以在其他XML文档中使用。
3.1.2. 员工
在场景中还存在员工的概念。以下是其在 XML 中的形式:
<Employee xmlns="http://mycompany.com/hr/schemas">
<Number>42</Number>
<FirstName>Arjen</FirstName>
<LastName>Poutsma</LastName>
</Employee>
我们使用了与之前相同的命名空间。如果这个 <Employee/> 元素可以用于其他场景,那么使用一个不同的命名空间可能会更有意义,例如 http://example.com/employees/schemas。
3.1.3. 假期请求
holiday 元素和 employee 元素都可以放入 <HolidayRequest/> 中:
<HolidayRequest xmlns="http://mycompany.com/hr/schemas">
<Holiday>
<StartDate>2006-07-03</StartDate>
<EndDate>2006-07-07</EndDate>
</Holiday>
<Employee>
<Number>42</Number>
<FirstName>Arjen</FirstName>
<LastName>Poutsma</LastName>
</Employee>
</HolidayRequest>
两个元素的顺序并不重要:<Employee/> 也可以是第一个元素。重要的是所有的数据都在这里。事实上,数据是唯一重要的内容:我们采取以数据为导向的方法。
3.2. 数据契约
现在我们已经看了一些可以使用的 XML 数据示例,将其正式化为一个模式是合理的。该数据契约定义了我们接受的消息格式。定义这种 XML 契约的方式有四种:
DTD 的命名空间支持有限,因此不适合用于 Web 服务。Relax NG 和 Schematron 比 XML Schema 更简单。但遗憾的是,它们在跨平台方面的支持并不广泛。因此,我们使用 XML Schema。
到目前为止,创建 XSD 的最简单方法是从示例文档中推断出它。任何优秀的 XML 编辑器或 Java IDE 都提供此功能。基本上,这些工具使用一些示例 XML 文档来生成一个能够验证它们所有内容的模式。最终结果肯定需要进行一些优化,但这是一个很好的起点。
使用前面描述的示例,我们最终得到以下生成的模式:
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
elementFormDefault="qualified"
targetNamespace="http://mycompany.com/hr/schemas"
xmlns:hr="http://mycompany.com/hr/schemas">
<xs:element name="HolidayRequest">
<xs:complexType>
<xs:sequence>
<xs:element ref="hr:Holiday"/>
<xs:element ref="hr:Employee"/>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="Holiday">
<xs:complexType>
<xs:sequence>
<xs:element ref="hr:StartDate"/>
<xs:element ref="hr:EndDate"/>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="StartDate" type="xs:NMTOKEN"/>
<xs:element name="EndDate" type="xs:NMTOKEN"/>
<xs:element name="Employee">
<xs:complexType>
<xs:sequence>
<xs:element ref="hr:Number"/>
<xs:element ref="hr:FirstName"/>
<xs:element ref="hr:LastName"/>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="Number" type="xs:integer"/>
<xs:element name="FirstName" type="xs:NCName"/>
<xs:element name="LastName" type="xs:NCName"/>
</xs:schema>
生成的模式可以进一步改进。首先要注意的是,每个类型都有一个顶级元素声明。这意味着 Web 服务应该能够接受所有这些元素作为数据。但这并不是我们期望的:我们只想接受 <HolidayRequest/>。通过移除包裹的元素标签(同时保留类型)并将结果内联,我们可以实现这一目标,具体如下:
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:hr="http://mycompany.com/hr/schemas"
elementFormDefault="qualified"
targetNamespace="http://mycompany.com/hr/schemas">
<xs:element name="HolidayRequest">
<xs:complexType>
<xs:sequence>
<xs:element name="Holiday" type="hr:HolidayType"/>
<xs:element name="Employee" type="hr:EmployeeType"/>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:complexType name="HolidayType">
<xs:sequence>
<xs:element name="StartDate" type="xs:NMTOKEN"/>
<xs:element name="EndDate" type="xs:NMTOKEN"/>
</xs:sequence>
</xs:complexType>
<xs:complexType name="EmployeeType">
<xs:sequence>
<xs:element name="Number" type="xs:integer"/>
<xs:element name="FirstName" type="xs:NCName"/>
<xs:element name="LastName" type="xs:NCName"/>
</xs:sequence>
</xs:complexType>
</xs:schema>
该模式仍然存在一个问题:使用这样的模式,您可以预期以下消息能够通过验证:
<HolidayRequest xmlns="http://mycompany.com/hr/schemas">
<Holiday>
<StartDate>this is not a date</StartDate>
<EndDate>neither is this</EndDate>
</Holiday>
PlainText Section qName:lineannotation level:4, chunks:[<, !-- ... --, >] attrs:[:]
</HolidayRequest>
显然,我们必须确保开始日期和结束日期确实是日期。XML Schema 提供了一个非常优秀的内置 date 类型供我们使用。我们还将 NCName 改为 string 实例。最后,我们将 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(它告诉客户端在何处调用它)。
添加一个具体的部件是非常标准的操作。为此,请参考你之前定义的抽象部分,确保为soap:binding元素使用document/literal(rpc/encoded已被弃用),选择一个soapAction用于操作(在本例中为http://mycompany.com/RequestHoliday,但任何URI都可以工作),并确定你希望请求到达的location URL(在本例中为http://mycompany.com/humanresources):
<wsdl:definitions xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
xmlns:schema="http://mycompany.com/hr/schemas"
xmlns:tns="http://mycompany.com/hr/definitions"
targetNamespace="http://mycompany.com/hr/definitions">
<wsdl:types>
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<xsd:import namespace="http://mycompany.com/hr/schemas" (1)
schemaLocation="hr.xsd"/>
</xsd:schema>
</wsdl:types>
<wsdl:message name="HolidayRequest"> (2)
<wsdl:part element="schema:HolidayRequest" name="HolidayRequest"/> (3)
</wsdl:message>
<wsdl:portType name="HumanResource"> (4)
<wsdl:operation name="Holiday">
<wsdl:input message="tns:HolidayRequest" name="HolidayRequest"/> (2)
</wsdl:operation>
</wsdl:portType>
<wsdl:binding name="HumanResourceBinding" type="tns:HumanResource"> (4)(5)
<soap:binding style="document" (6)
transport="http://schemas.xmlsoap.org/soap/http"/> (7)
<wsdl:operation name="Holiday">
<soap:operation soapAction="http://mycompany.com/RequestHoliday"/> (8)
<wsdl:input name="HolidayRequest">
<soap:body use="literal"/> (6)
</wsdl:input>
</wsdl:operation>
</wsdl:binding>
<wsdl:service name="HumanResourceService">
<wsdl:port binding="tns:HumanResourceBinding" name="HumanResourcePort"> (5)
<soap:address location="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 传输。 |
| 8 | soapAction 属性表示将随每个请求发送的 SOAPAction HTTP 标头。 |
| 9 | http://localhost:8080/holidayService/ 地址是可调用 Web 服务的 URL。 |
前面的清单显示了最终的 WSDL。我们将在下一节中描述如何实现生成的模式和 WSDL。
3.4. 创建项目
在本节中,我们使用Maven为我们创建初始的项目结构。虽然这不是必需的,但这样做可以大大减少我们为设置 HolidayService 所需编写的代码量。
以下命令通过使用 Spring-WS 原型(即项目模板)为我们创建一个 Maven Web 应用程序项目:
mvn archetype:create -DarchetypeGroupId=org.springframework.ws \ -DarchetypeArtifactId=spring-ws-archetype \ -DarchetypeVersion= \ -DgroupId=com.mycompany.hr \ -DartifactId=holidayService
前述命令创建了一个名为holidayService的新目录。在此目录中有一个src/main/webapp目录,它包含了WAR文件的根目录。您可以在这里找到标准的Web应用程序部署描述符('WEB-INF/web.xml'),它定义了一个Spring-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。因此,如果您定义了一个名为'dynamite'的MessageDispatcherServlet,那么特定于Spring-WS的配置文件的名称将变为WEB-INF/dynamite-servlet.xml。
(您可以在[tutorial.example.sws-conf-file]中查看此示例的WEB-INF/spring-ws-servlet.xml文件的内容。)
一旦你创建了项目结构,你可以将上一节中的 schema 和 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 业务服务才能运行,因此我们通过构造函数注入依赖,并使用 @Autowired 进行注解。 接下来,我们使用 JDOM2 API 设置 XPath 表达式。共有四个表达式://hr:StartDate 用于提取 <StartDate> 的文本值,//hr:EndDate 用于提取结束日期,另外两个用于提取员工的姓名。 |
| 3 | @PayloadRoot 注解告诉 Spring-WS handleHolidayRequest 方法适合用于处理 XML 消息。该方法能够处理的消息类型由注解值指示。在这种情况下,它可以处理具有 HolidayRequest 本地部分和 http://mycompany.com/hr/schemas 命名空间的 XML 元素。 有关将消息映射到端点的更多信息,请参阅下一节。 |
| 4 | handleHolidayRequest(..) 方法是主要的处理方法,它从传入的 XML 消息中获取传递的 <HolidayRequest/> 元素。@RequestPayload 注解表示 holidayRequest 参数应映射到请求消息的有效负载。我们使用 XPath 表达式从 XML 消息中提取字符串值,并通过 SimpleDateFormat(parseData 方法)将这些值转换为 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 配置文件中配置这些类。我们还通过 <sws:annotation-driven> 元素指示 Spring-WS 使用注解驱动的端点。
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:sws="http://www.springframework.org/schema/web-services"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/web-services http://www.springframework.org/schema/web-services/web-services-2.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd">
<context:component-scan base-package="com.mycompany.hr"/>
<sws:annotation-driven/>
</beans>
3.5.2. 将消息路由到端点
在编写端点的过程中,我们还使用了@PayloadRoot注解来指明哪些类型的消息可以被handleHolidayRequest方法处理。在Spring-WS中,这个过程是由EndpointMapping负责的。在这里,我们通过使用PayloadRootAnnotationMethodEndpointMapping根据消息的内容来进行路由。以下代码清单展示了我们之前使用的注解:
@PayloadRoot(namespace = "http://mycompany.com/hr/schemas", localPart = "HolidayRequest")
前一个示例中显示的注解基本上意味着,每当接收到带有命名空间http://mycompany.com/hr/schemas和局部名称HolidayRequest的XML消息时,它将被路由到handleHolidayRequest方法。通过在配置中使用<sws:annotation-driven>元素,我们启用了对@PayloadRoot注解的检测。在端点中拥有多个相关的处理方法是很常见的情况,每个方法处理不同的XML消息。
还有其他将端点映射到XML消息的方式,这将在下一章中描述。
3.5.3. 提供服务和存根实现
现在我们已经有了端点,我们需要HumanResourceService及其供HolidayEndpoint使用的实现。以下清单显示了HumanResourceService接口:
package com.mycompany.hr.service;
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。这将该类标记为业务外观,使其成为 HolidayEndpoint 中 @Autowired 注入的候选者。 |
3.6. 发布 WSDL
最后,我们需要发布 WSDL。正如服务契约中所述,我们不需要自己编写 WSDL。Spring-WS 可以根据一些约定生成一个。以下是生成方式的定义:
<sws:dynamic-wsdl id="holiday" (1)
portTypeName="HumanResource" (3)
locationUri="/holidayService/" (4)
targetNamespace="http://mycompany.com/hr/definitions"> (5)
<sws:xsd location="/WEB-INF/hr.xsd"/> (2)
</sws:dynamic-wsdl>
| 1 | The id 决定了可以检索 WSDL 的 URL。在此情况下,id 是 holiday,这意味着 WSDL 可以作为 holiday.wsdl 在 servlet 上下文中检索。完整的 URL 是 http://localhost:8080/holidayService/holiday.wsdl。 |
| 2 | 接下来,我们将 WSDL 端口类型设置为 HumanResource。 |
| 3 | 我们设置了服务可以访问的位置:/holidayService/。我们使用相对URI,并指示框架将其动态转换为绝对URI。因此,如果服务被部署到不同的上下文中,我们不需要手动更改URI。欲了解更多信息,请参见“自动WSDL暴露”部分。为了使位置转换生效,我们需要在web.xml中为spring-ws servlet添加一个初始化参数(如下一个列表所示)。 |
| 4 | 我们为WSDL定义本身定义目标命名空间。设置此属性不是必需的。如果未设置,WSDL将与XSD模式具有相同的命名空间。 |
| 5 | The xsd 元素指的是我们在 数据契约 中定义的人力资源模式。我们将该模式放置在应用程序的 WEB-INF 目录中。 |
以下清单显示了如何添加初始化参数:
<init-param>
<param-name>transformWsdlLocations</param-name>
<param-value>true</param-value>
</init-param>
你可以通过使用mvn install来创建一个WAR文件。如果你将应用程序部署到(Tomcat、Jetty等)并将浏览器指向这个位置,你将会看到生成的WSDL。此WSDL已准备好供客户端使用,例如soapUI或其他SOAP框架。
到此,本教程结束。教程的代码可以在 Spring-WS 的完整发行版中找到。如果你想继续学习,可以查看发行版中包含的 echo 示例应用程序。接下来,可以查看航空公司示例,该示例稍微复杂一些,因为它使用了 JAXB、WS-Security、Hibernate 和事务性服务层。最后,你可以阅读参考文档的其余部分。