本教程将向您展示如何编写契约优先 Web 服务,即首先开发 XML Schema/WSDL 契约,然后开发 Java 代码的 Web 服务。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 定义这样的契约
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>
<!-- ... -->
</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://127.0.0.1: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 配置文件中,使用组件扫描来配置这些类的方式。我们还指示 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>
在编写端点时,我们还使用了 @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 可以根据一些约定为我们生成一个。以下是我们如何定义生成
<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 的完整发行版中找到。下一步是查看发行版中包含的回显示例应用程序。之后,查看航空公司示例,它稍微复杂一些,因为它使用了 JAXB、WS-Security、Hibernate 和事务性服务层。最后,您可以阅读参考文档的其余部分。