歡迎您光臨本站 註冊首頁

精通 Grails: RESTful Grails

←手機掃碼閱讀     火星人 @ 2014-03-12 , reply:0
  
我們生活在 mashup 的時代。創建能夠給用戶提供所需信息的 Web 頁面固然是一個好的出發點,但是要是能夠提供原始數據的源,從而讓其他的 Web 開發人員能夠輕易將其與他們自己的應用程序相融合的話,這樣豈不是更好。在本期的 精通 Grails 中,Scott Davis 將介紹用 Grails 生成 XML 而不是通常的 HTML 的各種方法。

本月,我將向您呈現如何讓您的 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 的搜尋結果是一樣的。

關於本系列

Grails 是一種新型 Web 開發框架,它將常見的 Spring 和 Hibernate 等 Java™ 技術與當前流行的實踐(比如約定優於配置)相結合。在加入腳本語言的靈活性和動態性的同時,用 Groovy 編寫的 Grails 可以無縫地集成遺留的 Java 代碼。學習完 Grails 之後,您將改變看待 Web 開發的方式。

如果假設 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 可以刪除資源(參見 POST 與 PUT 側邊欄)。

現在您已經對 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(參見 參考資源,查看更多有關 MarkupBuilder 的消息的鏈接)。清單 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(參見 參考資料,查看更多有關 HTTP 狀態碼的信息的鏈接)。

將清單 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)來訪問(參見 參考資料,查看有關 XmlSlurper 的更多信息的鏈接)。

最後,使用 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 系列文章吧。 (責任編輯:A6)



[火星人 ] 精通 Grails: RESTful Grails已經有788次圍觀

http://coctec.com/docs/linux/show-post-69108.html