2011年9月26日

Resin使用Restlet建構Restful應用



研究目的
使用rest的方式減少網址複雜度。

Servlet的限制
2003年末,Jetty Web容器的作者、Servlet規範的貢獻者:Greg Wilkins在其博客上對Servlet的問題進行了如下總計:
    *
沒有對協議與應用之間的關係進行清洗的劃分。
    *
由於在設計Servlet時存在對阻塞IO的假設,因此不能充分利用非阻塞NIO機制。
    *
所有的Servlet Web容器對於某些應用來講是過度設計的。

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

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


REST架構概述
讓我們先從REST的視角審視一下典型的web架構。在下面的圖表中,埠代表了connector,而後者負責component之間的通訊(元件在圖中被表示為大盒子)。連結代表了用於實際通訊的特定協定(HTTPSMTP)



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

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




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

// Adds a new server connector in the map supporting the given protocol on the specified port.
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
// Attaches a target Restlet to this router with an empty URI pattern.
component.getDefaultHost().attach("/trace", restlet);

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



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時使用。

路由器和分層URI
作為Redirector的補充,我們還具有另一個管理cool URI的工具:Router(路由器)。它們是一種特殊的Restlet,能夠使其它Restlet(例如FinderFilter)依附於它們,並基於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框架運行時,與requestURI最為匹配的Route將接收調用,並調用它所附著的 Restlet。同時,request的屬性工作表也將自動更新為URI範本變數。





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

// 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)方法

實作
Resin配置方法 :
1.          首先下載Restlet1.1.9版本
2.          將裡面lib包中的 org.restlet.jar , com.noelios.restlet.jar , com.noelios.restlet.ext.servlet_2.5.jar放入專案目錄的/lib以及/resin ext-lib資料夾。
3.          然後修改resin/wapps/專案/WEB-INF/web.xml檔案,新增以下

<context-param> 
    <param-name>org.restlet.component</param-name> 
    <param-value>com.firstResource.RestComponent</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>/rest/*</url-pattern>
</servlet-mapping>

4.          其中RestComponent就是所有Application 管理者,用來分配uri給相對的Appication
5.          建立RestComponent

public class RestComponent extends Component {
   public RestComponent() {
      getServers().add(Protocol.HTTP, 8080); 
      getDefaultHost().attach("/member",
             new MemberApp(getContext()));
      getDefaultHost().attach("/circle",
             new CircleApp(getContext()));
      getDefaultHost().attach("/calculate",
             new CalculateApp(getContext()));
   }
}

MemberApp(Application)

   @Override
   public synchronized Restlet createRoot() {
      // Create a router Restlet that defines routes.
      Router router = new Router(getContext());
      router.attachDefault(MemberProfileResource.class);
      router.attach("/{value}/photo", MemberPhotoResource.class);
      router.attach("/{value}/profile", MemberProfileResource.class);
      router.attach("/{value}/company", MemberCompanyResource.class);
     
      return router;
   }


Application可以在將近來的url再分派給底下的Resource類別實作。
/{value}/profile{}表示這個url的可變數,後面以/profile結尾

MemberProfileResource(Resource)

public class MemberProfileResource extends Resource {

   String value;
  
   public MemberProfileResource(Context context, Request request,
          Response response) {
      super(context, request, response);
      this.value = (String) getRequest().getAttributes().get("value");
      // This representation has only one type of representation.
      getVariants().add(new Variant(MediaType.TEXT_PLAIN));
   }

   /**
    * Returns a full representation for a given variant.
    */
   @Override
   public Representation getRepresentation(Variant variant) {
      byte[] ba_mrscid = BytesUtil.hexToBytes(value);
     
      Representation representation = new StringRepresentation(
             "test", MediaType.TEXT_PLAIN);
      return representation;
   }
}


可以使用getRequest().getAttributes().get("value");
取得url命名之變數,這裡是得到value的值,然後將其存入類別裡的成員變數中,最後Resource會在getRepresentation中實作回應的內容,這邊我們就使用value作為faceboss的會員hex_mrscid,透過MemberInfoKeeper取得會員資料。

2011.09.26 補充 :
如果中文顯示亂碼請在representation中加入下面程式碼
representation.setCharacterSet(CharacterSet.UTF_8);


取得url參數(parameter)方法
for (Parameter parameter : form) {
   if(parameter.getName().equals("value")){
   }
}




本文參考於cleverpig作為貢獻者完成了本文的翻譯和整理工作,在這裡為其說明在resin上坐實作並推廣,有興趣的人可以到restlet官方上參考英文文件。


沒有留言:

ShareThis