本教程將向您展示如何編寫契約優先的 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 和事務性服務層。最後,您可以閱讀其餘的參考文件。