gembin

OSGi, Eclipse Equinox, ECF, Virgo, Gemini, Apache Felix, Karaf, Aires, Camel, Eclipse RCP

HBase, Hadoop, ZooKeeper, Cassandra

Flex4, AS3, Swiz framework, GraniteDS, BlazeDS etc.

There is nothing that software can't fix. Unfortunately, there is also nothing that software can't completely fuck up. That gap is called talent.

About Me

 

Restlet创建面向资源的服务

Restlet项目(http://www.restlet.org)为“建立REST概念与Java类之间的映射”提供了一个轻量级而全面的框架。它可用于实现任何种类的REST式系统,而不仅仅是REST式Web服务;而且,事实证明它自从2005年诞生之时起,就是一个可靠的软件。
estlet项目受到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报头)完全隐藏起来。

基本概念

Restlet在术语上参照了Roy Fielding博士论文在讲解REST时采用的术语,如:资源(resource)、表示(representation)、连接器 (connector)、组件(component)、媒体类型(media type)、语言(language),等等。这些术语你应该不会陌生。Restlet增加了一些专门的类(如Application、Filter、 Finder、Router和Route),用以简化restlets的彼此结合,以及简化把收到的请求(incoming requests)映射为处理它们的资源。

图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上时使用这种语法。假如你不相信真会这么好的话,可以等到下一节,那时我会实际实现部分书签服务。

编写Restlet客户端

在示例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(http://retrotranslator. sourceforge.net/)轻易地把Restlet代码反移植(backport)到J2SE 4.0版上去。

示例12-3:Yahoo!搜索服务的一个Restlet客户端

// YahooSearch.java
import 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();

            // 用XPath找出数据结构中重要部分
            String expr = "/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 expr = "/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.java
import 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 =
    "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 {

            // 获取一个资源,即一个包含搜索结果的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的查询语言。

编写Restlet服务

接下来的例子会稍微复杂一些。我将向你展示如何设计并实现一个服务端应用。在第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();
}

资源与URI设计

由于Restlet未对资源设计作特别的限制,所以你完全可以根据ROA的设计原则来进行资源类(resource classes)及URIs的设计。在第7章,我要围绕“Rails的基于控制器的架构”来进行设计;而这里,我不需要围绕Restlet架构来进行设 计。图12-2 展示了URI是如何经由Router映射到资源,再映射到下层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,等等。正如我前面所提到的,这里的代码只是社会性书签应用所有代码的一部分;所以,如果你有兴趣进一 步学习的话,可以去下载一个完整的示例代码来阅读。

现在,你应该了解Restlet架构是如何把收到的(incoming)HTTP请求路由给特定的Resource类,然后再路由给该类的特定方法 了。你也应该知道如何由资源状态来构造表示(representations)了。一般,只要关注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项目在2007年初发布了1.0正式版。它只用了12个多月的开发时间。目前,该项目具有一个繁荣的开发与用户群体。Restlet 邮件列表很友好,不论是新手,还是有经验的开发者,它都欢迎。作为该项目的创建者,Noelios咨询公司是主要的开发力量,他们也提供专业的支持计划与 培训。

在本书编写之时,1.0版处于维护中,新的1.1版已经开始开发了。该项目计划将来把Restlet API提交给JCP(Java Community Process)。还有一个用于REST式Web服务的高层API,它已由Sun公司提交给JCP(JSR311)。这个高层API使得“把Java领域 对象暴露为REST式资源”更加容易。这将是对Restlet API(尤其是其Resource类)的一个很好的补充。Noelios咨询公司是最初的专家组成员,他们将根据标准的进展来对Restlet引擎作相应 的更新。


posted on 2008-07-24 11:53 gembin 阅读(1515) 评论(0)  编辑  收藏 所属分类: SOA


只有注册用户登录后才能发表评论。


网站导航:
 

导航

统计

常用链接

留言簿(6)

随笔分类(440)

随笔档案(378)

文章档案(6)

新闻档案(1)

相册

收藏夹(9)

Adobe

Android

AS3

Blog-Links

Build

Design Pattern

Eclipse

Favorite Links

Flickr

Game Dev

HBase

Identity Management

IT resources

JEE

Language

OpenID

OSGi

SOA

Version Control

最新随笔

搜索

积分与排名

最新评论

阅读排行榜

评论排行榜

free counters