本教程将向您展示如何编写契约优先的 Web 服务,即先编写 XML Schema/WSDL 契约,再编写 Java 代码。Spring-WS 专注于这种开发风格,本教程将帮助您入门。请注意,本教程的第一部分几乎不包含 Spring-WS 特定的信息:它主要关于 XML、XSD 和 WSDL。第二部分则侧重于使用 Spring-WS 实现此契约。
在进行契约优先的 Web 服务开发时,最重要的是尝试用 XML 的方式思考。这意味着 Java 语言概念的重要性相对较低。通过网络传输的是 XML,您应该专注于此。Java 用于实现 Web 服务是一个实现细节。一个重要的细节,但仍然是细节。
在本教程中,我们将定义一个由人力资源部门创建的 Web 服务。客户端可以向此服务发送假期申请表来预订假期。
在本节中,我们将重点关注发送到 Web 服务和从 Web 服务发送的实际 XML 消息。我们将从确定这些消息的外观开始。
在场景中,我们需要处理假期申请,因此确定假期在 XML 中是什么样子是有意义的
<Holiday xmlns="http://mycompany.com/hr/schemas">
<StartDate>2006-07-03</StartDate>
<EndDate>2006-07-07</EndDate>
</Holiday>
假期由开始日期和结束日期组成。我们还决定使用标准 ISO 8601 日期格式,因为这样可以省去大量的解析麻烦。我们还在元素中添加了一个命名空间,以确保我们的元素可以在其他 XML 文档中使用。
场景中还有一个员工的概念。下面是它在 XML 中的样子
<Employee xmlns="http://mycompany.com/hr/schemas">
<Number>42</Number>
<FirstName>Arjen</FirstName>
<LastName>Poutsma</LastName>
</Employee>
我们使用了与之前相同的命名空间。如果此 <Employee/> 元素可以在其他场景中使用,那么使用不同的命名空间(例如 http://mycompany.com/employees/schemas)可能更有意义。
假期和员工元素都可以放入 <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/> 也可以是第一个元素。重要的是所有数据都在那里。事实上,数据是唯一重要的事情:我们正在采用数据驱动的方法。
既然我们已经看到了一些我们将要使用的 XML 数据示例,那么将其形式化为模式是有意义的。此数据契约定义了我们接受的消息格式。有四种不同的方式来定义 XML 的此类契约
DTDs 对命名空间的支持有限,因此不适用于 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>
<!-- ... -->
</HolidayRequest>
显然,我们必须确保开始日期和结束日期确实是日期。XML Schema 有一个出色的内置 date 类型,我们可以使用。我们还将 NCName 更改为 string。最后,我们将 <HolidayRequest/> 中的 sequence 更改为 all。这告诉 XML 解析器 <Holiday/> 和 <Employee/> 的顺序无关紧要。我们最终的 XSD 现在看起来像这样
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:hr="http://mycompany.com/hr/schemas"
elementFormDefault="qualified"
targetNamespace="http://mycompany.com/hr/schemas">
<xs:element name="HolidayRequest">
<xs:complexType>
<xs:all>
<xs:element name="Holiday" type="hr:HolidayType"/>
<xs:element name="Employee" type="hr:EmployeeType"/>
</xs:all>
</xs:complexType>
</xs:element>
<xs:complexType name="HolidayType">
<xs:sequence>
<xs:element name="StartDate" type="xs:date"/>
<xs:element name="EndDate" type="xs:date"/>
</xs:sequence>
</xs:complexType>
<xs:complexType name="EmployeeType">
<xs:sequence>
<xs:element name="Number" type="xs:integer"/>
<xs:element name="FirstName" type="xs:string"/>
<xs:element name="LastName" type="xs:string"/>
</xs:sequence>
</xs:complexType>
</xs:schema>
|
|
|
我们使用 |
|
|
我们将此文件保存为 hr.xsd。
服务契约通常表示为 WSDL 文件。请注意,在 Spring-WS 中,不需要手动编写 WSDL。根据 XSD 和一些约定,Spring-WS 可以为您创建 WSDL,如题为第 3.6 节,“实现端点”的章节中所述。如果您愿意,可以跳到下一节;本节的其余部分将向您展示如何手动编写自己的 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"
schemaLocation="hr.xsd"/>
</xsd:schema>
</wsdl:types>
<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 name="HumanResourceBinding" type="tns:HumanResource"> 
<soap:binding style="document"
transport="http://schemas.xmlsoap.org/soap/http"/>
<wsdl:operation name="Holiday">
<soap:operation soapAction="http://mycompany.com/RequestHoliday"/>
<wsdl:input name="HolidayRequest">
<soap:body use="literal"/>
</wsdl:input>
</wsdl:operation>
</wsdl:binding>
<wsdl:service name="HumanResourceService">
<wsdl:port binding="tns:HumanResourceBinding" name="HumanResourcePort">
<soap:address location="https://:8080/holidayService/"/>
</wsdl:port>
</wsdl:service>
</wsdl:definitions>
|
我们导入在第 3.3 节,“数据契约”中定义的模式。 |
|
我们定义了 |
|
|
|
我们定义了 |
|
我们定义了 |
|
我们使用文档/文字样式。 |
|
文字 |
|
|
|
|
这是最终的 WSDL。我们将在下一节中描述如何实现生成的模式和 WSDL。
在本节中,我们将使用 Maven3 为我们创建初始项目结构。这样做不是必需的,但大大减少了我们设置 HolidayService 所需编写的代码量。
以下命令使用 Spring-WS 原型(即项目模板)为我们创建一个 Maven3 Web 应用程序项目
mvn archetype:create -DarchetypeGroupId=org.springframework.ws \ -DarchetypeArtifactId=spring-ws-archetype \ -DarchetypeVersion=2.1.4.RELEASE \ -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 especial 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'。
(您可以在???中查看此示例的 'WEB-INF/spring-ws-servlet.xml' 文件的内容。)
创建项目结构后,您可以将上一节中的模式和 wsdl 放入 'WEB-INF/' 文件夹中。
在 Spring-WS 中,您将实现端点来处理传入的 XML 消息。端点通常通过使用 @Endpoint 注解类来创建。在此端点类中,您将创建一个或多个处理传入请求的方法。方法签名可以非常灵活:您可以包含几乎任何与传入 XML 消息相关的参数类型,具体将在后面解释。
在此示例应用程序中,我们将使用 JDom 来处理 XML 消息。我们还使用 XPath,因为它允许我们选择 XML JDOM 树的特定部分,而不需要严格的模式一致性。
package com.mycompany.hr.ws; import java.text.SimpleDateFormat; 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.jdom.Element; import org.jdom.JDOMException; import org.jdom.Namespace; import org.jdom.xpath.XPath; @Endpointpublic class HolidayEndpoint { private static final String NAMESPACE_URI = "http://mycompany.com/hr/schemas"; private XPath startDateExpression; private XPath endDateExpression; private XPath nameExpression; private HumanResourceService humanResourceService; @Autowired public HolidayEndpoint(HumanResourceService humanResourceService)
throws JDOMException { this.humanResourceService = humanResourceService; Namespace namespace = Namespace.getNamespace("hr", NAMESPACE_URI); startDateExpression = XPath.newInstance("//hr:StartDate"); startDateExpression.addNamespace(namespace); endDateExpression = XPath.newInstance("//hr:EndDate"); endDateExpression.addNamespace(namespace); nameExpression = XPath.newInstance("concat(//hr:FirstName,' ',//hr:LastName)"); nameExpression.addNamespace(namespace); } @PayloadRoot(namespace = NAMESPACE_URI, localPart = "HolidayRequest")
public void handleHolidayRequest(@RequestPayload Element holidayRequest)
throws Exception { SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd"); Date startDate = dateFormat.parse(startDateExpression.valueOf(holidayRequest)); Date endDate = dateFormat.parse(endDateExpression.valueOf(holidayRequest)); String name = nameExpression.valueOf(holidayRequest); humanResourceService.bookHoliday(startDate, endDate, name); } }
|
|
|
|
|
|
|
|
使用 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>2.1.4.RELEASE</version>
</dependency>
<dependency>
<groupId>jdom</groupId>
<artifactId>jdom</artifactId>
<version>1.0</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>
在编写端点时,我们还使用了 @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 消息的方法,将在下一章中描述。
现在我们有了端点,我们需要 HumanResourceService 及其实现供 HolidayEndpoint 使用。
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
public class StubHumanResourceService implements HumanResourceService {
public void bookHoliday(Date startDate, Date endDate, String name) {
System.out.println("Booking holiday for [" + startDate + "-" + endDate + "] for [" + name + "] ");
}
}
|
|
最后,我们需要发布 WSDL。如第 3.4 节,“服务契约”所述,我们不需要自己编写 WSDL;Spring-WS 可以根据一些约定为我们生成 WSDL。以下是我们定义生成的方式
<sws:dynamic-wsdl id="holiday"portTypeName="HumanResource"
locationUri="/holidayService/"
targetNamespace="http://mycompany.com/hr/definitions">
<sws:xsd location="/WEB-INF/hr.xsd"/>
</sws:dynamic-wsdl>
|
id 决定了可以检索 WSDL 的 URL。在这种情况下,id 是 |
|
接下来,我们将 WSDL 端口类型设置为 |
|
我们设置了服务可访问的位置: 为了使位置转换生效,我们需要在 <init-param> <param-name>transformWsdlLocations</param-name> <param-value>true</param-value> </init-param>
|
|
我们定义 WSDL 定义本身的目标命名空间。设置此属性不是必需的。如果未设置,WSDL 将与 XSD 模式具有相同的命名空间。 |
|
|
您可以使用 mvn install 创建一个 WAR 文件。如果您将应用程序部署(到 Tomcat、Jetty 等),并将浏览器指向此位置,您将看到生成的 WSDL。此 WSDL 已准备好供客户端使用,例如 soapUI 或其他 SOAP 框架。
本教程到此结束。本教程代码可在 Spring-WS 的完整发行版中找到。下一步是查看发行版中包含的 echo 示例应用程序。之后,查看 airline 示例,它稍微复杂一些,因为它使用 JAXB、WS-Security、Hibernate 和事务性服务层。最后,您可以阅读其余的参考文档。