用 Restlet 创建面向资源的服务

阅读数:13300 2008 年 5 月 30 日 09:05

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

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 报头)完全隐藏起来。

基本概念

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 引擎作相应的更新。


本文节选自博文视点出版公司即将推出的经典著作《RESTful Web Services 中文版》中的第12 章《REST 式服务框架》。

《RESTful Web Services 中文版》向读者介绍了什么是REST、什么是面向资源的架构(Resource-Oriented Architecture,ROA)、REST 式设计的优点、REST 式Web 服务的真实案例分析、如何用各种流行的编程语言编写Web 服务客户端、如何用三种流行的框架(Ruby on Rails、Restlet 和Django)实现REST 式服务等。不仅讲解REST 与面向资源的架构(ROA)的概念与原理,还向读者介绍如何编写符合 REST 风格的Web 2.0 应用。本书详实、易懂,实战性强,提供了大量RESTful Web 服务开发的最佳实践和指导,适合广大的Web 开发人员、Web 架构师及对Web 开发或Web 架构感兴趣的广大技术人员与学生阅读。

与此同时,博文视点还授权 InfoQ 中文站独家为大家提供额外的样章进行试读:欢迎下载第 3 章《REST 式服务有什么不同》

评论

发布