rest概述

轉載:http://my.oschina.net/javagg/blog/3254

關於本指南

本指南的翻譯工作經過了Restlet社區的官方授權,cleverpig作爲貢獻者完成了本文的翻譯和整理工作。在此發佈Matrix社區試讀版的目的是爲了讓更多的技術愛好者閱讀並提出翻譯中的不足之處,以提高本指南的質量,以期修改後正式發佈。

Servlet的限制

在2003年末,Jetty Web容器的作者、Servlet規範的貢獻者:Greg Wilkins在其博客上對Servlet的問題進行了如下總計:

    * 沒有對協議與應用之間的關係進行清洗的劃分。
    * 由於在設計Servlet時存在對阻塞IO的假設,因此不能充分利用非阻塞NIO機制。
    * 所有的Servlet Web容器對於某些應用來講是過度設計的。

他提出構思新的API規範,使其能夠真實地脫離協議,並定義能夠暴露內容和元數據的contentlets。這些想法就是Restlet項目創建的靈感源泉。在之後的文章中,Greg Wilkins解釋了爲什麼當前Servlet API限制非阻塞NIO API得到高效使用的詳細理由:這種傳統的用法針對每個HTTP請求都創建獨立的線程進行處理。並提出了他對下一代Servlet技術的設想

另一個主要問題就是Servlet API鼓勵應用開發者在應用或者用戶會話級別直接將session狀態保存於內存中,儘管這看上去不錯,但它造成了Servlet容器擴展性和高可用性的主要問題。爲了克服這些問題,就必須實現複雜的負載均衡、session複製、持久化機制。這導致了可擴展性必然成爲災難。

Restlet簡介

當複雜核心化模式日趨強大之時,面向對象設計範例已經不總是Web開發中的最佳選擇,Java開發者需要認識到這一點,並且在開發新的Web服務端或是AJAX Web客戶端時開始思考更加RESTfully的設計。Restlet這個開源項目爲那些要採用REST結構體系來構建應用程序的Java開發者提供了一個具體的解決方案。它的非常簡單易用的功能和RESTfully的Web框架,這使其成爲了Web2.0開發中的又一利器。好吧,朋友們,下面就讓我們開始Restlet探索之旅吧!

1. 註冊一個Restlet實現

Restlet框架由兩部分構成。第一部分是"Restlet API", 這個中立的API完美地實現了REST概念並簡化了客戶端和服務端應用的調用處理。在使用它之前,我們還需要一個支持此API的Restlet實現。Restlet的諸多實現可以通過開源項目或者商業產品獲得。


API與實現的分離和Servlet API與web容器的分離(就像Jetty或Tomcat)、JDBC API與相應JDBC驅動的分離非常類似。目前,"Noelios Restlet Engine" (縮寫爲NRE)是Restle tAPI的參考實現之一。當下載Restlet發佈版本時,API和NRE就綁定在一起,以備隨時使用。如果你需要使用不同的實現,那麼只需要添加JAR 文件到classpath,並刪除com.noelios.restlet.jar這個NRE的JAR文件即可。

API實現的註冊過程是完全自動的,如果你對此存在疑問,那麼請參考JAR規範。當完成實現裝載工作後,它將自動回調org.restlet.util.Engine.setInstance()方法,來進行自注冊。

2. 接收Web頁面的內容

正如我們在Restlet介紹中所提到的,Restlet框架即是一個客戶端,又是一個服務端框架。例如,NRE能夠簡單地通過它的HTTP客戶端 connector(連接器)訪問遠程資源。在REST中,connector是一種軟件元素,它使兩個component(組件)之間能夠進行通訊,其典型的實現方式是通過某種網絡協議完成通訊。NRE提供了多種客戶端connector實現,這些實現都基於現存的開源項目。在connector一節中,列舉出了所有可用的客戶端、服務端connector,並解釋瞭如何使用和配置它們。

下面,我們將獲取一個現存資源的表示法(representation )並將其輸出在JVM控制檯:

// Outputting the content of a Web page
Client client = new Client(Protocol.HTTP);
client.get("http://www.restlet.org").getEntity().write(System.out);



請注意上面的示例使用了最簡單的方式:通過通用的客戶端類(generic Client class)調用。更加靈活的方式是創建一個新的Request對象,然後請求客戶端去處理它。下面的示例展示瞭如何在調用時設置首選項(例如 referrer URI)。當然,也可以是接收回應時的首選語言或者媒體類型:

// Prepare the request
Request request = new Request(Method.GET, "http://www.restlet.org");
request.setReferrerRef("http://www.mysite.org");

// Handle it using an HTTP client connector
Client client = new Client(Protocol.HTTP);
Response response = client.handle(request);

// Write the response entity on the console
Representation output = response.getEntity();
output.write(System.out);



3. 偵聽瀏覽器

現在,我們將瞭解一下Restlet框架是如何偵聽客戶端請求並作出迴應的。這裏,我們選用了NRE HTTP服務端connector(例如基於Jetty的HTTP服務端connector),返回簡單的字符串表達式“Hello World!”。請注意在更加實際的應用中,我們可以創建一個獨立的類,此類繼承自Restlet類,而不依靠這裏的匿名內部類。

Restlet類與Servlet非常相似,並且在RESTful應用中處理調用時提供了有限的幫助。我們後面將看到一個提供了一些特定子類的框架,它能夠更抽象、簡單地進行處理。下面讓我們先看一個簡單的示例:

// Creating a minimal Restlet returning "Hello World"
Restlet restlet = new Restlet() {
    @Override
    public void handle(Request request, Response response) {
        response.setEntity("Hello World!", MediaType.TEXT_PLAIN);
    }
};

// Create the HTTP server and listen on port 8182
new Server(Protocol.HTTP, 8182, restlet).start();



如果你運行並啓動服務端,那麼你可以打開瀏覽器輸入http://localhost:8182。實際上,輸入任何的URI都可以工作,你也可以嘗試一下http://localhost:8182/test/tutorial。值得注意的是,如果你從另一臺服務器上測試服務端,那麼就需要將localhost替換爲服務器的IP地址或者它的域名。

4. REST架構概述

讓我們先從REST的視角審視一下典型的web架構。在下面的圖表中,端口代表了connector,而後者負責component之間的通訊(組件在圖中被表示爲大盒子)。鏈接代表了用於實際通訊的特定協議(HTTP,SMTP等)。
image

請注意,同一個component能夠具有任何數量的客戶端/服務端connector。例如,Web服務器B就具有一個用於迴應用戶代理組件(User Agent component)的服務端connector,和多個發送請求到其它服務端的客戶端connector。

5. Component、virtual hosts和applications

另外,爲了支持前面所表述的標準REST軟件架構元素,Restlet框架也提供了一套類:它們極大地簡化了在單一JVM中部署多個應用的工作。其目的在於提供一種RESTful、可移植的、比現存的Servlet API更加靈活的框架。在下面的圖表中,我們將看到三種Restlet,它們用於管理上述複雜情況:Components能夠管理多個Virtual Hosts和Applications。Virtual Hosts支持靈活的配置,例如同一個IP地址能夠分享多個域名、使用同一個域名實現跨越多個IP地址的負載均衡。最後,我們使用應用去管理一套相關的 Restlet、Resource、Representations。另外,應用確保了在不同Restlet實現、不同Virtual Hosts之上的可移植性和可配置性。這三種Restlet的協助爲我們提供了衆多的功能:譬如訪問日誌、請求自動解碼、配置狀態頁設置等。image

爲了展示這些類,讓我們嘗試一個簡單的示例。首先,我們創建一個component,然後在其上添加一個HTTP服務端connector,並偵聽 8182端口。接着創建一個簡單的、具有追蹤功能的Restlet,將它放置到組件默認的Virtual Hosts上。這個默認的主機將捕捉那些沒有路由到指定Virtual Hosts的請求(詳見Component.hosts屬性)。在後面的一個示例中,我們還將介紹應用類的使用方法。請注意,目前你並不能在控制檯輸出中看到任何的訪問日誌。

// Create a new Restlet component and add a HTTP server connector to it
Component component = new Component();
component.getServers().add(Protocol.HTTP, 8182);

// Create a new tracing Restlet
Restlet restlet = new Restlet() {
    @Override
    public void handle(Request request, Response response) {
        // Print the requested URI path
        String message = "Resource URI  : " + request.getResourceRef()
                + '
' + "Root URI      : " + request.getRootRef()
                + '
' + "Routed part   : "
                + request.getResourceRef().getBaseRef() + '
'
                + "Remaining part: "
                + request.getResourceRef().getRemainingPart();
        response.setEntity(message, MediaType.TEXT_PLAIN);
    }
};

// Then attach it to the local host
component.getDefaultHost().attach("/trace", restlet);

// Now, let's start the component!
// Note that the HTTP server connector is also automatically started.
component.start();


讓我們通過在瀏覽器中輸入http://localhost:8182/trace/abc/def?param=123來進行測試,得到測試結果如下:

Resource URI  : http://localhost:8182/trace/abc/def?param=123
Root URI      : http://localhost:8182/trace
Routed part   : http://localhost:8182/trace
Remaining part: /abc/def?param=123



6. 爲靜態文件提供服務

你遇到過提供靜態頁面(類似Javadocs)服務的web應用?如果正在使用,那麼可以直接編寫一個Directory類,而無需爲它建立Apache服務。請見下面如何使用:

// Create a component
Component component = new Component();
component.getServers().add(Protocol.HTTP, 8182);
component.getClients().add(Protocol.FILE);

// Create an application
Application application = new Application(component.getContext()) {
    @Override
    public Restlet createRoot() {
        return new Directory(getContext(), ROOT_URI);
    }
};

// Attach the application to the component and start it
component.getDefaultHost().attach("", application);
component.start();


正如你所注意到的,我們通過傳遞應用的父組件上下文(context)的方式來實例化應用,而不是在第5章中提到的代碼那樣簡單。而其主要原因是應用在分配客戶端請求時,請求的分配工作需要客戶端connector來完成,而後者被component所控制,並在所有被包含其中的應用之間貢獻。

爲了運行此示例,你需要爲ROOT_URI提供一個有效值,該值依賴於你的Restlet安裝路徑。默認情況下,它被設置爲"file:///D: /Restlet/www/docs/api/"。請注意,這裏不需要任何附加的配置。如果你希望自定義在文件擴展名和元數據(metadata,包括媒體類型、語言、編碼等)之間的映射,或是提供一個與衆不同的索引名,你可以使用應用的“metadataService”屬性。

7. 訪問日誌

有目的地記錄web應用的活動是一種常見的需求。Restlet組件能夠在默認的情況下生成類似Apache風格的日誌、甚至自定義日誌。通過使用 JDK內置的日誌功能,logger能夠配置爲像任何標準JDK日誌那樣過濾信息、對它們進行重新格式化或者發送它們到指定位置。並且支持日誌的循環(rotation);細節請查看java.util.logging包。

值得注意的是,你能夠通過修改component的"logService"屬性來爲java.util.logging框架自定義logger名。如果希望完全掌控日誌的配置,你需要通過設置系統屬性來聲明一個配置文件:

System.setProperty("java.util.logging.config.file", "/your/path/logging.config");



關於配置文件格式的細節,請查看JDK的LogManager類。

8. 顯示錯誤頁

另外一個常見的需求是:在調用處理過程中某些期望結果沒有出現時,能夠自定義返回的狀態頁面。也許它是某個資源沒有找到或者一個可接受的表示是無效的。在這種情況下,或者遇到任何無法處理的異常時,Application或者Component將自動提供一個默認的狀態頁面。此服務與 org.restlet.util.StatusService類相關聯,並可以作爲被稱爲“statusService”的Application或者 Component的屬性而被訪問。

爲了自定義默認的信息,你只需要簡單地創建StatusService類的子類,並覆蓋其getRepresentation(Status, Request, Response)方法。然後設置這個類的實例爲指定的“statusService”屬性即可。

9. 對敏感資源的訪問保護

當你需要保護對某些Restlet的訪問時,可以使用下面的方法:一種通用的方法是依靠cookie來識別客戶端(或者客戶端session),並根據你的應用狀態檢查給定的用戶ID或者session ID,從而判斷次訪問是否被允許。Restlet通過訪問Request或者Response中的CookieCookieSetting對象支持cookie。

另一種方法是基於標準HTTP認證機制。Neolios Restlet引擎目前允許基於簡單HTTP方案的證書發送、接收和基於Amazon Web服務方案的證書發送。

當接收到調用時,開發者能夠通過Request.challengeResponse.identifier/secret類中的Guard filter(保護過濾器)使用已經解析好的證書。過濾器是一種特殊的Restlet,它能夠在調用相應Restlet之前進行預處理,或者在相應 Restlet調用返回後進行後期處理。如果你熟知Servlet API,這裏的過濾器概念和Servlet API中的Filter接口非常接近。看一下我們如何修改從前的代碼來對目錄訪問進行訪問保護:

// Create a Guard
Guard guard = new Guard(getContext(),
        ChallengeScheme.HTTP_BASIC, "Tutorial");
guard.getSecrets().put("scott", "tiger".toCharArray());

// Create a Directory able to return a deep hierarchy of files
Directory directory = new Directory(getContext(), ROOT_URI);
guard.setNext(directory);
return guard;


image

請注意:認證和授權的最終結果是完全可定製的,這隻需要通過authenticate()和authorize()方法便可完成。任何自定義的機制都能夠被用來檢查給定的證書是否有效、通過認證的用戶是否被授權繼續訪問相應Restlet。下面是我們簡單地硬編碼了用戶、密碼對。爲了測試,我們使用了客戶端Restlet API:

// Prepare the request
Request request = new Request(Method.GET, "http://localhost:8182/");

// Add the client authentication to the call
ChallengeScheme scheme = ChallengeScheme.HTTP_BASIC;
ChallengeResponse authentication = new ChallengeResponse(scheme,
        "scott", "tiger");
request.setChallengeResponse(authentication);

// Ask to the HTTP client connector to handle the call
Client client = new Client(Protocol.HTTP);
Response response = client.handle(request);

if (response.getStatus().isSuccess()) {
    // Output the response entity on the JVM console
    response.getEntity().write(System.out);
} else if (response.getStatus()
        .equals(Status.CLIENT_ERROR_UNAUTHORIZED)) {
    // Unauthorized access
    System.out
            .println("Access authorized by the server, " +
                    "check your credentials");
} else {
    // Unexpected status
    System.out.println("An unexpected status was returned: "
            + response.getStatus());
}


你可以修改這裏的user ID或者password,來檢查服務端返回的response。請別忘記了在啓動客戶端之前,先執行Restlet服務端程序。請注意,如果你從另一臺機器上測試服務端,那麼在瀏覽器中輸入URI時需要將"localhost"替換爲服務器的IP地址或者域名。由於使用了默認接收任何類型URI的 VirtualHost,因此服務端無需任何修改。

10. URI重寫和重定向

Restlet框架的另一個優點是對cool URI的內建支持。Jacob Nielsen在他的AlertBox中給出了對URI設計的重要性的絕佳描述

首先介紹的工具是Redirector,它能夠將cool URI重寫爲另一個URI,並接着進行相應的自動重定向。這裏支持一些重定向類型:通過客戶端/瀏覽器的外部重定向、類似代理行爲的connector重定向。在下面的例子中,我們將基於Google爲名爲"mysite.org"的站點定義一個檢索服務。與URI相關的"/search"就是檢索服務,它通過"kwd"參數接收一些檢索關鍵字:

// Create an application
Application application = new Application(component.getContext()) {
    @Override
    public Restlet createRoot() {
        // Create a Redirector to Google search service
        String target =
           "http://www.google.com/search?q=site:mysite.org+{keywords}";
        return new Redirector(getContext(), target,
                Redirector.MODE_CLIENT_TEMPORARY);
    }
};

// Attach the application to the component's default host
Route route = component.getDefaultHost().attach("/search", application);

// While routing requests to the application, extract a query parameter
// For instance :
// http://localhost:8182/search?kwd=myKeyword1+myKeyword2
// will be routed to
// http://www.google.com/search?q=site:mysite.org+myKeyword1%20myKeyword2
route.extractQuery("keywords", "kwd", true);


請注意,Redirector只需要三個參數。第一個參數是父級上下文,第二個參數定義瞭如何基於URI模板重寫URI。這裏的URI模板將被Template類處理。第三個參數定義了重定向類型:出於簡化的目的,我們選擇了客戶端重定向。

同時,當調用被傳遞給application時,我們使用了Route類從request中提取查詢參數“kwd”。如果發現參數,參數將被複制到request的“keywords”屬性中,以便Redirector在格式化目標URI時使用。

11. 路由器和分層URI

作爲Redirector的補充,我們還具有另一個管理cool URI的工具:Router(路由器)。它們是一種特殊的Restlet,能夠使其它Restlet(例如Finder和Filter)依附於它們,並基於URI模板進行自動委派調用(delegate call)。通常,你可以將Router設置爲Application的根。

這裏,我們將解釋一下如何處理下面的URI模板:

   1. /docs/ 用於顯示靜態文件
   2. /users/{user} 用於顯示用戶帳號
   3. /users/{user}/orders 用於顯示特定用戶的所有訂單
   4. /users/{user}/orders/{order} 用於顯示特定的訂單

實際上,這些URI包含了可變的部分(在大括號中)並且沒有文件擴展名,這在傳統的web容器中很難處理。而現在,你只需要做的只是使用URI模板將目標Restlet附着到Router上。在Restlet框架運行時,與request的URI最爲匹配的Route將接收調用,並調用它所附着的 Restlet。同時,request的屬性表也將自動更新爲URI模板變量。
image

請看下面的具體實現代碼。在真實的應用中,你可能希望創建單獨的子類來代替我們這裏使用的匿名類:

// Create a component
Component component = new Component();
component.getServers().add(Protocol.HTTP, 8182);
component.getClients().add(Protocol.FILE);

// Create an application
Application application = new Application(component.getContext()) {
    @Override
    public Restlet createRoot() {
        // Create a root router
        Router router = new Router(getContext());

        // Attach a guard to secure access to the directory
        Guard guard = new Guard(getContext(),
                ChallengeScheme.HTTP_BASIC, "Restlet tutorial");
        guard.getSecrets().put("scott", "tiger".toCharArray());
        router.attach("/docs/", guard);

        // Create a directory able to expose a hierarchy of files
        Directory directory = new Directory(getContext(), ROOT_URI);
        guard.setNext(directory);

        // Create the account handler
        Restlet account = new Restlet() {
            @Override
            public void handle(Request request, Response response) {
                // Print the requested URI path
                String message = "Account of user \""
                        + request.getAttributes().get("user") + "\"";
                response.setEntity(message, MediaType.TEXT_PLAIN);
            }
        };

        // Create the orders handler
        Restlet orders = new Restlet(getContext()) {
            @Override
            public void handle(Request request, Response response) {
                // Print the user name of the requested orders
                String message = "Orders of user \""
                        + request.getAttributes().get("user") + "\"";
                response.setEntity(message, MediaType.TEXT_PLAIN);
            }
        };

        // Create the order handler
        Restlet order = new Restlet(getContext()) {
            @Override
            public void handle(Request request, Response response) {
                // Print the user name of the requested orders
                String message = "Order \""
                        + request.getAttributes().get("order")
                        + "\" for user \""
                        + request.getAttributes().get("user") + "\"";
                response.setEntity(message, MediaType.TEXT_PLAIN);
            }
        };

        // Attach the handlers to the root router
        router.attach("/users/{user}", account);
        router.attach("/users/{user}/orders", orders);
        router.attach("/users/{user}/orders/{order}", order);

        // Return the root router
        return router;
    }
};

// Attach the application to the component and start it
component.getDefaultHost().attach(application);
component.start();



請注意,變量的值是直接從URI中提取的,因此這是沒有精確解碼的。爲了實現這樣的工作,請查看手冊中的decode(String)方法

12. 抵達目標資源

在前面的示例中,在從目標URI中提取那些有趣部分時,我們利用了Restlet框架非常靈活的路由特性對request進行路由。但是,我們沒有注意request方法和客戶端對於它所期望的response的偏好。於是,我們如何才能將Restlet處理器和後臺系統、域對象聯繫在一起呢?

到目前爲止,我們已經介紹了一些在Restlet中超越傳統Servlet API的特性。但我們並沒有在"Restlet"這個框架名稱中使用"REST"。如果你還沒有做的話,我推薦你學習一些關於REST架構風格和將其應用於Web應用的最佳實踐。這裏提供了相關的FAQ記錄,希望能給你一些啓示,同時我們也運營着很有用的REST搜索引擎(基於Google)。如果你對傳統MVC框架有一定了解,那麼你可以閱讀一下另一個FAQ記錄,它提供了對MVC與Restlet關係的詳細說明。
image

總結一下,request中含有標識目標資源的URI,而目標資源就是調用的主旨。這種資源信息被保存在Request.resourceRef屬性中,並能夠像我們之前所見那樣服務於路由機制。因此在處理request時的首要目標就是發現目標資源。。。Resource類的實例或者其子類中的某個。爲了幫助我們完成此項任務,我們可以使用專用的Finder,一個Restlet子類,它將Resource類引用作爲參數並在request到來時自動實例化它。然後Finder將動態將調用分配給最新創建的實例,實際上就是根據request方法調用它的handle*()方法中的某一個(handleGet,handleDelete等)。當然,我們可以自定義這種行爲,甚至使用Router的attach()方法,將URI模板和 Resource類作爲其參數透明地創建Finder!現在,讓我們看一下展示了示例中主框架類之間關係的全景圖表:

回到代碼中,我們在這裏重構了Application.createRoot()方法。出於簡化目的,我們沒有提供具有靜態文件的目錄。你可以發現將Resource類直接指派給Router的方法。

// Create a router
Router router = new Router(getContext());

// Attach the resources to the router
router.attach("/users/{user}", UserResource.class);
router.attach("/users/{user}/orders", OrdersResource.class);
router.attach("/users/{user}/orders/{order}",
        OrderResource.class);

// Return the root router
return router;



我們最後將重審一下UserResource類。這個類繼承自org.restlet.resource.Resource類,因此它覆蓋了具有三個參數的構造方法。此方法初始化了"context"、"request"和"response"屬性。接着,我們使用從"/users/{user} "URI模板中提取出的"user"屬性,將它的值保存在一個方便使用的成員變量中。然後,我們便可以在整個application中查找與"user" 相關的域對象了。最終,我們聲明瞭用於暴露給用戶的表示變量(representation variants),在這個簡單的例子中只是文字而已。它將用於在運行時透明地完成一些內容導航,以便爲每個request選擇適合的變量,所有這些工作都是透明的。

public class UserResource extends Resource {
    String userName;

    Object user;

    public UserResource(Context context, Request request,
            Response response) {
        super(context, request, response);
        this.userName = (String) request.getAttributes().get("user");
        this.user = null; // Could be a lookup to a domain object.

        // Here we add the representation variants exposed
        getVariants().add(new Variant(MediaType.TEXT_PLAIN));
    }

    @Override
    public Representation getRepresentation(Variant variant) {
        Representation result = null;
        if (variant.getMediaType().equals(MediaType.TEXT_PLAIN)) {
            result = new StringRepresentation("Account of user \""
                    + this.userName + "\"");
        }
        return result;
    }
}



你可以查看本指南中提供的代碼包並對應用進行測試,並能夠以僅接受Get請求的方式獲得在第十一章中的相同行爲。如果你希望使用PUT方法,那麼就需要在UserResource中創建一個"allowPut()"方法並簡單地返回"true",並且添加一個"put (Representation)"方法來處理調用。關於詳細內容請查閱Restlet的Javadocs。

結論

我們已經涵蓋了Restlet框架的許多方面。在你打算行動之前,讓我們先回顧一下展示了本指南的主要概念和它們之間關係的兩個層次圖表:
image

這裏是核心表示類:
image

除了本指南,你最好的信息來源就是Restlet API的Javadocs、Restlet擴展和NRE。還可以閱讀一下connector一節,它列舉出了客戶端和服務端connector,並解釋瞭如何使用、配置它們。集成一節列出了提供可插入特性的所有可用擴展:例如與servlet容器的集成、動態表示的生成等。你還可以在我們的討論組中提出問題並幫助別人。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章