restlet

Restlet項目為“建立REST概念與Java類之間的映射”提供了一個輕量級而全面的框架。它可用於實現任何種類的REST式系統,而不僅僅是REST式Web服務;而且,事實證明它自從2005年誕生之時起,就是一個可靠的軟體。

基本概念

Restlet在術語上參照了Roy Fielding博士論文在講解REST時採用的術語,如:資源(resource)、表示(representation)、連線器(connector)、組件(component)、媒體類型(media type)、語言(language),等等。這些術語你應該不會陌生。Restlet增加了一些專門的類(如Application、Filter、Finder、Router和Route),用以簡化restlets的彼此結合,以及簡化把收到的請求(incoming requests)映射為處理它們的資源。

restlet restlet

圖12-1:Restlet的類層次結構

抽象類Uniform及其具體子類Restlet,是Restlet的核心概念。正如其名稱所暗示的,Uniform暴露一個符合REST規定的統一接口(uniform interface)。雖然該接口是按HTTP統一接口定義的,但它也可用於其他協定(如FTP和SMTP)。

handle是一個重要的方法,它接受兩個參數:Request和Response。正如你可以從圖12-1中看到的,每個暴露於網上的調用處理者(call handler)(無論作為客戶端還是服務端)都是Restlet的一個子類——也就是說,它是一個restlets——並遵守這個統一接口。由於有統一接口,restlets可以非常複雜的方式組合在一起。

Restlet支持的每一個協定都是通過handle方法暴露的。這就是說,HTTP(伺服器和客戶端)、HTTPS、SMTP,以及JDBC、檔案系統,甚至類載入器(class loaders)都是通過調用handle方法來操作的。這減少了開發者需掌握的APIs的數量。

過濾、安全、數據轉換及路由是“通過把Restlet的子類鏈起來”進行處理的。Filters可以在處理下個restlet調用之前或之後進行處理。Filters實例的工作方式與Rails過濾器差不多,只不過Filters實例跟其他Restlet類一樣回響handle方法,而不是具有一個專門的API。

一個Router restlet有許多附屬的Restlet對象,它把每個收到的協定調用(incoming protocol call)路由給適當的Restlet處理器。路由(routing)通常是根據目標URI進行的。跟Rails不同的是,Restlet沒有對資源層次結構(resource hierarchy)作URI規則限定,所以可以隨意設定想要的URI,只要對Routers作相應編程就行了。

除了這一常見用途,Router還可用於其他用途。你可以用一個Router在多台遠程機器之間以動態負載均衡的方式來轉發調用。即使在這種複雜的情況下,它也一樣回響Restlet的統一接口,並且可成為一個更大路由系統中的一個組件。VirtualHost類(Router的一個子類)使我們可以在同一台物理主機上運行多個具有不同域名的套用。在過去,你要引入一個前端Web伺服器(如Apache的httpd)才能實現此功能;而用Restlet的話,它只是另一個回響統一接口的Router實現。

一個Application對象能夠管理一組restlets,並提供常見的服務,比方說對壓縮的請求進行透明解碼,或者利用method查詢參數在重載的POST(overloaded POST)之上實現PUT和DELETE請求。最後,Component對象可以包含並編配(orchestrate)一組Connectors、VirtualHosts及Applications(作為獨立Java套用運行的,或者嵌入在一個更大系統(如J2EE環境)中的)。

在第6章,我向你介紹了“把一個問題劃分為一組回響HTTP統一接口的資源”的步驟。在第7章,為了處理Ruby on Rails的簡單化假設(simplifying assumptions),我對該步驟作了相應的調整。因為Restlet沒有做簡單化假設(simplifying assumptions),所以我們無須對此步驟進行修改。它可以實現任何REST式系統。如果你剛好想實現一個REST式面向資源的Web服務,可以按願意的方式來組織和實現這些資源。Restlet確實提供了一些便於創建面向資源的套用的類。其中特別值得一提的是Resource類,它可作為你所有套用資源的基礎。

我在本書中一直用URI模板作為一組URIs的簡化表達(見第9章)。Restlet用URI模板來進行URI與資源的映射。假如用Restlet來實現第7章那個社會性書籤服務的話,它也許要指定一個代表特定書籤的URI:

/users/{username}/bookmarks/{URI}你可以在把Resource子類附加到Router上時使用這種語法。假如你不相信真會這么好的話,可以等到下一節,那時我會實際實現部分書籤服務。

客戶端

在示例2-1中,你看到的是一個從Yahoo!搜尋服務獲取XML搜尋結果的Ruby客戶端。示例12-3是一個用Java參照Restlet 1.0實現的具有同樣功能的客戶端。要確保把以下JAR包寫在你的classpath中,才能成功編譯並運行接下來的例子:

org.restlet.jar(Restlet API) com.noelios.restlet.jar (Noelios Restlet Engine核心) com.noelios.restlet.ext.net.jar (基於JDK的HttpURLConnection的HTTP客戶端連線器) 這些JAR包可以在Restlet發布包中的lib目錄里找到。要確保你的Java環境支持Java SE 5.0(或更高)版本。如果你確實需要的話,可以用Retrotranslator輕易地把Restlet代碼反移植(backport)到J2SE 4.0版上去。

示例12-3:Yahoo!搜尋服務的一個Restlet客戶端

// YahooSearch.javaimport org.restlet.Client;import org.restlet.data.Protocol;import org.restlet.data.Reference;import org.restlet.data.Response;import org.restlet.resource.DomRepresentation;import org.w3c.dom.Node;/** * 用返回XML的Yahoo!搜尋服務來搜尋Web */public class YahooSearch { static final String BASE_URI = "http://api.search.yahoo.com/WebSearchService/V1/webSearch"; public static void main(String[] args) throws Exception { if (args.length != 1) { System.err.println("You need to pass a term to search"); } else { // 獲取一個資源,即一個包含搜尋結果的XML文檔 String term = Reference.encode(args[0]); String uri = BASE_URI + "?appid=restbook&query=" + term; Response response = new Client(Protocol.HTTP).get(uri); DomRepresentation document = response.getEntityAsDom(); pr = "/ResultSet/Result/Title"; for (Node node : document.getNodes(expr)) { System.out.println(node.getTextContent()); } } }} 跟示例2-1一樣,你可以在執行這個類時把一個搜尋關鍵字作為命令行參數傳給它。比如像下面這樣:

$ java YahooSearch xslt XSL Transformations (XSLT) The Extensible Stylesheet Language Family (XSL) XSLT Tutorial ...該示例證明了“用Restlet從Web服務獲取XML數據,並用標準工具處理它”是極其簡單的事。Yahoo!資源的URI是用一個常量和用戶提供的搜尋關鍵字構造而成的。客戶端連線器(client connector)是用HTTP協定來初始化的。XML文檔是通過get方法獲得的,該方法對應於HTTP統一接口的GET方法。當調用返回時,程式將得到一個DOM表示。跟前面的Ruby例子一樣,XPath是對XML進行查詢的最簡單方式。

跟前面的Ruby例子一樣,這個程式也忽略了XML文檔里的XML名稱空間(namespaces)。Yahoo!為整個文檔採用名稱空間urn:yahoo:srch,但我是直接引用標籤的,比方說,我用ResultSet,而不是urn:yahoo:srch:ResultSet。前面的Ruby例子忽略名稱空間,是因為Ruby的默認XML解析器不支持名稱空間。Java的XML解析器支持名稱空間,而且Restlet API令正確處理名稱空間變得更加容易。雖然對上面那個簡單例子來說,它們區別不大,但支持名稱空間可以避免一些因名稱空間而導致的微妙的問題。

當然,若一直用urn:yahoo:srch:ResultSet,是比較煩人的。Restlet API可以容易地把一個簡短前綴跟一個名稱空間進行關聯,然後就可以在XPath表達式中使用這個簡短前綴而不是整個名稱空間了。示例12-4對示例12-3後半部分代碼作了改動,它使用了帶名稱空間的Xpath,這樣就不會把來自Yahoo!的ResultSet標籤與來自其他名稱空間的標籤搞混了。

示例12-4:支持名稱空間的文檔處理代碼

DomRepresentation document = response.getEntityAsDom(); // 把該名稱空間與前綴‘y’關聯起來 document.setNamespaceAware(true); document.putNamespace("y", "urn:yahoo:srch"); // 用XPath找出數據結構中重要部分 String ex "/y:ResultSet/y:Result/y:Title/text()"; for (Node node : document.getNodes(expr)) { System.out.println(node.getTextContent()); }示例2-15是Yahoo!搜尋服務的另一個Ruby客戶端。它請求的是JSON格式(而不是XML格式)的搜尋數據。示例12-5是一個與之功能等價的Restlet客戶端。它通過Restlet里的另兩個JAR檔案獲取JSON支持:

org.restlet.ext.json_2.0.jar(用於JSON的Restlet擴展) org.json_2.0/org.json.jar(JSON官方程式庫) 示例12-5:Yahoo!的JSON搜尋服務的一個Restlet客戶端

// YahooSearchJSON.javaimport org.json.JSONArray;import org.json.JSONObject;import org.restlet.Client;import org.restlet.data.Protocol;import org.restlet.data.Reference;import org.restlet.data.Response;import org.restlet.ext.json.JsonRepresentation;/** * 用返回JSON的Yahoo!搜尋服務來搜尋Web */public class YahooSearchJSON { static final String BASE_URI = "/WebSearchService/V1/webSearch"; public static void main(String[] args) throws Exception { if (args.length != 1) { System.err.println("You need to pass a term to search"); } else { // 獲取一個資源,即一個包含搜尋結果的JSON文檔 String term = Reference.encode(args[0]); String uri = BASE_URI + "?appid=restbook&output=json&query=" + term; Response response = new Client(Protocol.HTTP).get(uri); JSONObject json = new JsonRepresentation(response.getEntity()) .toJsonObject(); // 在JSON文檔中尋找並顯示標題 JSONObject resultSet = json.getJSONObject("ResultSet"); JSONArray results = resultSet.getJSONArray("Result"); for (int i = 0; i < results.length(); i++) { System.out.println(results.getJSONObject(i).getString("Title")); } } }}當你為Yahoo!的Web服務編寫客戶端時,可以選擇表示格式(representation format)。Restlet核心API支持XML,另外還可以通過擴展支持JSON。正如你所預料的那樣,這兩個例子的區別僅僅在於對回響的處理上。JsonRepresentation類可以把回響實體主體(response entity-body)轉換成一個JSONObject實例(而Ruby的JSON庫是把JSON數據結構轉換成一個本地數據結構)。該數據結構只能進行人工遍歷,因為目前JSON中還沒有類似XPath的查詢語言。

服務

接下來的例子會稍微複雜一些。我將向你展示如何設計並實現一個服務端套用。在第7章,我用Ruby on Rails實現了一個書籤管理套用,現在我用Restlet來重新實現其部分功能。為了簡單起見,該套用只支持對用戶及其書籤進行安全的(safe)操作。

Java包結構是這樣的:

org restlet example book rest ch7 -Application -ApplicationTest -Bookmark -BookmarkResource -BookmarksResource -User -UserResource也就是說,Bookmark等類都在org.restlet.example.book.rest.ch7包里。

我不打算在此展示完整的代碼。如果需要,你可以去本書的官方網站(http://www.oreilly. com/catalog/9780596529260),那裡提供了本書的所有示例程式代碼。你也可以在restlet.org(http://www.restlet.org)上找到本例的完整代碼。如果你已經下載了Restlet的話,那么也可以在src/org/restlet.example/org/restlet/example/book/rest目錄里找到本節的示例代碼。

我將從一些簡單的代碼開始。示例12-6是Application.main方法,它用來建立Web伺服器,並開始處理請求。

示例12-6:Application.main方法:建立Web伺服器

public static void main(String... args) throws Exception { // 用HTTP伺服器連線器創建一個組件 Component comp = new Component(); comp.getServers().add(Protocol.HTTP, 3000); // 把套用附加到默認主機上,並啟動 comp.getDefaultHost().attach("/v1", new Application()); comp.start();}

設計

由於Restlet未對資源設計作特別的限制,所以你完全可以根據ROA的設計原則來進行資源類(resource classes)及URIs的設計。在第7章,我要圍繞“Rails的基於控制器的架構”來進行設計;而這裡,我不需要圍繞Restlet架構來進行設計。圖12-2 展示了URI是如何經由Router映射到資源,再映射到下層restlet類的。

restlet restlet

圖12-2:社會性書籤套用的Restlet架構

為了理解如何用Java代碼實現這些映射,我們來看一下Application類及它的createRoot 方法(見示例12-7)。它跟示例7-3所示的Rails routes.rb檔案在功能上是等價的。

示例12-7:Application.createRoot方法:實現URI模板到restlet的映射

public Restlet createRoot() { Router router = new Router(getContext()); // 為用戶資源增加路由 router.attach("/users/{username}", UserResource.class); // 為用戶的書籤資源增加路由 router.attach("/users/{username}/bookmarks", BookmarksResource.class); // 為書籤資源增加路由 Route uriRoute = router.attach("/users/{username}/bookmarks/{URI}", BookmarkResource.class); uriRoute.getTemplate().getVariables() .put("URI", new Variable(Variable.TYPE_URI_ALL));}在我創建一個Application對象(比如像示例12-6中的那樣)時,這段代碼便會運行。它會在資源類UserResource與URI模板“/users/(username)”之間建立起清晰而直觀的映射關係。Router先拿請求的目標URI跟URI模板(URI templates)進行比較,然後把請求轉發給一個新建的相應的資源類實例。模板變數的值被存放在請求的屬性地圖(attributes map)里(跟Rails例子中的params地圖類似),以便於在Resource代碼中使用。這既有效,又易於理解;當你事隔很久再回顧代碼時,這很有幫助。

表示

假定一個客戶端向URI http://localhost:3000/v1/users/jerome發出GET請求。我有一個監聽本地主機3000連線埠的Component對象,和一個隸屬於 /v1 的Application對象。該Application有一個Router和一組Route對象,這些Route對象正等待著跟各個URI模板匹配的請求。 URI路徑片段“/users/jerome”跟模板“/users/{username}”相匹配,而該模板的Route是與UserResource類(大致等價於Rails UsersController類)相關聯的。

Restlet通過初始化一個新的UserResource對象,並調用它的handleGet方法來處理該請求。示例12-8是UserResource類的構造方法。

示例12-8:UserResource類的構造方法

/**

*構造方法

*

* @param context

* 上級上下文

* @param request

* 要處理的請求

* @param response

* 要返回的回響

*/

public UserResource(Context context, Request request, Response response) {

super(context, request, response);

this.userName = (String) request.getAttributes().get("username");

ChallengeResponse cr = request.getChallengeResponse();

this.login = (cr != null) ? cr.getIdentifier() : null;

this.password = (cr != null) ? cr.getSecret() : null;

this.user = findUser();

if (user != null) {

getVariants().add(new Variant(MediaType.TEXT_PLAIN));

}

}

至此,這個架構已經建立了一個Request對象,它包含了我所需要的關於請求的所有信息。username屬性來自URI,認證證書來自請求的Authorization報頭。我還調用findUser方法來根據認證證書在資料庫中查找用戶(為節省篇幅,我就不在此展示findUser方法的代碼了)。這些工作在第7章都是由Rails過濾器完成的。

在框架把一個UserResource實例化後,它會對資源對象調用適當的handle方法。HTTP統一接口中的每一個方法,都有一個對應handle方法。 在這個例子中,Restlet架構最後的任務是調用UserResource.handleGet。

由於我沒有定義UserResource.handleGet這個方法,所以它將具有繼承Resource. handleGet方法的行為。HandleGet的默認行為是找到最符合客戶端要求的資源的表示。客戶端通過內容協商(content-negotiation)來表達它的要求。Restlet通過Accept報頭的值來決定返回哪個表示。由於這裡只有一個表示格式,所以客戶端的要求不起作用。這是由getVariants和getRepresentation方法處理的。由於在上述構造方法中把text/ plain定義為唯一支持的表示格式,所以我的getRepresentation方法的實現是很簡單的(見示例12-9)。

示例12-9:UserResoure.getRepresentation:構造一個用戶的表示

@Override

public Representation getRepresentation(Variant variant) {

Representation result = null;

if (variant.getMediaType().equals(MediaType.TEXT_PLAIN)) {

// 創建一個文本表示

StingBuilder sb=new StringBuilder();

sb.append("------------\n"); sb.append("User details\n");

sb.append("------------\n\n");

sb.append("Name: ").append(this.user.getFullName()).append('\n');

sb.append("Email: ").append(this.user.getEmail()).append('\n');

result = new StringRepresentation(sb);

}

return result;

}

雖然這只是一個資源的一個方法,但其他資源,以及UserResource的其他HTTP方法的工作原理都差不多,比如:對用戶的PUT請求將被路由給UserResource.handlePut,等等。正如我前面所提到的,這裡的代碼只是社會性書籤套用所有代碼的一部分;所以,如果你有興趣進一步學習的話,可以去下載一個完整的示例代碼來閱讀。

一般,只要關注Application和Router代碼一次就行,因為一個Router可用於你的所有資源。

測試

Application類實現了運行社會性書籤服務的HTTP伺服器。你需要在classpath中加入以下JAR檔案:

org.restlet.jar com.noelios.restlet.jar com.noelios.restlet.ext.net.jar org.simpleframework_3.1/org.simpleframework.jar com.noelios.restlet.ext.simple_3.1.jar com.db4o_6.1/com.db4o.jar 這些JAR包可以在Restlet發布包中的lib目錄里找到。有兩點需要注意:第一,Web伺服器的實際工作是由一個非常緊湊的、基於Simple框架的HTTP伺服器連線器來處理的;第二,我們是用強大的db4o對象資料庫(而不是關係資料庫)來存儲領域對象(用戶和書籤)的。在編譯好所有示例檔案後,運行org.restlet.example.book.rest.ch7. Application,它將作為伺服器的端點(endpoint)。

ApplicationTest類為服務提供了一個客戶端接口。它採用上節描述的Restlet客戶端類來添加和刪除用戶和書籤。它是通過HTTP統一接口進行工作的:用PUT請求創建用戶和書籤,用DELETE請求刪除用戶和書籤。

在命令行下運行ApplicationTest類,你將得到以下訊息:

Usage depends on the number of arguments:- Deletes a user : userName, password- Deletes a bookmark : userName, password, URI- Adds a new user : userName, password, "full name", email- Adds a new bookmark : userName, password, URI, shortDescription, longDescription, restrict[true / false]你可以用這個程式來添加一些用戶,並增加一些書籤。然後,你就可以在Web瀏覽器中通過訪問適當的URI(如http://localhost:3000/v1/users/jerome等)來瀏覽用戶書籤的HTML表示了。

其他

Restlet項目受到Servlet API、JSP(Java Server Pages)、HttpURLConnection及Struts等Web開發技術的影響。該項目的主要目標是:在提供同等功能的同時,儘量遵守Roy Fielding博士論文中所闡述的REST的目標。它的另一個主要目標是:提出一個既適於客戶端套用又適於服務端的套用的、統一的Web視圖。

Restlet的思想是:HTTP客戶端與HTTP伺服器之間的差別,對架構來說無所謂。一個軟體應可以既充當Web客戶端又充當Web伺服器,而無須採用兩套完全不同的APIs。

Restlet包括Restlet API和Noelios Restlet Engine(NRE)兩部分,NRE是對Restlet API的一種參考實現。這種劃分,使得不同實現可以具有同樣的API。NRE包括若干HTTP伺服器連線器(HTTP server connector),它們都是基於Mortbay的Jetty、Codehaus的AsyncWeb,以及Simple框架這些流行的HTTP Java開源項目的。它甚至提供一個適配器(adapter),使你可以在標準Servlet容器(如Apache Tomcat)內部署一個Restlet套用。

Restlet還提供兩個HTTP客戶端連線器(HTTP client connector)。它們一個是基於官方的HttpURLConnection類,一個是基於Apache的HTTP客戶端庫。還有一個連線器允許你容易地按REST風格通過XML文檔來處理JDBC源(source);此外,一個基於JavaMail API的SMTP連線器允許你傳送內容為XML的Email。

Restlet API包括一些能夠創建基於字元串、檔案、流(stream)、通道(channel)及XML文檔的表示(representation),它支持SAX、DOM及XSLT。使用FreeMaker或Apache Velocity模板引擎,你可以很容易地創建基於JSP式模板的表示(representations)。你甚至可以像普通Web伺服器那樣,用一個支持內容協商(content negotiation)的Directory類來返回靜態檔案與目錄。

簡單性(simplicity)和靈活性(flexibility)是貫穿整個框架的設計原則。Restlet API旨在把HTTP、URI及REST的概念抽象成一系列類(classes),同時又不把低層信息(如原始HTTP報頭)完全隱藏起來。

相關詞條

熱門詞條

聯絡我們