天天看點

Restlet

轉載自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等)。

Restlet

請注意,同一個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的協助為我們提供了衆多的功能:譬如通路日志、請求自動解碼、配置狀态頁設定等。

Restlet

為了展示這些類,讓我們嘗試一個簡單的示例。首先,我們建立一個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中的Cookie和CookieSetting對象支援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;      
Restlet

請注意:認證和授權的最終結果是完全可定制的,這隻需要通過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模闆變量。

Restlet

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

// 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關系的詳細說明。

Restlet

總結一下,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架構的許多方面。在你打算行動之前,讓我們先回顧一下展示了本指南的主要概念和它們之間關系的兩個層次圖表:

Restlet

這裡是核心表示類:

Restlet

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