构建 RESTful Web 服务

来源:互联网 发布:linux for qq2016安装 编辑:程序博客网 时间:2024/05/17 23:22

from: https://www.ibm.com/developerworks/cn/education/java/j-rest/j-rest.html


开始之前

关于本教程

REST 是一种思维方式,而非协议或标准。它是设计基于命名资源而非消息的松耦合应用程序 — 通常指面向 Web 的应用程序 — 的一种风格。在本教程中,您将了解到何为 REST 以及如何使用 Restlet(一个面向 Java™ 应用程序的轻量级框架)构建 RESTful 应用程序。

目标

本教程从 REST 的基本概念开始逐步指导您使用 Restlet 构建应用程序。您将学习如何:

  • 定义 RESTful Web 服务
  • 用 Restlet 框架实现它们
  • 用 JUnit 测验框架验证它们

学完本教程后,您就会领会到使用 RESTful 原则进行设计的益处,了解到 Restlet 框架是如何轻松地实现它们的。

先决条件

为了更好地学习本教程,您需要熟悉 Java 语法以及 Java 平台中面向对象开发的基本概念。还要熟悉 Web 应用程序。熟悉 Groovy、Unit、DbUnit 和 XMLUnit 对此也很有帮助。

系统需求

要跟随并试用本教程中的代码,您需要如下列出的任意一种可行的安装:

  • Sun's JDK 1.5.0_09(或更高版本)
  • IBM Developer Kit for Java technology 1.5.0 SR3
  • Apache Ant 1.7 或者更高级版本

本文的源代码有两种版本(参见 下载)。其中一个版本包含了全部代码以及所需依赖项(Restlet 框架 JUnit、XMLUnit 和 DbUnit)。使用低带宽连接的读者可以从它们各自的站点分别下载 Restlet 框架、JUnit、XMLUnit 和 DbUnit(参见参考资料)并使用不包含依赖项的 下载 包。

要学习本教程,推荐的系统配置为:

  • 支持 Sun JDK 1.5.0_09(或更新版本)或者 IBM JDK 1.5.0 SR3 的系统,主内存至少为 500MB
  • 有至少 20MB 的磁盘空间安装软件组件和涉及到的示例

本教程中的指令和示例基于 Microsoft® Windows® 操作系统。教程中所用到的所有工具在 Linux® 和 UNIX® 系统上都可以运行。

何为 REST?

REST 是设计基于命名资源 — 例如,以 Uniform Resource Locators(URL)、Uniform Resource Identifiers(URI)和 Uniform Resource Names(URN)的形式 — 而非消息的松耦合 Web 应用程序的一种风格。REST 巧妙地借助已经验证过的成功的 Web 基础设施 — HTTP。换句话说,REST 利用了 HTTP 协议的某些方面,例如GETPOST 请求。这些请求可以很好地映射到标准业务应用程序需求,诸如创建、读取、更新和删除(CRUD),如表 1 所示:

表 1. CRUD/HTTP 映射

请求就像是动词,而资源就像是名词,把两者相关联就形成了对行为的逻辑表达 — 例如, GET 这个文件,DELETE 那条记录。

真正的 REST 之父 Roy Fielding 在他的博士毕业论文中陈述到:REST “强调组件交互的可伸缩性、界面的普遍性、独立部署组件以及使用中间组件来减少交互延迟,增强安全性并封装遗留系统”(参见参考资料)。构建 RESTful 系统并不难,且这样的系统具有高度的可伸缩性,同时与底层数据松散耦合;这样的系统还可以很好地利用缓存。

Web 上所有的东西(页面、图像等)本质上都是资源。而 REST 正是基于命名资源而非消息的,这就限制了底层技术的曝光,从而给应用程序设计中的松耦合提供了便利条件。例如,下面的 URL 在不暗示任何底层技术的情况下,公开了资源:http://thediscoblog.com/2008/03/20/unambiguously-analyzing-metrics/。

该 URL 表示一个资源 — 一篇名为 “Unambiguously analyzing metrics” 的文章。请求该资源就会调用 HTTP GET 命令。注意该 URL 是基于名词的。基于动词的版本(大概类似 http://thediscoblog.com/2008/03/20/getArticle?name=unambiguously-analyzing-metrics)会违反 REST 原则,因为它以 getArticle 的形式嵌套了一条消息。您也可以设想通过 HTTP 的POST 命令来发布一个新资源,(比如说,一篇诸如 http://thediscoblog.com/2008/03/22/rest-is-good-for-you/ 的文章)。你还可以设想用关联的、基于动词的 API — 如 createArticle?name=rest-is-good-for-you and deleteArticle?name=rest-is-good-for-you — 这样的调用来拦截 HTTPGET 命令,并最大限度地忽略已有的(并且是成功的)HTTP 基础设施。换句话说,它们不是 RESTful 风格。

REST 的魅力在于任何东西都可以成为资源,且表示方法也可以不同。在前面的例子中,资源为一个 HTML 文件,因此,其响应可能是 HTML 格式的。但是资源也可以是一个 XML 文档、序列化的对象或者 JSON 表示。其实,这些都无关紧要。重要的是资源被命名了,并且与它通信不会影响其状态。不影响状态是很重要的,因为无状态的交互有利于可伸缩性。

它的价值在那里?

引用达芬奇的一句名言 “简洁就是终极复杂”。万维网的实现非常简单,并且无可置否地获得了成功。REST 正是利用了 Web 的简单性,并因此造就了高度可伸缩的、松散耦合的系统,而且事实证明,这样的系统很容易构建。

正如您所看到的,构建 RESTful 应用程序最难的部分在于确定要公开的资源。解决了这个问题之后,再使用开源 Restlet 框架构建 RESTful Web 服务就是小菜一碟了。

起跑:构建一个 RESTful API

在本节中,您将为一个 Web 服务构建一个 RESTful API,该服务利用了支持数据库的现有应用程序的功能。

RESTful 比赛

设想这样一个在线应用程序,它管理赛跑比赛,参赛人员要跑完不同的路程(比如芝加哥马拉松赛跑)。应用程序管理赛跑(或者赛事)以及与其相关的参赛人员。它会报告某个选手的时间(跑完全程所用的时间)和排名(参赛人员以第几名跑完全程)。赛事筹办公司 Acme Racing 要求您构建一个 RESTful Web 服务,主办方可以用它来为特定比赛安排新的赛事和选手,并且可以为某次特定比赛提供官方记录。

Acme Racing 已经有了一个遗留的胖客户机应用程序,它支持类似的请求,并利用了一个简单的数据库和一个域模型。因此,剩下的工作就只有公开这个功能了。记住 REST 的魅力就在于它与底层应用程序的隐式松散耦合。因此,您目前的工作并非是去操心数据模型或与其相关联的技术 — 而是去构造一个支持公司需求的 RESTful API。

比赛 URI

Acme Races 希望主办方能够:

  • 查看现有比赛细节
  • 创建新的比赛
  • 更新现有比赛
  • 删除比赛

由于 REST 最终归结为命名资源,API 就成为了一系列 URI 模式,并且与资源相关联的行为要通过标准 HTTP 命令调用。

正如您可以看到的,客户的请求很好地映射到了 CRUD。并且如您在 表 1 中所了解到的一样,REST 分别通过 HTTP 的 POSTGETPUT 以及DELETE 请求来支持 CRUD。因此,一个支持这些请求的基 RESTful URI 应为 http://racing.acme.com/race。注意,在这种情况下,比赛是客户机要使用的资源。

用 HTTP GET 来调用 URI 会返回一个比赛列表(这时先不要考虑响应的格式)。要添加新比赛,要用包含适当信息(例如,一个包含诸如名称、日期和距离等信息的 XML 文档)的 HTTPPOST 来调用同一 URI。

要更新和删除现有比赛,则需要对特定比赛的实例进行操作。因此,可以给单个比赛赋予一个 URI:http://racing.acme.com/race/race_id。在这种情况下,race_id 表示任一比赛标识符的一个占位符(诸如 1 或者 600 米)。因此,查看一个现有比赛实例就是针对该 URI 执行一个 HTTPGET 请求;更新或者删除一个比赛分别为一个 PUT 或者 DELETE 请求。

Acme Racing 可能还希望公开有关某次比赛的参赛人员的数据。他们希望他们的服务支持:

  • 获得有关特定比赛的全部参赛人员的数据。该数据还要包含已结束的比赛的赛跑时间和排名。
  • 为特定比赛创建一个或多个参赛人员。
  • 更新特定比赛的某一参赛人员的信息(如年龄)。
  • 删除特定比赛的某一参赛人员。

Acme 可能还希望该服务可以让用户查看特定比赛的某个参赛人员的个人数据。

和比赛一样,将 RESTful URI 应用于与比赛相关联的参赛人员同样是一个逻辑行为。例如,查看特定比赛的全部参赛人员可以通过对 http://racing.acme.com/race/race_id/runner 的GET 请求来实现。

要获得一个比赛的某个参赛人员的个人信息,可以编址为 http://racing.acme.com/race/race_id/runner/runner_id

race_id 一样,runner_id 是 ID 的逻辑实现的一个占位符,这些 ID 可以是数字、姓名、字母数字组合等。

向一个比赛添加参赛人员就是一个对 http://racing.acme.com/race/race_id/runner 的 POST 请求。更新或删除特定参赛人员则分别是对 http://racing.acme.com/race/race_id/runner/runner_idPUTDELETE 请求。

因此,这些 URI(每一个 URI 都支持四个标准 HTTP 请求的其中一些或者全部)就满足了 Acme Racing 的需求:

  • /race
  • /race/race_id
  • /race/race_id/runner
  • /race/race_id/runner/runner_id

记住,一个 URI 可以映射到不止一个 HTTP 动词(例如,将一个HTTP GET 应用到 /race 将返回数据;使用 POST 和适当的数据在服务器上创建数据)。因此,有些 HTTP 命令不能实现。例如,/race 可能不支持 DELETE 命令(Acme Racing 不会删除所有的比赛);/race/race_id 可能支持DELETE 命令,因为移除一个比赛的某个特定实例是一个业务需求。

格式化资源

在本小节中,您将构造一系列的 XML 文档来表示 RESTful 比赛 Web 服务将会支持的资源。

比赛 URI

前一小节中为 Acme Racing 构建的 RESTful API 涉及了网络端点或者 URI,但并未提及资源。正如我在前面提到的,就 REST 而言,资源的格式并不重要。例如,您可以来回传送 XML 或者二进制流。

在业务事务环境中,XML 是不折不扣的在机器之间通信的混合语言,所以构造一系列受 RESTful 服务支持的 XML 文档很有意义。竞赛的域相当简单,而且您可以使用现有的数据模型,所以定义几个表示比赛和参赛人员的 XML 文档也很简单。

例如,可以用清单 1 中的 XML 定义一个比赛:

清单 1. 某次比赛的 XML 文档
1
2
3
4
<racename="Mclean 1/2 Marathon"date="2008-05-12"distance="13.1"id="1">
 <uri>/races/1</uri>
  <description/>
</race>

注意一个 <race> 有一个 id,且清单 1 包含一个作为比赛定义的一部分的 id。这是 REST 与 Web 的一个关键的方面 — 资源是相关的,应该链接在一起。因此,一个<race> 总是含有一个描述它的 RESTful 表示的 <uri> 元素。清单 1 中的 XML 是一个 GET 到 /races/1 的请求的响应。

要创建一个新比赛,您可以省略 id 方面(因为管理惟一 ID 是由您正在构建的应用程序控制的)。这暗示您可以排除 <uri> 元素。因此,POST 请求应该如清单 2 所示:

清单 2. 创建比赛的 XML
1
2
3
<racename="Limerick 2008 Half"date="2008-05-12"distance="13.4">
 <description>erin go braugh and have a good time!</description>
</race>

那参赛人员呢?参赛人员与比赛有关,对么?所以 <race> 元素支持保存一到两个 <runner> 元素,如清单 3 所示:

清单 3. 与比赛相关的参赛人员
1
2
3
4
5
6
7
8
9
10
11
12
<racename="Limerick 200 Half"date="2008-05-12"distance="13.4"id="9">
 <uri>races/9</uri>
 <description>erin go braugh and have a good time!</description>
 <runners>
  <runnerfirst_name="Linda"last_name="Smith"age="25"id="21">
   <uri>/races/9/runner/21</uri>
  </runner>
  <runnerfirst_name="Andrew"last_name="Glover"age="22"id="20">
   <uri>/races/9/runner/20</uri>
  </runner>
 </runners>
</race>

例如,清单 3 中的 XML 文档就是通过 URI /race/race_id/runner 而返回的。API 还支持通过 URI /race/race_id/runner/runner_id 对单一参赛人员执行的 CRUD 操作。

因此,这些 CRUD 操作的 XML 如清单 4 所示:

清单 4. CRUD XML
1
2
3
4
5
6
7
8
<racename="Mclean 1/2 Marathon"date="2008-05-12"distance="13.1"id="1">
 <uri>/races1</uri>
 <description/>
 <runnerfirst_name="Andrew"last_name="Glover"age="32"id="1">
  <uri>/races/1/runner/1</uri>
  <resulttime="100.04"place="45"/>
 </runner>
</race>

注意,如果比赛已经完成,XML 文档中会包含参赛人员的比赛结果。记住,使用一个 POST 请求就意味着创建一个参赛人员,因此,<runner> 元素的id 属性不会呈现。

Restlets

您已经定义了一个 RESTful API 了,它可以很好地映射到 CRUDing 比赛和参赛人员。并且您已经定义了通信的格式:XML 文档。在这一小节中,您将开始用一个仿效 servlet 的创新的框架将所有这些组合起来。

Restlet 框架

Restlet 应用程序与 servlet 应用程序有一个相似点,就是它们都处在容器中,但实际上它们在两个方面是截然不同的。第一,Restlet 不使用 HTTP 的直接概念或其状态显示,如 cookies 或者 session。第二,Restlet 框架极其轻便。正如您将要看到的,只用几个核心 Restlet 基类扩展出来的几个类就能够构建一个功能完善的 RESTful 应用程序。配置和部署利用现有的容器模型,所以您只需更新原来的 web.xml 文件,并部署一个标准 Web 归档文件(WAR)就可以了。

基本上,一个用 Restlet 框架构建的 RESTful 应用程序的大部分都需要使用两个基类: ApplicationResource。逻辑上,Application 实例将 URI 映射到 Resource 实例。Resource 实例处理基本的 CRUD 命令,当然,这些命令都要映射到GETPOSTPUTDELETE

比赛应用程序

通过扩展 Restlet 框架的 Application 类来创建一个起跑点。在这个类中,定义响应 URI 的 Resource。该定义过程是用框架的Router 类来完成的。例如,如果您有诸如 /order_id 这样的 URI 的话,您需要指定哪一个对象可以处理这些请求。这个对象是框架的Resource 类型的一个实例。可以通过将这些对象附加到 Router 实例来使它们同 URI 链接,如清单 5 所示:

清单 5. 创建 Router 实例并映射 URI
1
2
Router router = new Router(this.getContext());
router.attach("order/{order_id}", Order.class);

所以在这个例子中,URI order/order_id 被逻辑映射到 Order 类(它再扩展 Resource)。

Acme Racing 拥有四个您已经定义了的逻辑 RESTful URI — 处理比赛和参赛人员的各个方面的四种模式:

  • /race
  • /race/race_id
  • /race/race_id/runner
  • /race/race_id/runner/runner_id

每一个 URI 的行为(比如,如果它使用 POSTDELETEGET 等)在这点上并不重要。每一个Resource 的行为则是由 Resource 实例负责;而 Application 实例用于通过Router 实例将这些 URI 映射到(尚未定义的)Resource ,如清单 6 所示:

清单 6. 将 Acme Racing 的 URI 映射到 Resource
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class RaceApplication extends Application{
 public RaceApplication(Context context) {
  super(context);
 }
 
 public Restlet createRoot() {
  Router router = new Router(this.getContext());
  router.attach("/race", RacesResource.class);
  router.attach("/race/{race_id}", RaceResource.class);
  router.attach("/race/{race_id}/runner", RaceRunnersResource.class);
  router.attach("/race/{race_id}/runner/{runner_id}", RaceRunnerResource.class);
  return router;
 }
}

基类 Application 是一个抽象类。扩展类必须实现 createRoot() 方法。在这个方法中,可以创建一个Router 实例,并将 Resource 附加到 URI,像清单 6 中所示的那样。

正如您所看到的,有四个不同的 Resource 类。我已经将它们命名,使它们与期望的 URI 的高级行为相匹配。例如 /race URI 应该处理多个比赛实例,因此Resource 类型被命名为 RacesResource。一旦 URI(/race/race_id)中包含了id,就暗示着正在操作一个单独的比赛,因此,Resource 类型适合命名为 RaceResource

比赛资源

定义了 Application 实例来处理四种不同的 URI 模式后,一定要实现四个 Resource

Restlet 框架中的 Resource 类型被认为是 Restlet。它们是使用 Restlet 框架开发的 RESTful 的应用程序的核心。与Application 类型不同,基 Resource 类并非抽象类。它更像是一个可以按需要覆盖其默认行为的模版。

从较高的程度来看,Resource 有四个需要覆盖的方法。它们映射到了基本的 HTTP 命令,这些命令是 REST — GETPOSTPUT 以及DELETE— 的标准,这并不是巧合。由于 Resource 类是非抽象类,所以框架需要为期望调用的行为实现一对方法。例如,如果想让一个特定的资源来响应DELETE 请求的话,首先要实现 delete() 方法。第二,必须实现 allowDelete() 方法,并且要让这个方法返回true(默认为 false)。相应的 PUTPOSTDELETE 默认允许方法返回 false,而 allowGet() 方法默认返回 true。这意味着对于只读的 Resource,只需要覆盖一个方法(而不是像在另外三种情况中覆盖两个)。或者也可以调用 Resource 类中的 setModifcation(true),因此不需要覆盖单个 HTTP 动词 allow 方法。

例如,RacesResource 应该用一个在系统中描述比赛的 XML 文档来响应 GET 请求。用户也可以通过这个Resource 类型创建新比赛。因此,RacesResource 类覆盖了 Resource 基类中的至少三个方法:

  • getRepresentation()
  • allowPost()
  • post()

记住,Resource 实例默认为只读。因此,不需要覆盖 allowGet() 方法。

生成 XML 文档

在 格式化资源 小节,我们决定利用 XML 作为客户机和服务之间共享信息的数据机制。因此,Restlet 一定要操作 XML:在 GET 的情况下构建它,在POSTPUT 或者 DELETE 的情况下消费它。在这一小节中,您将通过使用 Groovy 脚本语言来消除生成和操作 XML 文档的痛苦(参见参考资料)。

利用 Groovy

使用 XML 可不是个轻松的活儿。退一步说,这个工作很单调乏味,且容易发生错误。幸运的是,Groovy 让使用 XML 简单了许多。

您将利用 Groovy 的力量来生成 XML 并完成操作 XML 文档这个沉闷的工作。使用 Groovy 处理 XML 是再简单不过了。例如,解析 XML 文档就很简单。以清单 7 中的 XML 文档为例:

清单 7. 一个简单的待解析的 XML 文档
1
2
3
4
5
6
<acme-races>
  <racename="Alaska 200 below"date="Thu Jan 01"distance="3.2"id="20">
    <uri>/races/20</uri>
    <description>Enjoy the cold!</description>
  </race>
</acme-races>

假设您想获取 <race> 元素的 name 属性的值,您只需向 Groovy 的 XMLSlurper 类传入一个 XML 文档实例,调用parse() 方法,然后定位到您想要的元素或者属性就可以了,如清单 8 所示:

清单 8. 用 Groovy 解析 XML
1
2
def root = new XmlSlurper().parseText(raceXML)
def name = root.race.@name.text()

如果您想获得描述,调用 root.race.description.text() 就可以了。

创建 XML 也很简单。如果想创建清单 7 中的 XML 代码片断的话,只要创建一个 Groovy 的 MarkupBuilder 类的实例,然后给它添加个节点就可以了,如清单 9 所示:

清单 9. 创建 XML 再简单不过了
1
2
3
4
5
6
7
8
9
def writer = new StringWriter()
def builder = new MarkupBuilder(writer)
builder."acme-races"() {
    race(name: "Alaska 200 below",  date: "Thu Jan 01", distance: "3.2", id: "20") {
        uri("/races/20")
        description("Enjoy the cold!")
    }
}
println writer.toString()

注意元素是如何通过附加名称到 builder 实例而被添加到 XML 文档的。我需要为 acme-races 加上引号,因为在 Groovy 字符串字母中不允许使用连字符,因此,使acme-races 成为 String 就很好地解决了这个问题。

元素可以有属性。属性名和属性值是通过构造 Groovy 映射创建的,同时 Groovy 映射将两者链接在一起(例如,name:"Alaska 200 below")。

数据层

本节描述了现有的域对象,它生成了 RESTful 服务将会重用的数据层。

域对象

正如您从 起跑:构建 RESTful API 一节了解到的,Acme Racing 投资于以前的项目的数据层,并且想将其重用于新的 Web 服务。当然,这会让您的工作更轻松。简单地说,该数据层由三个业务对象组成:RaceRunner 以及Result。它们由 Spring 和 Hibernate 有效地管理着;然而,这些框架您是看不见的;您仅有一个运行良好的 JAR 文件(换句话说,它可以让您轻松创建新比赛,找到现有参赛人员等)。

业务对象支持一系列的寻找方法,它们使获取比赛和参赛人员实例变得很轻松。对象可以分别通过 save()update()remove() 方法被持久化、更新、以及从底层数据库中移除。

例如,Race 对象支持一系列的寻找方法,并为良好地操作持久化数据提供便利条件。Race 对象的 API 很简单,如清单 10 所示:

清单 10. Race 的 API
1
2
3
4
5
6
Collection<Race> findAll();
Race findById(long id);
Race findByName(String name);
void create(Race race);
void update(Race race);
void remove(Race race);

一个 Race 实例有很多的属性,如清单 11 所示:

清单 11. Race 的属性
1
2
3
4
5
6
7
private long id;
private String name;
private Date date;
private double distance;
private Set<Runner> participants;
private Set<Result> results;
private String description;

Race 的所有属性都可以通过 getter 和 setter 使用。而且,项集合(如 participantsresults)支持添加单独的项。因此,Race 对象有一个 addParticipant() 方法,如清单 12 所示:

清单 12. RaceaddParticipant() 方法
1
public void addParticipant(final Runner participant) ;

正如您所看到的,使用这个域模型是很简单的。

构建与测试服务

知道如何使用 XML 并已经有了可以用的数据层之后,是时候继续使用 Restlet 构建 RESTful 应用程序并做一些测试准备了。

比赛服务

回忆一下,Acme Racing 希望服务可以让客户机查看现有比赛,并创建新比赛。您已经大致了解了支持 /race 这个行为的 RESTful URI。

通过 RaceApplication 类中的 Router 类,可以将这个 URI 链接到 RacesResource 类。您已经知道了您必须实现三个方法:

  • getRepresentation()
  • allowPost()
  • post()

因此,创建一个叫做 RacesResource 的类,并确保它扩展了 org.restlet.resource.Resource。另外,实现一个含三个参数的构造函数,如清单 13 所示:

清单 13. RacesResource 中含三个参数的构造函数
1
2
3
4
5
public class RacesResource extends Resource {
 public RacesResource(Context context, Request request, Response response) {
  super(context, request, response);
 }
}

一定要指导 Restlet 如何正确通信资源表示。由于 XML 将会起到资源格式的作用,一定要通过添加 XML 变体类型来指导 Restlet。Restlet 中的Variant 表示 Resource 的格式。基类 Resource 含有一个 getVariants() 方法,它使添加各种 Variant 类型变得容易。因此,将清单 14 中的内容添加到您的构造函数:

清单 14. 将 XML 表示为一个变体
1
this.getVariants().add(new Variant(MediaType.TEXT_XML));

Restlet 框架支持很多种媒介类型,包括图像和视频。

处理 GET 请求

现在是时候实现类的最简单行为了:处理 GET 请求。如清单 15 所示涵盖 getRepresentation() 方法:

清单 15. 涵盖 getRepresentation()
1
2
3
public Representation getRepresentation(Variant variant) {
 return null;
}

可以看到,这个方法返回了一个 Representation 类型,它有多种实现。其中一个实现 — 适当地命名为 StringRepresentation— 表示字符串,并会满足您的需求。

正如您所知道的,您已经有了一个遗留域模型,它可以支持使用数据库。事实证明有人已经写出了实用程序类,叫做 RaceReporter,它可以将域对象转换为 XML 文件。这个类的racesToXml() 方法使用了一个 Race 实例集合,并返回了表示与清单 16 类似的 XML 文档的String

清单 16. XML 响应
1
2
3
4
5
6
7
8
9
10
11
12
<acme-races>
 <races>
  <racename="Leesburg 5K"date="2008-05-12"distance="3.1"id="5">
  <uri>/races/5</uri>
  <description/>
 </race>
 <racename="Leesburg 10K"date="2008-07-30"distance="6.2"id="6">
  <uri>/races/6</uri>
  <description/>
 </race>
 </races>
</acme-races>

事实上,这个 XML 文档演示了在响应 GET 请求而调用 /race URI 时 RESTful Web 服务将返回的内容。

因此,我们的工作是链接底层数据存储中所有比赛实例的检索;其实在这一点上,您可能已经编写了一个测试了。

测试服务

使用 Restlet 框架,您能够构造客户机实例,并让它调用 RESTful Web 服务。此外,您可以利用 XMLUnit(参见 参考资源)来验证服务的输出是一些已知的 XML 文档。最后,同样重要的是,您也可以使用 DbUnit(参见 参考资料)将底层数据库设置为已知状态(这样就能总是获得相同的 XML 文档)。

可以使用 JUnit 4 创建两个 fixture,它可以适当初始化 XMLUnit 和 DbUnit,如清单 17 所示:

清单 17. 设置 XMLUnit 和 DbUnit
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Before
public void setUpXMLUnit() {
 XMLUnit.setControlParser(
  "org.apache.xerces.jaxp.DocumentBuilderFactoryImpl");
 XMLUnit.setTestParser(
  "org.apache.xerces.jaxp.DocumentBuilderFactoryImpl");
 XMLUnit.setSAXParserFactory(
  "org.apache.xerces.jaxp.SAXParserFactoryImpl");
 XMLUnit.setIgnoreWhitespace(true);
}
 
@Before
public void setUpDbUnit() throws Exception {
 Class.forName("org.hsqldb.jdbcDriver");
 IDatabaseConnection conn =
  new DatabaseConnection(
   getConnection("jdbc:hsqldb:hsql://127.0.0.1", "sa", ""));
 IDataSet data = new FlatXmlDataSet(new File("etc/database/race-db.xml"));
 try {
  DatabaseOperation.CLEAN_INSERT.execute(conn, data);
 } finally {
  conn.close();
 }
}

setUpDbUnit 方法中,数据库的 XML 表示通过 CLEAN_INSERT 命令被插入数据库。这个 XML 文件有效地插入了六个不同的比赛。因此,GET 的响应将会是一个带有六个比赛的 XML 文档。

接下来,可以创建一个测试用例,它可以对 /race URI 调用 HTTP GET、获取响应 XML 并使用 XMLUnit 的 Diff 类将它比作为控制 XML 文件,如清单 18 所示:

清单 18. 用 XMLUnit 验证 GET 响应
1
2
3
4
5
6
7
8
9
10
11
@Test
public void getRaces() throws Exception {
 Client client = new Client(Protocol.HTTP);
 Response response =
  client.get("http://localhost:8080/racerrest/race/");
 
 Diff diff = new Diff(new FileReader(
  new File("./etc/control-xml/control-web-races.xml")),
   new StringReader(response.getEntity().getText()));
 assertTrue(diff.toString(), diff.identical());
}

control-web-races.xml 文件是预期的来自 Web 服务的 XML 响应。它含有如清单 19 所示的数据:

清单 19. 控制 XML 文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<acme-races>
 <races>
  <racename="Mclean 1/2 Marathon"date="2008-05-12"distance="13.1"id="1">
   <uri>http://localhost:8080/races/1</uri>
   <description/>
  </race>
  <racename="Reston 5K"date="2008-09-13"distance="3.1"id="2">
   <uri>http://localhost:8080/races/2</uri>
   <description/>
  </race>
  <racename="Herndon 10K"date="2008-10-22"distance="6.2"id="3">
   <uri>http://localhost:8080/races/3</uri>
   <description/>
  </race>
  <racename="Leesburg 1/2 Marathon"date="2008-01-02"distance="13.1"id="4">
   <uri>http://localhost:8080/races/4</uri>
   <description/>
  </race>
  <racename="Leesburg 5K"date="2008-05-12"distance="3.1"id="5">
   <uri>http://localhost:8080/races/5</uri>
   <description/>
  </race>
  <racename="Leesburg 10K"date="2008-07-30"distance="6.2"id="6">
   <uri>http://localhost:8080/races/6</uri>
   <description/>
  </race>
 </races>
</acme-races>

现在运行这个测验当然会引起一系列的失败,因为还没有实现 RESTful 服务。同样要注意,源代码下载中的 Ant 构建文件包含部署 WAR 文件和启动与停止 Tomcat 的任务(参见下载)。这些都是成功运行测试的必要条件。

结果证明满足 GET 请求其实很简单。只需要对 Race 域对象调用 findAll 方法,然后将该调用的结果传入RaceReporterracesToXml() 方法。因此,您需要更新 RacesResource 实例,引入新的成员变量和构造函数的新的初始化,如清单 20 所示:

清单 20. 不要忘记添加 RaceReporter
1
2
3
4
5
6
7
8
9
public class RacesResource extends Resource {
 private RaceReporter reporter;
 
 public RacesResource(Context context, Request request, Response response) {
  super(context, request, response);
  this.getVariants().add(new Variant(MediaType.TEXT_XML));
  this.reporter = new RaceReporter();
 }
}

现在,实现 GET 请求就很容易了。只要向 getRepresentation 方法中添加三行内容就可以了,如清单 21 所示:

清单 21. 完成 GET request
1
2
3
4
5
public Representation getRepresentation(Variant variant) {
 Collection<Race> races = Race.findAll();
 String xml = this.reporter.racesToXml(races);
 return new StringRepresentation(xml);
}

信不信由您,仅此而已!

但是等等,不需要部署这个应用程序来测试它么?

部署和验证

在实际测试 RESTful 服务是否可以返回一个比赛列表之前,您需要先部署应用程序。本节展示了如何部署。

配置 web.xml

幸运的是,部署 Restlet 应用程序再简单不过了。您只要创建一个普通的 WAR 文件,确保 web.xml 文件适当配置就可以了。

要让 Restlet 应用程序在 servlet 容器中正常发挥功能,一定要更新 web.xml 文件:

  • 正确载入应用程序
  • 通过框架的自定义 servlet 传递所有需求

因此,您的 web.xml 文件应该如清单 22 所示:

清单 22. 简单的 web.xml 文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?xmlversion="1.0"encoding="UTF-8"?>
<web-appid="WebApp_ID"version="2.4"
 xmlns="http://java.sun.com/xml/ns/j2ee"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee
   http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
 <display-name>RESTful racing</display-name>
 <context-param>
  <param-name>org.restlet.application</param-name>
  <param-value>RaceApplication</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>/*</url-pattern>
 </servlet-mapping>
</web-app>

可以看到,在清单 22 中的第一部分,org.restlet.application 搭配 Restlet 应用程序的类名 RaceApplication(如果要赋予它一个包名的话,可能需要完全限定那个名称)。也要注意,文档最后的部分将所有请求映射到 RestletServlet 类型,该类型先前已被映射到了com.noelios.restlet.ext.servlet.ServerServlet 类。

RESTfully 测试

现在测试 RESTful Web 服务只要重新运行 清单 18 中的测验用例。

再看一下测试,可以解释一些东西。Restlet 的 Client 对象支持基本的 HTTP 命令 GETPUTPOSTDELETEClient 对象可以采取不同的协议的形式 — 在这种情况下只会偶尔依赖 HTTP。

GET 请求已经奏效了(参见图 1),所以你可以写另外一个测试。这一次,要完成 POST 所有期望的行为;换句话说,通过RacesResource 类测试新比赛的创建。

图 1. 在浏览器中查看 RESTful GET 请求
在浏览器中查看 RESTful GET 请求

要测试 POST,您需要用相关的信息来形成一个 XML 请求文档,并确保服务成功发回响应。当然,事实证明写这样的一个测试是相当简单的。只需向现有 JUnit 类添加一些额外的代码,如清单 23 所示:

清单 23. createRace 测验用例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private static String raceXML = "<?xmlversion=\"1.0\" encoding=\"UTF-8\"?>" +
 "<acme-races>\n" +
 " <racename='Limerick 2008 Half'date='2008-05-12'distance='13.4'>\n" +
 " <description>erin go brach</description>\n" +
 " </race>\n" +
 "</acme-races>";
 
@Test
public void createRace() {
 Form form = new Form();
 form.add("data", this.raceXML);
 Representation rep = form.getWebRepresentation();
 Client client = new Client(Protocol.HTTP);
 
 Response response =
  client.post("http://localhost:8080/racerrest/race/", rep);
 assertTrue(response.getStatus().isSuccess());

正如您所看到的,清单 23 迅速形成了一个表示 XML 文档的 String。在这里,我在创建一个称为 Limerick 2008 Half 的新比赛。然后它用 Restlet 框架的Client 对象将这个文档传给服务器。最后,它确保返回一个成功指示。

现在运行测验。失败了,是么?那是因为您还没有实现 POST 请求代码,这是下一小节的任务。

通过 RESTful 服务创建比赛

通过 RESTful Web 服务创建比赛只需要几个步骤就可以完成:接受 XML 文档、解析它、在底层数据库中创建一个新的 Race 实例,最后返回一个表示事务结果的响应。本小节涵盖了这几步。

处理 POST 请求

要通过 REST 实现创建行为,需要逻辑处理 POST 请求。因此,在 RacesResource 类中,必须覆盖两个方法:allowPost()post()

post() 方法完成这里的所有工作。它采用了一个 Representation 实例,可以从该实例中获取传送的数据。回忆一下清单 23 中的 createRace 测试用例将 XML 文档与一个名称相关联:data。因此,通过 Restlet 框架的Form 对象,可以获取一个表示传入的 XML 的 String,然后您可以将它传入所提供的 RaceConsumer 对象。这个对象很容易接受 XML 文档,并会相应地操作底层数据库。

如果事务有效的话,然后您将要相应地用一个成功的响应进行回应;无效的话,你将需要回应一个失败消息。

继续并覆盖 allowPost()post(),如清单 24 所示:

清单 24. 覆盖 POST 方法
1
2
3
4
5
public boolean allowPost() {
 return true;
}
 
public void post(Representation representation) {}

由于您将使用 RaceConsumer 对象,所以要将其添加为 RacesResource 类的一个成员变量,并在构造函数中将其初始化。相应地更新对象,如清单 25 所示:

清单 25. 添加 RaceConsumer
1
2
3
4
5
6
7
8
9
10
11
public class RacesResource extends Resource {
 private RaceReporter reporter;
 private RaceConsumer consumer;
 
 public RacesResource(Context context, Request request, Response response) {
  super(context, request, response);
  this.getVariants().add(new Variant(MediaType.TEXT_XML));
  this.reporter = new RaceReporter();
  this.consumer = new RaceConsumer();
 }
}

接下来,确保您的 post() 方法如清单 26 所示:

清单 26. 实现 post()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public void post(Representation representation) {
 Form form = new Form(representation);
 String raceXML = form.getFirstValue("data");
 Representation rep = null;
 try {
  long id = this.consumer.createRace(raceXML);
  getResponse().setStatus(Status.SUCCESS_CREATED);
  rep = new StringRepresentation(raceXML, MediaType.TEXT_XML);
  rep.setIdentifier(getRequest().getResourceRef().getIdentifier() + id);
 } catch (Throwable thr) {
  getResponse().setStatus(Status.SERVER_ERROR_INTERNAL);
  rep = new StringRepresentation("there was an error creating the race",
    MediaType.TEXT_PLAIN);
 }
 getResponse().setEntity(rep);
}

正如您所看到的,post 方法内容很多;然而,仔细检查之后,没有看上去那么复杂。传入的 XML 是通过 Form 对象获取的。然后 XML(以一个String 的形式)会被传入 consumer 实例的 createRace() 方法。如果一切都奏效的话(换句话说,比赛被持久化),会生成一个包含成功状态的响应,然后重新散列传入的 XML 及由此而生的 URI(即,race/43,此处 43 为新创建的比赛的id)。

如果进展得不是很顺利的话,本质上会是相同的过程,不同之处是失败消息会返回失败状态:创建失败,无 URI 返回。

继续并重新运行 createRace 测试。假设您已经重新部署了 RESTful Web 应用程序,一切都会顺利运行。

结束语

本教程实现的仅仅是中等程度的 Acme Racing 需求。但在此过程中,您已经了解到了使用 Restlets 是很方便的。整个过程中最困难的部分就是实现逻辑 RESTful API。本教程的源代码为提高您的学习乐趣提供了大量特性(参见下载)。

诗人亚历山大·蒲柏曾说过 “保持简洁要远远优于费尽心思营造出来的复杂。” 对于 REST 来说这句话在合适不过了。记住 REST 是一种思维方式 — 是设计基于命名资源而非消息的松耦合应用程序的一种风格。通过借助已经验证了的成功的 Web 基础设施,REST 使得这些应用程序易于设计和实现。而且 REST 应用程序具有良好的扩展性。

本教程只涉及到了 Restlet 框架特性的冰山一角,但不要让它欺骗了您。这个框架可以做很多事,包括在需要时添加安全设施。Restlet 是一种编码享受,只要查看一些 Restlet 代码,就可以很容易地理解代码库。

艾伯特爱因斯坦曾经说过,“所有事物都应该尽量保持简单,但不可过分简化。” 我认为 Restlet 框架和 REST 本身例证了这个名言的智慧,希望您可以同意我的看法。

下载资源


原创粉丝点击