构建 RESTful Web 服务
from: https://www.ibm.com/developerworks/cn/education/java/j-rest/j-rest.html
開始之前
關于本教程
REST 是一種思維方式,而非協議或標準。它是設計基于命名資源而非消息的松耦合應用程序 — 通常指面向 Web 的應用程序 — 的一種風格。在本教程中,您將了解到何為 REST 以及如何使用 Restlet(一個面向 Java? 應用程序的輕量級框架)構建 RESTful 應用程序。
目標
本教程從 REST 的基本概念開始逐步指導您使用 Restlet 構建應用程序。您將學習如何:
- 定義 RESTful Web 服務
- 用 Restlet 框架實現它們
- 用 JUnit 測驗框架驗證它們
學完本教程后,您就會領會到使用 RESTful 原則進行設計的益處,了解到 Restlet 框架是如何輕松地實現它們的。
先決條件
為了更好地學習本教程,您需要熟悉 Java 語法以及 Java 平臺中面向對象開發的基本概念。還要熟悉 Web 應用程序。熟悉 Groovy、Unit、DbUnit 和 XMLUnit 對此也很有幫助。
系統需求
要跟隨并試用本教程中的代碼,您需要如下列出的任意一種可行的安裝:
- Sun's JDK 1.5.0_09(或更高版本)
- IBM Developer Kit for Java technology 1.5.0 SR3
- Apache Ant 1.7 或者更高級版本
本文的源代碼有兩種版本(參見 下載)。其中一個版本包含了全部代碼以及所需依賴項(Restlet 框架 JUnit、XMLUnit 和 DbUnit)。使用低帶寬連接的讀者可以從它們各自的站點分別下載 Restlet 框架、JUnit、XMLUnit 和 DbUnit(參見 參考資料)并使用不包含依賴項的 下載 包。
要學習本教程,推薦的系統配置為:
- 支持 Sun JDK 1.5.0_09(或更新版本)或者 IBM JDK 1.5.0 SR3 的系統,主內存至少為 500MB
- 有至少 20MB 的磁盤空間安裝軟件組件和涉及到的示例
本教程中的指令和示例基于 Microsoft? Windows? 操作系統。教程中所用到的所有工具在 Linux? 和 UNIX? 系統上都可以運行。
何為 REST?
REST 是設計基于命名資源 — 例如,以 Uniform Resource Locators(URL)、Uniform Resource Identifiers(URI)和 Uniform Resource Names(URN)的形式 — 而非消息的松耦合 Web 應用程序的一種風格。REST 巧妙地借助已經驗證過的成功的 Web 基礎設施 — HTTP。換句話說,REST 利用了 HTTP 協議的某些方面,例如 GET 和 POST 請求。這些請求可以很好地映射到標準業務應用程序需求,諸如創建、讀取、更新和刪除(CRUD),如表 1 所示:
表 1. CRUD/HTTP 映射
| POST |
| GET |
| PUT |
| DELETE |
請求就像是動詞,而資源就像是名詞,把兩者相關聯就形成了對行為的邏輯表達 — 例如, GET 這個文件,DELETE 那條記錄。
真正的 REST 之父 Roy Fielding 在他的博士畢業論文中陳述到:REST “強調組件交互的可伸縮性、界面的普遍性、獨立部署組件以及使用中間組件來減少交互延遲,增強安全性并封裝遺留系統”(參見 參考資料)。構建 RESTful 系統并不難,且這樣的系統具有高度的可伸縮性,同時與底層數據松散耦合;這樣的系統還可以很好地利用緩存。
Web 上所有的東西(頁面、圖像等)本質上都是資源。而 REST 正是基于命名資源而非消息的,這就限制了底層技術的曝光,從而給應用程序設計中的松耦合提供了便利條件。例如,下面的 URL 在不暗示任何底層技術的情況下,公開了資源:http://thediscoblog.com/2008/03/20/unambiguously-analyzing-metrics/。
該 URL 表示一個資源 — 一篇名為 “Unambiguously analyzing metrics” 的文章。請求該資源就會調用 HTTP GET 命令。注意該 URL 是基于名詞的。基于動詞的版本(大概類似 http://thediscoblog.com/2008/03/20/getArticle?name=unambiguously-analyzing-metrics)會違反 REST 原則,因為它以 getArticle 的形式嵌套了一條消息。您也可以設想通過 HTTP 的 POST 命令來發布一個新資源,(比如說,一篇諸如 http://thediscoblog.com/2008/03/22/rest-is-good-for-you/ 的文章)。你還可以設想用關聯的、基于動詞的 API — 如 createArticle?name=rest-is-good-for-you and deleteArticle?name=rest-is-good-for-you — 這樣的調用來攔截 HTTP GET 命令,并最大限度地忽略已有的(并且是成功的)HTTP 基礎設施。換句話說,它們不是 RESTful 風格。
REST 的魅力在于任何東西都可以成為資源,且表示方法也可以不同。在前面的例子中,資源為一個 HTML 文件,因此,其響應可能是 HTML 格式的。但是資源也可以是一個 XML 文檔、序列化的對象或者 JSON 表示。其實,這些都無關緊要。重要的是資源被命名了,并且與它通信不會影響其狀態。不影響狀態是很重要的,因為無狀態的交互有利于可伸縮性。
它的價值在那里?
引用達芬奇的一句名言 “簡潔就是終極復雜”。萬維網的實現非常簡單,并且無可置否地獲得了成功。REST 正是利用了 Web 的簡單性,并因此造就了高度可伸縮的、松散耦合的系統,而且事實證明,這樣的系統很容易構建。
正如您所看到的,構建 RESTful 應用程序最難的部分在于確定要公開的資源。解決了這個問題之后,再使用開源 Restlet 框架構建 RESTful Web 服務就是小菜一碟了。
起跑:構建一個 RESTful API
在本節中,您將為一個 Web 服務構建一個 RESTful API,該服務利用了支持數據庫的現有應用程序的功能。
RESTful 比賽
設想這樣一個在線應用程序,它管理賽跑比賽,參賽人員要跑完不同的路程(比如芝加哥馬拉松賽跑)。應用程序管理賽跑(或者賽事)以及與其相關的參賽人員。它會報告某個選手的時間(跑完全程所用的時間)和排名(參賽人員以第幾名跑完全程)。賽事籌辦公司 Acme Racing 要求您構建一個 RESTful Web 服務,主辦方可以用它來為特定比賽安排新的賽事和選手,并且可以為某次特定比賽提供官方記錄。
Acme Racing 已經有了一個遺留的胖客戶機應用程序,它支持類似的請求,并利用了一個簡單的數據庫和一個域模型。因此,剩下的工作就只有公開這個功能了。記住 REST 的魅力就在于它與底層應用程序的隱式松散耦合。因此,您目前的工作并非是去操心數據模型或與其相關聯的技術 — 而是去構造一個支持公司需求的 RESTful API。
比賽 URI
Acme Races 希望主辦方能夠:
- 查看現有比賽細節
- 創建新的比賽
- 更新現有比賽
- 刪除比賽
由于 REST 最終歸結為命名資源,API 就成為了一系列 URI 模式,并且與資源相關聯的行為要通過標準 HTTP 命令調用。
正如您可以看到的,客戶的請求很好地映射到了 CRUD。并且如您在 表 1 中所了解到的一樣,REST 分別通過 HTTP 的 POST、GET、PUT 以及 DELETE 請求來支持 CRUD。因此,一個支持這些請求的基 RESTful URI 應為 http://racing.acme.com/race。注意,在這種情況下,比賽是客戶機要使用的資源。
用 HTTP GET 來調用 URI 會返回一個比賽列表(這時先不要考慮響應的格式)。要添加新比賽,要用包含適當信息(例如,一個包含諸如名稱、日期和距離等信息的 XML 文檔)的 HTTP POST 來調用同一 URI。
要更新和刪除現有比賽,則需要對特定比賽的實例進行操作。因此,可以給單個比賽賦予一個 URI:http://racing.acme.com/race/race_id。在這種情況下,race_id 表示任一比賽標識符的一個占位符(諸如 1 或者 600 米)。因此,查看一個現有比賽實例就是針對該 URI 執行一個 HTTP GET 請求;更新或者刪除一個比賽分別為一個 PUT 或者 DELETE 請求。
Acme Racing 可能還希望公開有關某次比賽的參賽人員的數據。他們希望他們的服務支持:
- 獲得有關特定比賽的全部參賽人員的數據。該數據還要包含已結束的比賽的賽跑時間和排名。
- 為特定比賽創建一個或多個參賽人員。
- 更新特定比賽的某一參賽人員的信息(如年齡)。
- 刪除特定比賽的某一參賽人員。
Acme 可能還希望該服務可以讓用戶查看特定比賽的某個參賽人員的個人數據。
和比賽一樣,將 RESTful URI 應用于與比賽相關聯的參賽人員同樣是一個邏輯行為。例如,查看特定比賽的全部參賽人員可以通過對 http://racing.acme.com/race/race_id/runner 的 GET 請求來實現。
要獲得一個比賽的某個參賽人員的個人信息,可以編址為 http://racing.acme.com/race/race_id/runner/runner_id。
和 race_id 一樣,runner_id 是 ID 的邏輯實現的一個占位符,這些 ID 可以是數字、姓名、字母數字組合等。
向一個比賽添加參賽人員就是一個對 http://racing.acme.com/race/race_id/runner 的 POST 請求。更新或刪除特定參賽人員則分別是對 http://racing.acme.com/race/race_id/runner/runner_id 的 PUT 和 DELETE 請求。
因此,這些 URI(每一個 URI 都支持四個標準 HTTP 請求的其中一些或者全部)就滿足了 Acme Racing 的需求:
- /race
- /race/race_id
- /race/race_id/runner
- /race/race_id/runner/runner_id
記住,一個 URI 可以映射到不止一個 HTTP 動詞(例如,將一個HTTP GET 應用到 /race 將返回數據;使用 POST 和適當的數據在服務器上創建數據)。因此,有些 HTTP 命令不能實現。例如,/race 可能不支持 DELETE 命令(Acme Racing 不會刪除所有的比賽);/race/race_id 可能支持 DELETE 命令,因為移除一個比賽的某個特定實例是一個業務需求。
格式化資源
在本小節中,您將構造一系列的 XML 文檔來表示 RESTful 比賽 Web 服務將會支持的資源。
比賽 URI
前一小節中為 Acme Racing 構建的 RESTful API 涉及了網絡端點或者 URI,但并未提及資源。正如我在前面提到的,就 REST 而言,資源的格式并不重要。例如,您可以來回傳送 XML 或者二進制流。
在業務事務環境中,XML 是不折不扣的在機器之間通信的混合語言,所以構造一系列受 RESTful 服務支持的 XML 文檔很有意義。競賽的域相當簡單,而且您可以使用現有的數據模型,所以定義幾個表示比賽和參賽人員的 XML 文檔也很簡單。
例如,可以用清單 1 中的 XML 定義一個比賽:
清單 1. 某次比賽的 XML 文檔
| 1 2 3 4 | <race name="Mclean 1/2 Marathon" date="2008-05-12" distance="13.1" id="1"> ?<uri>/races/1</uri> ??<description/> </race> |
注意一個 <race> 有一個 id,且清單 1 包含一個作為比賽定義的一部分的 id。這是 REST 與 Web 的一個關鍵的方面 — 資源是相關的,應該鏈接在一起。因此,一個 <race> 總是含有一個描述它的 RESTful 表示的 <uri> 元素。清單 1 中的 XML 是一個 GET 到 /races/1 的請求的響應。
要創建一個新比賽,您可以省略 id 方面(因為管理惟一 ID 是由您正在構建的應用程序控制的)。這暗示您可以排除 <uri> 元素。因此,POST 請求應該如清單 2 所示:
清單 2. 創建比賽的 XML
| 1 2 3 | <race name="Limerick 2008 Half" date="2008-05-12" distance="13.4"> ?<description>erin go braugh and have a good time!</description> </race> |
那參賽人員呢?參賽人員與比賽有關,對么?所以 <race> 元素支持保存一到兩個 <runner> 元素,如清單 3 所示:
清單 3. 與比賽相關的參賽人員
| 1 2 3 4 5 6 7 8 9 10 11 12 | <race name="Limerick 200 Half" date="2008-05-12" distance="13.4" id="9"> ?<uri>races/9</uri> ?<description>erin go braugh and have a good time!</description> ?<runners> ??<runner first_name="Linda" last_name="Smith" age="25" id="21"> ???<uri>/races/9/runner/21</uri> ??</runner> ??<runner first_name="Andrew" last_name="Glover" age="22" id="20"> ???<uri>/races/9/runner/20</uri> ??</runner> ?</runners> </race> |
例如,清單 3 中的 XML 文檔就是通過 URI /race/race_id/runner 而返回的。API 還支持通過 URI /race/race_id/runner/runner_id 對單一參賽人員執行的 CRUD 操作。
因此,這些 CRUD 操作的 XML 如清單 4 所示:
清單 4. CRUD XML
| 1 2 3 4 5 6 7 8 | <race name="Mclean 1/2 Marathon" date="2008-05-12" distance="13.1" id="1"> ?<uri>/races1</uri> ?<description /> ?<runner first_name="Andrew" last_name="Glover" age="32" id="1"> ??<uri>/races/1/runner/1</uri> ??<result time="100.04" place="45" /> ?</runner> </race> |
注意,如果比賽已經完成,XML 文檔中會包含參賽人員的比賽結果。記住,使用一個 POST 請求就意味著創建一個參賽人員,因此,<runner> 元素的 id 屬性不會呈現。
Restlets
您已經定義了一個 RESTful API 了,它可以很好地映射到 CRUDing 比賽和參賽人員。并且您已經定義了通信的格式:XML 文檔。在這一小節中,您將開始用一個仿效 servlet 的創新的框架將所有這些組合起來。
Restlet 框架
Restlet 應用程序與 servlet 應用程序有一個相似點,就是它們都處在容器中,但實際上它們在兩個方面是截然不同的。第一,Restlet 不使用 HTTP 的直接概念或其狀態顯示,如 cookies 或者 session。第二,Restlet 框架極其輕便。正如您將要看到的,只用幾個核心 Restlet 基類擴展出來的幾個類就能夠構建一個功能完善的 RESTful 應用程序。配置和部署利用現有的容器模型,所以您只需更新原來的 web.xml 文件,并部署一個標準 Web 歸檔文件(WAR)就可以了。
基本上,一個用 Restlet 框架構建的 RESTful 應用程序的大部分都需要使用兩個基類: Application 和 Resource。邏輯上,Application 實例將 URI 映射到 Resource 實例。Resource 實例處理基本的 CRUD 命令,當然,這些命令都要映射到 GET、POST、PUT 和 DELETE。
比賽應用程序
通過擴展 Restlet 框架的 Application 類來創建一個起跑點。在這個類中,定義響應 URI 的 Resource。該定義過程是用框架的 Router 類來完成的。例如,如果您有諸如 /order_id 這樣的 URI 的話,您需要指定哪一個對象可以處理這些請求。這個對象是框架的 Resource 類型的一個實例。可以通過將這些對象附加到 Router 實例來使它們同 URI 鏈接,如清單 5 所示:
清單 5. 創建 Router 實例并映射 URI
| 1 2 | Router router = new Router(this.getContext()); router.attach("order/{order_id}", Order.class); |
所以在這個例子中,URI order/order_id 被邏輯映射到 Order 類(它再擴展 Resource)。
Acme Racing 擁有四個您已經定義了的邏輯 RESTful URI — 處理比賽和參賽人員的各個方面的四種模式:
- /race
- /race/race_id
- /race/race_id/runner
- /race/race_id/runner/runner_id
每一個 URI 的行為(比如,如果它使用 POST、DELETE、GET 等)在這點上并不重要。每一個 Resource 的行為則是由 Resource 實例負責;而 Application 實例用于通過 Router 實例將這些 URI 映射到(尚未定義的)Resource ,如清單 6 所示:
清單 6. 將 Acme Racing 的 URI 映射到 Resource
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | public class RaceApplication extends Application{ ?public RaceApplication(Context context) { ??super(context); ?} ?public Restlet createRoot() { ??Router router = new Router(this.getContext()); ??router.attach("/race", RacesResource.class); ??router.attach("/race/{race_id}", RaceResource.class); ??router.attach("/race/{race_id}/runner", RaceRunnersResource.class); ??router.attach("/race/{race_id}/runner/{runner_id}", RaceRunnerResource.class); ??return router; ?} } |
基類 Application 是一個抽象類。擴展類必須實現 createRoot() 方法。在這個方法中,可以創建一個 Router 實例,并將 Resource 附加到 URI,像清單 6 中所示的那樣。
正如您所看到的,有四個不同的 Resource 類。我已經將它們命名,使它們與期望的 URI 的高級行為相匹配。例如 /race URI 應該處理多個比賽實例,因此 Resource 類型被命名為 RacesResource。一旦 URI(/race/race_id)中包含了 id,就暗示著正在操作一個單獨的比賽,因此,Resource 類型適合命名為 RaceResource。
比賽資源
定義了 Application 實例來處理四種不同的 URI 模式后,一定要實現四個 Resource。
Restlet 框架中的 Resource 類型被認為是 Restlet。它們是使用 Restlet 框架開發的 RESTful 的應用程序的核心。與 Application 類型不同,基 Resource 類并非抽象類。它更像是一個可以按需要覆蓋其默認行為的模版。
從較高的程度來看,Resource 有四個需要覆蓋的方法。它們映射到了基本的 HTTP 命令,這些命令是 REST — GET、POST、PUT 以及 DELETE— 的標準,這并不是巧合。由于 Resource 類是非抽象類,所以框架需要為期望調用的行為實現一對方法。例如,如果想讓一個特定的資源來響應 DELETE 請求的話,首先要實現 delete() 方法。第二,必須實現 allowDelete() 方法,并且要讓這個方法返回 true(默認為 false)。相應的 PUT、POST 和 DELETE 默認允許方法返回 false,而 allowGet() 方法默認返回 true。這意味著對于只讀的 Resource,只需要覆蓋一個方法(而不是像在另外三種情況中覆蓋兩個)。或者也可以調用 Resource 類中的 setModifcation(true),因此不需要覆蓋單個 HTTP 動詞 allow 方法。
例如,RacesResource 應該用一個在系統中描述比賽的 XML 文檔來響應 GET 請求。用戶也可以通過這個 Resource 類型創建新比賽。因此,RacesResource 類覆蓋了 Resource 基類中的至少三個方法:
- getRepresentation()
- allowPost()
- post()
記住,Resource 實例默認為只讀。因此,不需要覆蓋 allowGet() 方法。
生成 XML 文檔
在 格式化資源 小節,我們決定利用 XML 作為客戶機和服務之間共享信息的數據機制。因此,Restlet 一定要操作 XML:在 GET 的情況下構建它,在 POST、PUT 或者 DELETE 的情況下消費它。在這一小節中,您將通過使用 Groovy 腳本語言來消除生成和操作 XML 文檔的痛苦(參見 參考資料)。
利用 Groovy
使用 XML 可不是個輕松的活兒。退一步說,這個工作很單調乏味,且容易發生錯誤。幸運的是,Groovy 讓使用 XML 簡單了許多。
您將利用 Groovy 的力量來生成 XML 并完成操作 XML 文檔這個沉悶的工作。使用 Groovy 處理 XML 是再簡單不過了。例如,解析 XML 文檔就很簡單。以清單 7 中的 XML 文檔為例:
清單 7. 一個簡單的待解析的 XML 文檔
| 1 2 3 4 5 6 | <acme-races> ??<race name="Alaska 200 below" date="Thu Jan 01" distance="3.2" id="20"> ????<uri>/races/20</uri> ????<description>Enjoy the cold!</description> ??</race> </acme-races> |
假設您想獲取 <race> 元素的 name 屬性的值,您只需向 Groovy 的 XMLSlurper 類傳入一個 XML 文檔實例,調用 parse() 方法,然后定位到您想要的元素或者屬性就可以了,如清單 8 所示:
清單 8. 用 Groovy 解析 XML
| 1 2 | def root = new XmlSlurper().parseText(raceXML) def name = root.race.@name.text() |
如果您想獲得描述,調用 root.race.description.text() 就可以了。
創建 XML 也很簡單。如果想創建清單 7 中的 XML 代碼片斷的話,只要創建一個 Groovy 的 MarkupBuilder 類的實例,然后給它添加個節點就可以了,如清單 9 所示:
清單 9. 創建 XML 再簡單不過了
| 1 2 3 4 5 6 7 8 9 | def writer = new StringWriter() def builder = new MarkupBuilder(writer) builder."acme-races"() { ????race(name: "Alaska 200 below",? date: "Thu Jan 01", distance: "3.2", id: "20") { ????????uri("/races/20") ????????description("Enjoy the cold!") ????} } println writer.toString() |
注意元素是如何通過附加名稱到 builder 實例而被添加到 XML 文檔的。我需要為 acme-races 加上引號,因為在 Groovy 字符串字母中不允許使用連字符,因此,使 acme-races 成為 String 就很好地解決了這個問題。
元素可以有屬性。屬性名和屬性值是通過構造 Groovy 映射創建的,同時 Groovy 映射將兩者鏈接在一起(例如,name:"Alaska 200 below")。
數據層
本節描述了現有的域對象,它生成了 RESTful 服務將會重用的數據層。
域對象
正如您從 起跑:構建 RESTful API 一節了解到的,Acme Racing 投資于以前的項目的數據層,并且想將其重用于新的 Web 服務。當然,這會讓您的工作更輕松。簡單地說,該數據層由三個業務對象組成:Race、Runner 以及 Result。它們由 Spring 和 Hibernate 有效地管理著;然而,這些框架您是看不見的;您僅有一個運行良好的 JAR 文件(換句話說,它可以讓您輕松創建新比賽,找到現有參賽人員等)。
業務對象支持一系列的尋找方法,它們使獲取比賽和參賽人員實例變得很輕松。對象可以分別通過 save()、update() 和 remove() 方法被持久化、更新、以及從底層數據庫中移除。
例如,Race 對象支持一系列的尋找方法,并為良好地操作持久化數據提供便利條件。Race 對象的 API 很簡單,如清單 10 所示:
清單 10. Race 的 API
| 1 2 3 4 5 6 | Collection<Race> findAll(); Race findById(long id); Race findByName(String name); void create(Race race); void update(Race race); void remove(Race race); |
一個 Race 實例有很多的屬性,如清單 11 所示:
清單 11. Race 的屬性
| 1 2 3 4 5 6 7 | private long id; private String name; private Date date; private double distance; private Set<Runner> participants; private Set<Result> results; private String description; |
Race 的所有屬性都可以通過 getter 和 setter 使用。而且,項集合(如 participants 和 results)支持添加單獨的項。因此,Race 對象有一個 addParticipant() 方法,如清單 12 所示:
清單 12. Race 的 addParticipant() 方法
| 1 | public void addParticipant(final Runner participant) ; |
正如您所看到的,使用這個域模型是很簡單的。
構建與測試服務
知道如何使用 XML 并已經有了可以用的數據層之后,是時候繼續使用 Restlet 構建 RESTful 應用程序并做一些測試準備了。
比賽服務
回憶一下,Acme Racing 希望服務可以讓客戶機查看現有比賽,并創建新比賽。您已經大致了解了支持 /race 這個行為的 RESTful URI。
通過 RaceApplication 類中的 Router 類,可以將這個 URI 鏈接到 RacesResource 類。您已經知道了您必須實現三個方法:
- getRepresentation()
- allowPost()
- post()
因此,創建一個叫做 RacesResource 的類,并確保它擴展了 org.restlet.resource.Resource。另外,實現一個含三個參數的構造函數,如清單 13 所示:
清單 13. RacesResource 中含三個參數的構造函數
| 1 2 3 4 5 | public class RacesResource extends Resource { ?public RacesResource(Context context, Request request, Response response) { ??super(context, request, response); ?} } |
一定要指導 Restlet 如何正確通信資源表示。由于 XML 將會起到資源格式的作用,一定要通過添加 XML 變體類型來指導 Restlet。Restlet 中的 Variant 表示 Resource 的格式?;?Resource 含有一個 getVariants() 方法,它使添加各種 Variant 類型變得容易。因此,將清單 14 中的內容添加到您的構造函數:
清單 14. 將 XML 表示為一個變體
| 1 | this.getVariants().add(new Variant(MediaType.TEXT_XML)); |
Restlet 框架支持很多種媒介類型,包括圖像和視頻。
處理 GET 請求
現在是時候實現類的最簡單行為了:處理 GET 請求。如清單 15 所示涵蓋 getRepresentation() 方法:
清單 15. 涵蓋 getRepresentation()
| 1 2 3 | public Representation getRepresentation(Variant variant) { ?return null; } |
可以看到,這個方法返回了一個 Representation 類型,它有多種實現。其中一個實現 — 適當地命名為 StringRepresentation — 表示字符串,并會滿足您的需求。
正如您所知道的,您已經有了一個遺留域模型,它可以支持使用數據庫。事實證明有人已經寫出了實用程序類,叫做 RaceReporter,它可以將域對象轉換為 XML 文件。這個類的 racesToXml() 方法使用了一個 Race 實例集合,并返回了表示與清單 16 類似的 XML 文檔的 String:
清單 16. XML 響應
| 1 2 3 4 5 6 7 8 9 10 11 12 | <acme-races> ?<races> ??<race name="Leesburg 5K" date="2008-05-12" distance="3.1" id="5"> ??<uri>/races/5</uri> ??<description/> ?</race> ?<race name="Leesburg 10K" date="2008-07-30" distance="6.2" id="6"> ??<uri>/races/6</uri> ??<description/> ?</race> ?</races> </acme-races> |
事實上,這個 XML 文檔演示了在響應 GET 請求而調用 /race URI 時 RESTful Web 服務將返回的內容。
因此,我們的工作是鏈接底層數據存儲中所有比賽實例的檢索;其實在這一點上,您可能已經編寫了一個測試了。
測試服務
使用 Restlet 框架,您能夠構造客戶機實例,并讓它調用 RESTful Web 服務。此外,您可以利用 XMLUnit(參見 參考資源)來驗證服務的輸出是一些已知的 XML 文檔。最后,同樣重要的是,您也可以使用 DbUnit(參見 參考資料)將底層數據庫設置為已知狀態(這樣就能總是獲得相同的 XML 文檔)。
可以使用 JUnit 4 創建兩個 fixture,它可以適當初始化 XMLUnit 和 DbUnit,如清單 17 所示:
清單 17. 設置 XMLUnit 和 DbUnit
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | @Before public void setUpXMLUnit() { ?XMLUnit.setControlParser( ??"org.apache.xerces.jaxp.DocumentBuilderFactoryImpl"); ?XMLUnit.setTestParser( ??"org.apache.xerces.jaxp.DocumentBuilderFactoryImpl"); ?XMLUnit.setSAXParserFactory( ??"org.apache.xerces.jaxp.SAXParserFactoryImpl"); ?XMLUnit.setIgnoreWhitespace(true); } @Before public void setUpDbUnit() throws Exception { ?Class.forName("org.hsqldb.jdbcDriver"); ?IDatabaseConnection conn = ??new DatabaseConnection( ???getConnection("jdbc:hsqldb:hsql://127.0.0.1", "sa", "")); ?IDataSet data = new FlatXmlDataSet(new File("etc/database/race-db.xml")); ?try { ??DatabaseOperation.CLEAN_INSERT.execute(conn, data); ?} finally { ??conn.close(); ?} } |
在 setUpDbUnit 方法中,數據庫的 XML 表示通過 CLEAN_INSERT 命令被插入數據庫。這個 XML 文件有效地插入了六個不同的比賽。因此,GET 的響應將會是一個帶有六個比賽的 XML 文檔。
接下來,可以創建一個測試用例,它可以對 /race URI 調用 HTTP GET、獲取響應 XML 并使用 XMLUnit 的 Diff 類將它比作為控制 XML 文件,如清單 18 所示:
清單 18. 用 XMLUnit 驗證 GET 響應
| 1 2 3 4 5 6 7 8 9 10 11 | @Test public void getRaces() throws Exception { ?Client client = new Client(Protocol.HTTP); ?Response response = ??client.get("http://localhost:8080/racerrest/race/"); ?Diff diff = new Diff(new FileReader( ??new File("./etc/control-xml/control-web-races.xml")), ???new StringReader(response.getEntity().getText())); ?assertTrue(diff.toString(), diff.identical()); } |
control-web-races.xml 文件是預期的來自 Web 服務的 XML 響應。它含有如清單 19 所示的數據:
清單 19. 控制 XML 文件
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | <acme-races> ?<races> ??<race name="Mclean 1/2 Marathon" date="2008-05-12" distance="13.1" id="1"> ???<uri>http://localhost:8080/races/1</uri> ???<description/> ??</race> ??<race name="Reston 5K" date="2008-09-13" distance="3.1" id="2"> ???<uri>http://localhost:8080/races/2</uri> ???<description/> ??</race> ??<race name="Herndon 10K" date="2008-10-22" distance="6.2" id="3"> ???<uri>http://localhost:8080/races/3</uri> ???<description/> ??</race> ??<race name="Leesburg 1/2 Marathon" date="2008-01-02" distance="13.1" id="4"> ???<uri>http://localhost:8080/races/4</uri> ???<description/> ??</race> ??<race name="Leesburg 5K" date="2008-05-12" distance="3.1" id="5"> ???<uri>http://localhost:8080/races/5</uri> ???<description/> ??</race> ??<race name="Leesburg 10K" date="2008-07-30" distance="6.2" id="6"> ???<uri>http://localhost:8080/races/6</uri> ???<description/> ??</race> ?</races> </acme-races> |
現在運行這個測驗當然會引起一系列的失敗,因為還沒有實現 RESTful 服務。同樣要注意,源代碼下載中的 Ant 構建文件包含部署 WAR 文件和啟動與停止 Tomcat 的任務(參見 下載)。這些都是成功運行測試的必要條件。
結果證明滿足 GET 請求其實很簡單。只需要對 Race 域對象調用 findAll 方法,然后將該調用的結果傳入 RaceReporter 的 racesToXml() 方法。因此,您需要更新 RacesResource 實例,引入新的成員變量和構造函數的新的初始化,如清單 20 所示:
清單 20. 不要忘記添加 RaceReporter
| 1 2 3 4 5 6 7 8 9 | public class RacesResource extends Resource { ?private RaceReporter reporter; ?public RacesResource(Context context, Request request, Response response) { ??super(context, request, response); ??this.getVariants().add(new Variant(MediaType.TEXT_XML)); ??this.reporter = new RaceReporter(); ?} } |
現在,實現 GET 請求就很容易了。只要向 getRepresentation 方法中添加三行內容就可以了,如清單 21 所示:
清單 21. 完成 GET request
| 1 2 3 4 5 | public Representation getRepresentation(Variant variant) { ?Collection<Race> races = Race.findAll(); ?String xml = this.reporter.racesToXml(races); ?return new StringRepresentation(xml); } |
信不信由您,僅此而已!
但是等等,不需要部署這個應用程序來測試它么?
部署和驗證
在實際測試 RESTful 服務是否可以返回一個比賽列表之前,您需要先部署應用程序。本節展示了如何部署。
配置 web.xml
幸運的是,部署 Restlet 應用程序再簡單不過了。您只要創建一個普通的 WAR 文件,確保 web.xml 文件適當配置就可以了。
要讓 Restlet 應用程序在 servlet 容器中正常發揮功能,一定要更新 web.xml 文件:
- 正確載入應用程序
- 通過框架的自定義 servlet 傳遞所有需求
因此,您的 web.xml 文件應該如清單 22 所示:
清單 22. 簡單的 web.xml 文件
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | <?xml version="1.0" encoding="UTF-8"?> <web-app id="WebApp_ID" version="2.4" ?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"> ?<display-name>RESTful racing</display-name> ?<context-param> ??<param-name>org.restlet.application</param-name> ??<param-value>RaceApplication</param-value> ?</context-param> ?<servlet> ??<servlet-name>RestletServlet</servlet-name> ??<servlet-class>com.noelios.restlet.ext.servlet.ServerServlet</servlet-class> ?</servlet> ?<servlet-mapping> ??<servlet-name>RestletServlet</servlet-name> ??<url-pattern>/*</url-pattern> ?</servlet-mapping> </web-app> |
本教程的源代碼(參見 下載)含有一個 war 任務,它會自動構建 WAR 文件,以及支持將 WAR 文件部署到 Tomcat 本地實例的構建文件。
可以看到,在清單 22 中的第一部分,org.restlet.application 搭配 Restlet 應用程序的類名 RaceApplication(如果要賦予它一個包名的話,可能需要完全限定那個名稱)。也要注意,文檔最后的部分將所有請求映射到 RestletServlet 類型,該類型先前已被映射到了 com.noelios.restlet.ext.servlet.ServerServlet 類。
RESTfully 測試
現在測試 RESTful Web 服務只要重新運行 清單 18 中的測驗用例。
再看一下測試,可以解釋一些東西。Restlet 的 Client 對象支持基本的 HTTP 命令 GETPUT、POST 和 DELETE。Client 對象可以采取不同的協議的形式 — 在這種情況下只會偶爾依賴 HTTP。
GET 請求已經奏效了(參見圖 1),所以你可以寫另外一個測試。這一次,要完成 POST 所有期望的行為;換句話說,通過 RacesResource 類測試新比賽的創建。
圖 1. 在瀏覽器中查看 RESTful GET 請求
要測試 POST,您需要用相關的信息來形成一個 XML 請求文檔,并確保服務成功發回響應。當然,事實證明寫這樣的一個測試是相當簡單的。只需向現有 JUnit 類添加一些額外的代碼,如清單 23 所示:
清單 23. createRace 測驗用例
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | private static String raceXML = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" + ?"<acme-races>\n" + ?" <race name='Limerick 2008 Half' date='2008-05-12' distance='13.4'>\n" + ?" <description>erin go brach</description>\n" + ?" </race>\n" + ?"</acme-races>"; @Test public void createRace() { ?Form form = new Form(); ?form.add("data", this.raceXML); ?Representation rep = form.getWebRepresentation(); ?Client client = new Client(Protocol.HTTP); ?Response response = ??client.post("http://localhost:8080/racerrest/race/", rep); ?assertTrue(response.getStatus().isSuccess()); |
正如您所看到的,清單 23 迅速形成了一個表示 XML 文檔的 String。在這里,我在創建一個稱為 Limerick 2008 Half 的新比賽。然后它用 Restlet 框架的 Client 對象將這個文檔傳給服務器。最后,它確保返回一個成功指示。
現在運行測驗。失敗了,是么?那是因為您還沒有實現 POST 請求代碼,這是下一小節的任務。
通過 RESTful 服務創建比賽
通過 RESTful Web 服務創建比賽只需要幾個步驟就可以完成:接受 XML 文檔、解析它、在底層數據庫中創建一個新的 Race 實例,最后返回一個表示事務結果的響應。本小節涵蓋了這幾步。
處理 POST 請求
要通過 REST 實現創建行為,需要邏輯處理 POST 請求。因此,在 RacesResource 類中,必須覆蓋兩個方法:allowPost() 和 post()。
post() 方法完成這里的所有工作。它采用了一個 Representation 實例,可以從該實例中獲取傳送的數據。回憶一下 清單 23 中的 createRace 測試用例將 XML 文檔與一個名稱相關聯:data。因此,通過 Restlet 框架的 Form 對象,可以獲取一個表示傳入的 XML 的 String,然后您可以將它傳入所提供的 RaceConsumer 對象。這個對象很容易接受 XML 文檔,并會相應地操作底層數據庫。
如果事務有效的話,然后您將要相應地用一個成功的響應進行回應;無效的話,你將需要回應一個失敗消息。
繼續并覆蓋 allowPost() 和 post(),如清單 24 所示:
清單 24. 覆蓋 POST 方法
| 1 2 3 4 5 | public boolean allowPost() { ?return true; } public void post(Representation representation) {} |
由于您將使用 RaceConsumer 對象,所以要將其添加為 RacesResource 類的一個成員變量,并在構造函數中將其初始化。相應地更新對象,如清單 25 所示:
清單 25. 添加 RaceConsumer 類
| 1 2 3 4 5 6 7 8 9 10 11 | public class RacesResource extends Resource { ?private RaceReporter reporter; ?private RaceConsumer consumer; ?public RacesResource(Context context, Request request, Response response) { ??super(context, request, response); ??this.getVariants().add(new Variant(MediaType.TEXT_XML)); ??this.reporter = new RaceReporter(); ??this.consumer = new RaceConsumer(); ?} } |
接下來,確保您的 post() 方法如清單 26 所示:
清單 26. 實現 post()
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | public void post(Representation representation) { ?Form form = new Form(representation); ?String raceXML = form.getFirstValue("data"); ?Representation rep = null; ?try { ??long id = this.consumer.createRace(raceXML); ??getResponse().setStatus(Status.SUCCESS_CREATED); ??rep = new StringRepresentation(raceXML, MediaType.TEXT_XML); ??rep.setIdentifier(getRequest().getResourceRef().getIdentifier() + id); ?} catch (Throwable thr) { ??getResponse().setStatus(Status.SERVER_ERROR_INTERNAL); ??rep = new StringRepresentation("there was an error creating the race", ????MediaType.TEXT_PLAIN); ?} ?getResponse().setEntity(rep); } |
正如您所看到的,post 方法內容很多;然而,仔細檢查之后,沒有看上去那么復雜。傳入的 XML 是通過 Form 對象獲取的。然后 XML(以一個 String 的形式)會被傳入 consumer 實例的 createRace() 方法。如果一切都奏效的話(換句話說,比賽被持久化),會生成一個包含成功狀態的響應,然后重新散列傳入的 XML 及由此而生的 URI(即,race/43,此處 43 為新創建的比賽的 id)。
如果進展得不是很順利的話,本質上會是相同的過程,不同之處是失敗消息會返回失敗狀態:創建失敗,無 URI 返回。
繼續并重新運行 createRace 測試。假設您已經重新部署了 RESTful Web 應用程序,一切都會順利運行。
結束語
本教程實現的僅僅是中等程度的 Acme Racing 需求。但在此過程中,您已經了解到了使用 Restlets 是很方便的。整個過程中最困難的部分就是實現邏輯 RESTful API。本教程的源代碼為提高您的學習樂趣提供了大量特性(參見 下載)。
詩人亞歷山大·蒲柏曾說過 “保持簡潔要遠遠優于費盡心思營造出來的復雜?!?對于 REST 來說這句話在合適不過了。記住 REST 是一種思維方式 — 是設計基于命名資源而非消息的松耦合應用程序的一種風格。通過借助已經驗證了的成功的 Web 基礎設施,REST 使得這些應用程序易于設計和實現。而且 REST 應用程序具有良好的擴展性。
本教程只涉及到了 Restlet 框架特性的冰山一角,但不要讓它欺騙了您。這個框架可以做很多事,包括在需要時添加安全設施。Restlet 是一種編碼享受,只要查看一些 Restlet 代碼,就可以很容易地理解代碼庫。
艾伯特愛因斯坦曾經說過,“所有事物都應該盡量保持簡單,但不可過分簡化?!?我認為 Restlet 框架和 REST 本身例證了這個名言的智慧,希望您可以同意我的看法。
下載資源
- 使用依賴庫的樣例代碼 (j-rest.zip | 19.5MB)
- 未使用依賴庫的樣例代碼 (j-rest2.zip | 19KB)
總結
以上是生活随笔為你收集整理的构建 RESTful Web 服务的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: REST,Web 服务,REST-ful
- 下一篇: 基于 REST 的 Web 服务:基础