本教程將向您展示如何編寫契約優先的 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://:8080/holidayService/"/>
</wsdl:port> </wsdl:service> </wsdl:definitions>
我們匯入第 3.3 節,“資料契約”中定義的模式。 | |
我們定義 | |
| |
我們定義 | |
我們定義 | |
我們使用 document/literal 風格。 | |
字面值 | |
| |
|
這是最終的 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 中,您將實現端點(Endpoints)來處理傳入的 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 可以基於一些約定為我們生成一個。以下是我們如何定義生成過程
<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 和事務服務層。最後,您可以閱讀其餘的參考文件。