本月,我將向您呈現如何讓您的 Grails 應用程序成為原始數據 — 具體指 XML — 的源,從而讓其 他的 Web 應用程序也能夠使用它。我通常把這種情況表述為:為您的 Grails 應用程序建立 Web 服務, 但最近這個說法被賦予了新的含義。很多人把 Web 服務與 SOAP 及成熟的面向服務架構(service- oriented architecture,SOA)聯系到一起。如果選擇這種方法的話,Grails 擁有兩個插件可以用來將 SOAP 接口公開給應用程序。但我將向您呈現的內容並非處理某一個諸如 SOAP 這樣的具體實現,而是如 何使用一個基於具象狀態傳輸(Representational State Transfer,REST)的接口來返回普通舊式 XML (Plain Old XML,POX)。
說到 RESTful Web 服務,理解緣由 與理解方法 同樣重要。Roy Fielding 的博士論文— REST 這個 縮略詞的發源處 — 概括了實現 Web 服務的兩大方法:一個是面向服務,另一個是面向資源。在向您呈 現實現自己的 RESTful 面向資源架構(resource-oriented architecture,ROA)的代碼前,我將先澄清 這兩個設計原理之間的差異,並論述普遍使用的 REST 的兩種最有爭議的定義。學習了本文第一部分的所 有內容之後,稍後您就可以學習到很多的 Grails 代碼。
REST 簡介
當開發人員說要提供 RESTful Web 服務時,他們通常是指想要提供一個簡單的、無爭議的方法來從他 們的應用程序中獲取 XML。RESTful Web 服務通常提供一個可以響應 HTTP GET 請求而返回 XML 的 URL (稍後我將給出 REST 的更正式的定義,它對這個定義進行了改良,雖然改動不大,但仍然很重要)。
Yahoo! 提供了大量的 RESTful Web 服務,它們響應簡單的 HTTP GET 請求,而返回 POX。例如,在 Web 浏覽器的位置字段鍵入 http://api.search.yahoo.com/WebSearchService/V1/webSearch? appid=YahooDemo&query=beatles。您將獲得使用 XML 的 Web 搜索結果,它和在 Yahoo! 主頁的搜 索框裡鍵入 beatles 而獲得的使用 HTML 的搜尋結果是一樣的。
如果假設 Yahoo! 支持 SOAP 接口的話(實際上並不支持),那麼發出一個 SOAP 請求將會返回相同 的數據,但對於開發人員來說,發出請求可能更費勁一些。在查詢字符串裡,請求方將需要呈交的不是簡 單的一組名稱/值對,而是一份定義明確的、帶有一個 SOAP 報頭和正文部分的 XML 文檔 — 而且要用一 個 HTTP POST 而非 GET 來提交請求。所有這些額外的工作完成後,響應會以一個正式 XML 文檔的形式 返回,它與請求一樣,也有一個 SOAP 報頭和正文部分,但要獲得查詢結果,需要去掉這些內容。Web 服 務常常作為復雜 SOAP 的一種簡單替代品而被采用。
有幾種趨勢可以表明 Web 服務的 RESTful 方法越來越普及了。Amazon.com 既提供了 RESTful 服務 又提供了基於 SOAP 的服務。現實的使用模式表明十個用戶中幾乎有九個都偏愛 RESTful 接口。另外還 有一個值得注意的情況,Google 於 2006 年 12 月正式宣布反對基於 SOAP 的 Web 服務。它的所有數據 服務(歸類為 Google Data API)都包含了一個更加具有 REST 風格的方法。
面向服務的 Web 服務
如果把 REST 和 SOAP 之間的差異歸結為 GET 和 POST 之間的優劣,那就很容易區分了。所使用的 HTTP 方法是很重要的,但重要的原因與您最初預想的不同。要充分了解 REST 和 SOAP 之間的差異,您 需要先掌握這兩個策略的更深層語義。SOAP 包含了一個 Web 服務的面向對象的方法 — 其中包含的方法 (或動詞)是您與服務相交互的主要方式。REST 采取面向資源的方法,方法中的對象(或名詞)是最重 要的部分。
在一個 SOA 中,一個服務調用看起來就像是一個遠程過程調用(remote procedure call,RPC)。設 想,如果您有一個帶有 getForecast(String zipcode) 方法的 Java Weather 類的話,就可以輕易地將 這個方法公開為一個 Web 服務了。實際上,Yahoo! 就有這樣一個 Web 服務。在浏覽器中輸入 http://weather.yahooapis.com/forecastrss?p=94089,這樣就會用你自己的 ZIP 代碼來替代 p 參數了 。Yahoo! 服務還支持第二參數 — u —,該參數既接受華氏溫度(Fahrenheit)符號 f,又接受攝氏溫 度(Celsius)符號 c。不難想象,在假想的類上重載方法簽名就可以接受第二參數:getForecast ("94089", "f")。
回過來再看一下我剛才做的 Yahoo! 搜索查詢,同樣,不難想象出,可以將它重寫為一個方法調用。 http://api.search.yahoo.com/WebSearchService /V1/webSearch?appid=YahooDemo&query=beatles 輕松轉換成了 WebSearchService.webSearch("YahooDemo", "beatles")。
所以如果 Yahoo! 調用實際上為 RPC 調用的話,那這跟我先前所稱的 Yahoo! 服務是 RESTful 的豈 不是互相矛盾的麼?很不幸,就是矛盾的。但犯這種錯誤的不只我一個。Yahoo! 也稱這些服務是 RESTful 的,但它也坦言:從最嚴格的意義上講這些服務並不符合 RESTful 服務的定義。在 Yahoo! Web Services FAQ 中尋找 “什麼是 REST?”,答案是:“REST 代表 Representational State Transfer。 大多數的 Yahoo! Web Services 都使用 ‘類 REST’ 的 RPC 樣式的操作,而非 HTTP GET 或 POST…… ”
這個問題在 REST 社區內一直引發著爭論。問題是沒有准確的定義可以簡單明了地描述這種 “較之 POST 更偏好 HTTP GET 的、較之 XML 請求更偏好簡單的 URL 請求的、基於 RPC 的 Web 服務” 。有些 人稱之為 HTTP/POX 或者 REST/RPC 服務。其他人則對應 High REST Web 服務 — 一種與 Fielding 的 面向資源架構的定義更接近的服務 — 而稱之為 Low REST Web 服務。
我將類似 Yahoo! 的服務稱為 GETful 服務。這並不表示我看輕它 — 正相反,我認為 Yahoo! 在整 理不太正式的(low-ceremony)Web 服務的集合方面做的相當好。這個詞恰到好處地概括出了 Yahoo! 的 RPC 樣式的服務的益處 — 通過發出一個簡單的 HTTP GET 請求來獲得 XML 結果 —,而且沒有濫用 Fielding 所作的原始定義。
面向資源的 Web 服務
POST 與 PUT
在 REST 社區存在著有關 POST 和 PUT 在插入新資源方面所起的作用的爭議。在 HTTP 1.1 的原始的 RFC(Fielding 是主要作者)中對 PUT 的定義稱:如果不存在資源的話,服務器可以創建資源。而如果 已經存在資源的話,那麼 “……封裝的實體必須被當作是對駐留在初始服務器上的實體修改後的版本” 。因此如果不存在資源的話,PUT 就等於 INSERT。如果存在資源的話,PUT 就等於 UPDATE。 如果按如 下的方式定義 POST 的話,事情就復雜了:
“POST 旨在用一個統一的方法來涵蓋以下所有功能:
注釋現有資源;
將一則消息發布到告示板、新聞組、郵件列表或者類似文章上;
將諸如表格提交結果這樣的數據塊提供給數據處理進程;
通過追加操作擴展數據庫。”
“注釋現有資源” 似乎暗指 UPDATE,而 “將一則消息發布到告示板”、“擴展數據庫” 似乎暗指 INSERT。
由於所有的浏覽器在提交 HTML 表單數據時都不支持 PUT 方法(它們只支持 GET 和 POST),所以很 難確定在哪種情況下使用哪種方法最為明智。
Atom 發布協議(Atom Publishing Protocol)是一個遵循 RESTful 原則的流行的聚合格式。Atom 的 RFC 作者試圖給 POST 與 PUT 之間的爭議做個了結:
“Atom Publishing Protocol 對如下的 Member Resource 使用 HTTP 方法:
GET 用於檢索已知的 Resource 表示。
POST 用於創建新的、動態命名的 Resource……
PUT 用於編輯已知 Resource。不用它來創建 Resource。
DELETE 用於刪除已知 Resource。”
在本文中,我將以 Atom 為指引,使用 POST 來 INSERT,用 PUT 來 UPDATE。但如果您在您的應用程 序中反其道而行的話,那麼也是可以的 — RESTful Web Services 一書支持使用 PUT 來 INSERT。
那麼要成為真正的面向資源的服務要滿足哪些條件呢?可以這樣歸結:創建一個好的統一資源標識符 (Uniform Resource Identifier,URI),並以標准化的方式來使用 HTTP 動詞(GET、POST、PUT 和 DELETE),而不是使用與自定義的方法調用相結合的動詞(GET)。
再回到 Beatles 的查詢上,要想更接近正式的 RESTful 接口,第一步就是要調試 URI。Beatles 不 是作為參數而被傳入到 webSearch 方法,而是成為了 URI 的中心資源。例如,關於 Beatles 的 Wikipedia 文章的 URI 為 http://en.wikipedia.org/wiki/Beatles。
但是真正把 GETful 原理和 RESTful 原理區別開來的是用於返回資源表示的方法。Yahoo! RPC 接口 定義了很多自定義方法(webSearch、albumSearch、newsSearch 等等)。如果不讀取文檔的話,是無法 得知方法調用的名稱的。就 Yahoo! 而言,我可以跟隨它的模式並猜出它有 songSearch、imageSearch 和 videoSearch 這幾個方法調用,但卻不敢保證一定是這樣。同樣,其他的 Web 站點可能使用不同的命 名約定,如 findSong 或者 songQuery。就 Grails 而言,像 aiport/list 和 airport/show 這樣的自 定義操作在整個應用程序內都是標准操作,但這些方法名稱無法成為其他 Web 框架中的標准。
相反,RESTful 方法通常使用 HTTP GET 來返回所涉及的資源表示。因此對於 Wikipedia 上的任何資 源來說(http://en.wikipedia.org/wiki/Beatles、http://en.wikipedia.org /wiki/United_Airlines 或者 http://en.wikipedia.org/wiki/Peanut_butter_and_jelly_sandwich),我都可以得知 GET 是獲 取它的標准方式。
當處理一個資源的完整的 Create/Retrieve/Update/Delete(CRUD)生命周期時,標准化的方法調用 的強大功能就變得更加顯而易見了。RPC 接口不提供創建新資源的標准化方式。自定義的方法調用可以是 create、new、insert、add 抑或是其他任何調用。在 RESTful 接口中,每向 URI 發送一個 POST 請求 就會插入一個新資源。PUT 可以更新資源,而 DELETE 可以刪除資源。
現在您已經對 GETful 與 RESTful Web 服務之間的差異有了更充分的了解了,並已經准備好用 Grails 創建自己的服務了。這兩種服務的例子您都將看得到,但我要從簡單的 POX 例子開始說起。
用 Grails 實現 GETful Web 服務
從 Grails 應用程序中獲取 POX 的最快捷的方式就是導入 grails.converters.* 包,然後添加一對 新的閉包,如清單 1 所示:
清單1. 簡單的 XML 輸出
import grails.converters.*
class AirportController{
def xmlList = {
render Airport.list() as XML
}
def xmlShow = {
render Airport.get(params.id) as XML
}
//... the rest of the controller
}
您在 “精通 Grails:使用 Ajax 實現多對多關系 中見過了使用中的 grails.converters” 包。該 包向您提供了非常簡單的 JavaScript Object Notation(JSON)和 XML 輸出支持。圖 1 展示了調用 xmlList 操作的結果:
圖 1. 來自於 Grails 的默認 XML 輸出
雖然默認的 XML 輸出很好調試,但您還是想稍微自定義一下格式。還好,render() 方法給您提供了 一個 Groovy MarkupBuilder,它允許您動態定義自定義 XML。清單 2 創建了一些自定義 XML 輸出:
清單 2. 自定義 XML 輸出
def customXmlList = {
def list = Airport.list()
render(contentType:"text/xml"){
airports{
for(a in list){
airport(id:a.id, iata:a.iata){
"official-name"(a.name)
city(a.city)
state(a.state)
country(a.country)
location(latitude:a.lat, longitude:a.lng)
}
}
}
}
}
圖 2 展示了輸出結果:
圖 2. 使用 Groovy MarkupBuilder 的自定義 XML 輸出
注意源代碼和 XML 輸出之間的對應的緊密程度。您可以隨意定義元素名稱(airports、airport、 city),無需顧及它們是否與類的真實字段名稱對應。如果您想提供一個以連字符鏈接的元素名稱的話( 諸如 official-name),又或者想要添加名稱空間支持的話,只要給元素名稱加上引號就可以了。而屬性 (諸如 id 和 iata)是用 Groovy 散列映射鍵:值 語法定義的。要填充元素的正文,需要提供一個不帶 鍵:的值。
內容協商與 Accept 報頭
創建一個返回數據的 HTML 和 XML 表示的單獨閉包是很簡單的,但如果想創建一個既可以返回 HTML 又可以返回 XML 表示的閉包的話,該怎麼辦呢。這也是可以實現的,這要多虧在 HTTP 請求中包含有 Accept 報頭。這個簡單的元數據告訴服務器:“嗨,您對這個 URI 中的資源可能有不只一個資源表示 — 我更喜歡這個。”
cURL 是一個方便的開源命令行 HTTP 工具。在命令行輸入 curl http://localhost:9090/trip/airport/list ,以此來模擬請求機場列表的浏覽器請求。您應該會看到 HTML 響應展現在您的熒屏上。
現在,對請求做兩處小小的變動。這回,代替 GET 發出一個 HEAD 請求。HEAD 是一個標准 HTTP 方 法,它僅僅返回響應的元數據,而不返回正文(您現在正在進行的調試的類型包含在 HTTP 規范中)。另 外,將 cURL 放置於 verbose 模式,這樣您就也能夠看到請求元數據了,如清單 3 所示:
清單 3. 使用 cURL 來調試 HTTP
$ curl --request HEAD --verbose http://localhost:9090/trip/airport/list
* About to connect() to localhost port 9090 (#0)
* Trying ::1... connected
* Connected to localhost (::1) port 9090 (#0)
> HEAD /trip/airport/list HTTP/1.1
> User-Agent: curl/7.16.3 (powerpc-apple-darwin9.0)
libcurl/7.16.3 OpenSSL/0.9.7l zlib/1.2.3
> Host: localhost:9090
> Accept: */*
>
< HTTP/1.1 200 OK
< Content-Language: en-US
< Content-Type: text/html; charset=utf-8
< Content-Length: 0
< Server: Jetty(6.1.4)
<
* Connection #0 to host localhost left intact
* Closing connection #0
注意請求中的 Accept 報頭。客戶機要是提交 */* 的話,就意味著:“返回什麼樣的格式都無所謂。 我將接受任何內容。”
cURL 允許您使用這個值來覆蓋 --header 參數。輸入 curl --request HEAD --verbose --header Accept:text/xml http://localhost:9090/trip/airport/list,並驗證 Accept 報頭正在請求 text/xml 。這就是資源的 MIME 類型了。
那麼,Grails 是如何響應服務器端的 Accept 報頭的呢?再向 AirportController 添加一個閉包, 如清單 4 所示:
清單 4. debugAccept 操作
def debugAccept = {
def clientRequest = request.getHeader("accept")
def serverResponse = request.format
render "Client: ${clientRequest}\nServer: ${serverResponse}\n"
}
清單 4 中的第一行從請求中檢索出了 Accept 報頭。第二行展示了 Grails 如何轉換請求和它將要發 回的響應。
現在,使用 cURL 來做相同的搜索,如清單 5 所示:
清單 5. 調試 cURL 中的 Accept 報頭
$ curl http://localhost:9090/trip/airport/debugAccept
Client: */*
Server: all
$ curl --header Accept:text/xml http://localhost:9090/trip/airport/debugAccept
Client: text/xml
Server: xml
all 和 xml 值是哪來的呢?看一下 grails-app/conf/Config.groovy。在文件頂部,您應該看到了一 個散列映射,它對所有的鍵都使用了簡單名稱(像 all 和 xml 這樣的名稱),而且所有的值都使用了與 之對應的 MIME 類型。清單 6 展示了 grails.mime.types 散列映射:
清單 6. Config.groovy 中的 grails.mime.types 散列
grails.mime.types = [ html: ['text/html','application/xhtml+xml'],
xml: ['text/xml', 'application/xml'],
text: 'text-plain',
js: 'text/javascript',
rss: 'application/rss+xml',
atom: 'application/atom+xml',
css: 'text/css',
csv: 'text/csv',
all: '*/*',
json: ['application/json','text/json'],
form: 'application/x-www-form-urlencoded',
multipartForm: 'multipart/form-data'
]
高級的內容協商
典型的 Web 浏覽器提供的 Accept 報頭要比您與 cURL 一起使用的稍微復雜些。例如,Mac OS X 10.5.4 上的 Firefox 3.0.1 提供的 Accept 報頭大致是這樣的:
text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
它是一個用逗號隔開的列表,它帶有可選的 q 屬性,用以支持 MIME 類型(q 值 — quality 的縮寫 — 是 float 值,范圍是 0.0 到 1.0)。由於 application/xml 被賦予了一個為 0.9 的 q 值,所以與 其他類型的數據相比,Firefox 更偏好 XML 數據。
下面是 Mac OS X 10.5.4 上的 Safari 3.1.2 版本提供的 accept 報頭:
text/xml,application/xml,application/xhtml+xml,
text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5
text/html MIME 類型被賦予了一個為 0.9 的 q 值,所以首選的輸出類型是 HTML,0.8 時為 text/plain,0.5 時為 */*。
那麼,現在您應該對內容協商有了更多的了解了,您可以將 withFormat 塊添加到 list 操作,以此 來依據請求中的 Accept 報頭返回合適的數據類型,如清單 7 所示:
清單 7. 在一個操作中使用 withFormat 塊
def list = {
if(!params.max) params.max = 10
def list = Airport.list(params)
withFormat{
html{
return [airportList:list]
}
xml{
render list as XML
}
}
}
每一個塊的最後一行一定會是一個 render、return 或者 redirect — 與普通操作沒什麼不同。如果 Accept 報頭變成 “all”(*/*)的話,則會使用塊中的第一個條目。
改變 cURL 中的 Accept 報頭是不錯,但是通過改變 URI 您還可以作一些測試工作。 http://localhost:8080/trip/airport/list.xml 和 http://localhost:8080/trip/airport/list? format=xml 都可以用來顯式地覆蓋 Accept 報頭。隨便試一下 cURL 和各種 URI 值,確保 withFormat 塊能發揮預期作用。
如果想讓這個行為成為 Grails 中的標准的話,不要忘記您可以輸入 grails install-templates,並 在 /src/templates 中編輯文件。
所有的基本構建塊就位之後,最後一步就是將 GETful 接口轉化成一個真正的 RESTful 接口。
用 Grails 實現 RESTful Web 服務
首先,需要確保您的控制器已經開始響應那四個 HTTP 方法了。回想一下,如果用戶不指定一個像 list 或 show 這樣的操作的話,index 閉包就是通往控制器的入口點。index 默認重定向到 list 操作 :def index = { redirect(action:list,params:params) }。用清單 8 中的代碼替換這個代碼:
清單 8. 啟動 HTTP 方法
def index = {
switch(request.method){
case "POST":
render "Create\n"
break
case "GET":
render "Retrieve\n"
break
case "PUT":
render "Update\n"
break
case "DELETE":
render "Delete\n"
break
}
}
如清單 9 所示,使用 cURL 來驗證 switch 語句運行正常:
清單 9. 全部四個 HTTP 方法都使用 cURL
$ curl --request POST http://localhost:9090/trip/airport
Create
$ curl --request GET http://localhost:9090/trip/airport
Retrieve
$ curl --request PUT http://localhost:9090/trip/airport
Update
$ curl --request DELETE http://localhost:9090/trip/airport
Delete
實現 GET
由於您已經知道如何返回 XML 了,實現 GET 方法就應該是小菜一碟了。但有一點需要注意。對 http://localhost:9090/trip/airport 的 GET 請求應該返回一個機場列表。而對 http://localhost:9090/trip/airport/den 的 GET 請求應該返回 IATA 代碼為 den 的一個機場實例。 要達到這個目的,必須建立一個 URL 映射。
在文本編輯器中打開 grails-app/conf/UrlMappings.groovy。默認的 /$controller/$action?/$id? 映射看起來應該很熟悉。URL http://localhost:9090/trip/airport/show/1 映射到了 AiportController 和 show 操作,而 params.id 值被設置成 1。操作和 ID 結尾的問號說明 URL 元素 是可以選擇的。
如清單 10 所示,向將 RESTful 請求映射回 AirportController 的 static mappings 塊添加一行。 由於還沒有在其他控制器中實現 REST 支持,所以我暫時對控制器進行了硬編碼。稍候可能會用 $controller 來替代 URL 的 airport 部分。
清單 10. 創建一個自定義 URL 映射
class UrlMappings {
static mappings = {
"/$controller/$action?/$id?"{
constraints { // apply constraints here
}
}
"/rest/airport/$iata?"(controller:"airport",action:"index")
"500"(view:'/error')
}
}
該映射確保了所有以 /rest 開頭的 URI 都被傳送到了 index 操作(這樣就不需要協商內容了)。它 還意味著您可以檢查 params.iata 存在與否,以此來決定是應該返回列表還是一個實例。
按清單 11 所示的方法,修改 index 操作:
清單 11. 從 HTTP GET 返回 XML
def index = {
switch(request.method){
case "POST": //...
case "GET":
if(params.iata){render Airport.findByIata(params.iata) as XML}
else{render Airport.list() as XML}
break
case "PUT": //...
case "DELETE": //...
}
}
在 Web 浏覽器中輸入 http://localhost:9090/trip/rest/airport 和 http://localhost:9090/trip/rest/airport/den,確認自定義 URL 映射已經就位。
通過 HTTP 方法實現的自定義 URL 映射
您可以使用不同的方法來建立 RESTful URL 映射。您可以依照 HTTP 請求將請求傳送到具體操作。例 如,按照如下的方法可以將 GET、PUT、POST 和 DELETE 映射到已經存在的相應 Grails 操作:
static mappings = {
"/airport/$id"(controller:"airport"){
action = [GET:"show", PUT:"update", DELETE:"delete", POST:"save"]
}
}
實現 DELETE
添加 DELETE 支持與添加 GET 支持的差別不大。但在這裡,我僅需要通過 IATA 代碼逐個刪除機場。 如果用戶提交了一個不帶有 IATA 代碼的 HTTP DELETE 請求的話,我將返回一個 400 HTTP 狀態碼 Bad Request。如果用戶提交了一個無法找到的 IATA 代碼的話,我將返回一個常見的 404 狀態碼 Not Found 。只有刪除成功了,我才會返回標准的 200 OK。
將清單 12 中的代碼添加到 index 操作中的 DELETE case 中:
清單 12. 對 HTTP DELETE 做出響應
def index = {
switch(request.method){
case "POST": //...
case "GET": //...
case "PUT": //...
case "DELETE":
if(params.iata){
def airport = Airport.findByIata(params.iata)
if(airport){
airport.delete()
render "Successfully Deleted."
}
else{
response.status = 404 //Not Found
render "${params.iata} not found."
}
}
else{
response.status = 400 //Bad Request
render """DELETE request must include the IATA code
Example: /rest/airport/iata
"""
}
break
}
}
首先,試著刪除一個已知確實存在的機場,如清單 13 所示:
清單 13. 刪除一個存在的機場
Deleting a Good Airport</heading>
$ curl --verbose --request DELETE http://localhost:9090/trip/rest/airport/lga
> DELETE /trip/rest/airport/lga HTTP/1.1
< HTTP/1.1 200 OK
Successfully Deleted.
然後,試著刪除一個已知不存在的機場,如清單 14 所示:
清單 14. 試著 DELETE 一個不存在的機場
$ curl --verbose --request DELETE http://localhost:9090/trip/rest/airport/foo
> DELETE /trip/rest/airport/foo HTTP/1.1
< HTTP/1.1 404 Not Found
foo not found.
最後,試著發出一個不帶有 IATA 代碼的 DELETE 請求,如清單 15 所示:
清單 15. 試著一次性 DELETE 所有機場
$ curl --verbose --request DELETE http://localhost:9090/trip/rest/airport
> DELETE /trip/rest/airport HTTP/1.1
< HTTP/1.1 400 Bad Request
DELETE request must include the IATA code
Example: /rest/airport/iata
實現 POST
接下來您的目標是要插入一個新的 Airport。創建一個如清單 16 所示的名為 simpleAirport.xml 的 文件:
清單 16. simpleAirport.xml
<airport>
<iata>oma</iata>
<name>Eppley Airfield</name>
<city>Omaha</city>
<state>NE</state>
<country>US</country>
<lat>41.3019419</lat>
<lng>-95.8939015</lng>
</airport>
如果資源的 XML 表示是扁平結構(沒有深層嵌套元素),而且每一個元素名稱都與類中的一個字段名 稱相對應的話,Grails 就能夠直接從 XML 中構造出新類來。XML 文檔的根元素是通過 params 尋址的, 如清單 17 所示:
清單 17. 響應 HTTP POST
def index = {
switch(request.method){
case "POST":
def airport = new Airport(params.airport)
if(airport.save()){
response.status = 201 // Created
render airport as XML
}
else{
response.status = 500 //Internal Server Error
render "Could not create new Airport due to errors:\n ${airport.errors}"
}
break
case "GET": //...
case "PUT": //...
case "DELETE": //...
}
}
XML 一定要使用扁平結構,這是因為 params.airport 其實是一個散列(Grails 是在後台將 XML 轉 換成散列的)。這意味著您在對 Airport 使用命名參數構造函數 — def airport = new Airport (iata:"oma", city:"Omaha", state:"NE")。
要測試新代碼,就要使用 cURL 來 POST simpleAirport.xml 文件,如清單 18 所示:
清單 18. 使用 cURL 來發出一個 HTTP POST
$ curl --verbose --request POST -- header "Content-Type: text/xml" --data
@simpleAirport.xml http://localhost:9090/trip/rest/airport
> POST /trip/rest/airport HTTP/1.1
> Content-Type: text/xml
> Content-Length: 176
>
< HTTP/1.1 201 Created
< Content-Type: text/xml; charset=utf-8
<?xml version="1.0" encoding="utf-8"?><airport id="14">
<arrivals>
<null/>
</arrivals>
<city>Omaha</city>
<country>US</country>
<departures>
<null/>
</departures>
<iata>oma</iata>
<lat>41.3019419</lat>
<lng>-95.8939015</lng>
<name>Eppley Airfield</name>
<state>NE</state>
</airport>
如果 XML 比較復雜的話,則需要解析它。例如,還記得您先前定義的自定義 XML 格式麼?創建一個 名為 newAirport.xml 的文件,如清單 19 所示:
清單 19. newAirport.xml
<airport iata="oma">
<official-name>Eppley Airfield</official-name>
<city>Omaha</city>
<state>NE</state>
<country>US</country>
<location latitude="41.3019419" longitude="-95.8939015"/>
</airport>
現在,在 index 操作中,用清單 20 中的代碼替代 def airport = new Airport(params.airport) 行:
清單 20. 解析復雜的 XML
def airport = new Airport()
airport.iata = request.XML.@iata
airport.name = request.XML."official-name"
airport.city = request.XML.city
airport.state = request.XML.state
airport.country = request.XML.country
airport.lat = request.XML.location.@latitude
airport.lng = request.XML.location.@longitude
request.XML 對象是一個持有原始 XML 的 groovy.util.XmlSlurper。它是根元素,因此您可以通過 名稱(request.XML.city)來尋找子元素。如果名稱是用連字符連接的,或者使用了名稱空間,就加上引 號(request.XML."official-name")。元素的屬性要使用 @ 符號(request.XML.location.@latitude) 來訪問。
最後,使用 cURL 來測試它:curl --request POST --header "Content-Type: text/xml" --data @newAirport.xml http://localhost:9090/trip/rest/airport。
實現 PUT
您需要支持的最後一個 HTTP 方法就是 PUT。了解了 POST 之後,會知道代碼基本是一樣的。惟一不 同的就是它無法直接從 XML 構造類,您需要向 GORM 尋求現有的類。然後,airport.properties = params.airport 行會用新的 XML 數據來替代現有的字段數據,如清單 21 所示:
清單 21. 響應 HTTP PUT
def index = {
switch(request.method){
case "POST": //...
case "GET": //...
case "PUT":
def airport = Airport.findByIata(params.airport.iata)
airport.properties = params.airport
if(airport.save()){
response.status = 200 // OK
render airport as XML
}
else{
response.status = 500 //Internal Server Error
render "Could not create new Airport due to errors:\n ${airport.errors}"
}
break
case "DELETE": //...
}
}
創建一個名為 editAirport.xml 的文件,如清單 22 所示:
清單 22. editAirport.xml
<airport>
<iata>oma</iata>
<name>xxxEppley Airfield</name>
<city>Omaha</city>
<state>NE</state>
<country>US</country>
<lat>41.3019419</lat>
<lng>-95.8939015</lng>
</airport>
最後,使用 cURL: curl --verbose --request PUT --header "Content-Type: text/xml" --data @editAirport.xml http://localhost:9090/trip/rest/airport 來測試它。
結束語
我在很短的時間內講解了很多相關知識。現在,您應該了解到 SOA 和 ROA 之間的不同之處了。您同 樣也應該意識到,並不是所有的 RESTful Web 服務都如出一轍。有些 Web 服務是 GETful 的 — 使用 HTTP GET 請求來調用類 RPC 方法。而其他的則是純粹面向資源的,其中 URI 是訪問資源的關鍵,而標 准 HTTP GET、POST、PUT 和 DELETE 方法構成了完整的 CRUD 功能。無論您是喜歡 GETful 方法還是 RESTful 方法,Grails 都為輸出和輕易地獲取 XML 提供了強有力的支持。
在下一期的 精通 Grails 中,我將把重點轉向測試。Grails 配有優良的開箱即用的測試工具。而那 些沒有提供的功能則可以在以後以插件的形式添加進去。既然已經在 Grails 開發中投入了這麼多的時間 了,那麼就一定要確保它在無錯誤的情況下開始運行並可以在應用程序的整個生命周期中都可以保持這種 無錯誤的狀態。在達到這個目標之前,繼續關注精通 Grails 系列文章吧。