前言
在当前面向服务架构(SOA)的时代,越来越多的人使用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。
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-WS 的其他关键特性包括:
1.1.2. XML API 支持
传入的 XML 消息不仅可以使用标准的 JAXP API(如 DOM、SAX 和 StAX)来处理,还可以使用 JDOM、dom4j、XOM,甚至是编组技术来处理。
1.1.3. 灵活的 XML 编组
Spring-WS 基于 Spring 框架中的对象/XML映射模块,该模块支持 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) 提供了一个 WS-Security 实现,该实现与核心 Web 服务包集成。 它允许您对 SOAP 消息进行签名、解密和加密,并添加主体Tokens。 此外,它还允许您使用现有的 Spring Security 实现来进行身份验证和授权。
下图显示了 Spring-WS 模块之间的依赖关系。 箭头表示依赖关系(即 Spring-WS Core 依赖于 Spring-XML 和 Spring 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. 不可移植类型
One of the most important goals of a web service is to be interoperable: to support multiple platforms such as Java, .NET, Python, and others. Because all of these languages have different class libraries, you must use some common, cross-language format to communicate between them. That format is XML, which is supported by all of these languages.
由于这种转换,你必须确保在服务实现中使用可移植的类型。 例如,考虑一个返回 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,而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 作为实现语言。 这就是契约优先(contract-first)的全部意义所在。
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,从而导致响应时间变慢。
当使用契约优先时,您明确地描述了发送什么XML到何处,从而确保它正是您想要的内容。
3. 编写基于契约优先的Web服务
本教程展示了如何编写以契约优先的 Web 服务——即,如何开发从 XML Schema 或 WSDL 契约开始,然后才是 Java 代码的 Web 服务。 Spring-WS 专注于这种开发方式,本教程应该能帮助你入门。 请注意,本教程的第一部分几乎不包含 Spring-WS 的特定信息。 它主要涉及 XML、XSD 和 WSDL。 第二部分则侧重于使用 Spring-WS 实现此契约。
在进行基于契约优先的 Web 服务开发时,最重要的是以 XML 的角度思考问题。 这意味着 Java 语言概念的重要性较低。 通过网络传输的是 XML,因此应该专注于这一点。 Java 用于实现 Web 服务只是一个实现细节。
在本教程中,我们定义了一个由人力资源部门创建的网络服务。 客户端可以将假期申请表单发送到此服务以预订假期。
3.1. 消息
在本节中,我们重点关注发送到 Web 服务以及从 Web 服务接收的实际 XML 消息。 我们首先确定这些消息的外观。
3.1.1. 假期
在该场景中,我们需要处理假期请求,因此确定假期在XML中的样子是有意义的:
<Holiday xmlns="http://mycompany.com/hr/schemas">
<StartDate>2006-07-03</StartDate>
<EndDate>2006-07-07</EndDate>
</Holiday>
假期由开始日期和结束日期组成。 我们还决定使用标准的 ISO 8601 日期格式,因为这样可以省去很多解析的麻烦。 我们还为元素添加了命名空间,以确保我们的元素可以在其他 XML 文档中使用。
3.1.2. 员工
在场景中还有一个员工的概念。 以下是它在 XML 中的样子:
<Employee xmlns="http://mycompany.com/hr/schemas">
<Number>42</Number>
<FirstName>Arjen</FirstName>
<LastName>Poutsma</LastName>
</Employee>
我们使用了与之前相同的命名空间。 如果这个 <Employee/> 元素可以用于其他场景,那么使用不同的命名空间可能会更有意义,比如 http://example.com/employees/schemas。
3.1.3. 假期请求
holiday 元素和 employee 元素都可以放入 <HolidayRequest/> 中:
<HolidayRequest xmlns="http://mycompany.com/hr/schemas">
<Holiday>
<StartDate>2006-07-03</StartDate>
<EndDate>2006-07-07</EndDate>
</Holiday>
<Employee>
<Number>42</Number>
<FirstName>Arjen</FirstName>
<LastName>Poutsma</LastName>
</Employee>
</HolidayRequest>
两个元素的顺序并不重要:<Employee/> 本可以是第一个元素。 重要的是所有数据都在这里。 事实上,数据是唯一重要的内容:我们采取以数据为导向的方法。
3.2. 数据契约
现在我们已经看了一些可以使用的XML数据示例,将其形式化为一个模式是合理的。 该数据契约定义了我们接受的消息格式。 定义XML这种契约的方式有四种:
-
DTDs.
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-WS MessageDispatcherServlet并将所有传入请求映射到此Servlet:
<web-app xmlns="http://java.sun.com/xml/ns/j2ee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee
http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"
version="2.4">
<display-name>MyCompany HR Holiday Service</display-name>
<!-- take special notice of the name of this servlet -->
<servlet>
<servlet-name>spring-ws</servlet-name>
<servlet-class>org.springframework.ws.transport.http.MessageDispatcherServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>spring-ws</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>
</web-app>
除了前面提到的WEB-INF/web.xml文件之外,您还需要另一个特定于Spring-WS的配置文件,名为WEB-INF/spring-ws-servlet.xml。 该文件包含所有特定于Spring-WS的bean,例如EndPoints和WebServiceMessageReceivers,并用于创建一个新的Spring容器。 此文件的名称源自相关servlet的名称(在本例中为'spring-ws'),并在其后附加-servlet.xml。 因此,如果您定义了一个名为dynamite的MessageDispatcherServlet,那么特定于Spring-WS的配置文件名称将变为WEB-INF/dynamite-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 | 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 和事务性服务层。 最后,你可以阅读参考文档的其余部分。