构建一个RESTful Web服务

在你开始前

关于本教程

REST是一种思考方式,而不是协议或标准。 这是一种设计松散耦合的应用程序的样式,这种应用程序通常依赖于命名资源而不是消息,这些应用程序通常是面向Web的应用程序。 在本教程中,您将了解什么是REST,以及如何使用Restlets(用于Java™应用程序的轻量级REST框架)构建RESTful应用程序。

目标

本教程将引导您逐步了解REST的基本概念,并使用Restlet构建应用程序。 您将学习如何:

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

完成本教程的学习后,您将了解使用RESTful原理进行设计的好处,并且将了解Restlet框架如何使其变得容易。

先决条件

为了从本教程中获得最大收益,您应该熟悉Java语法和Java平台上面向对象开发的基本概念。 您还应该熟悉Web应用程序。 熟悉Groovy,JUnit,DbUnit和XMLUnit也很有帮助。

系统要求

要继续并尝试本教程的代码,您需要以下任一版本的有效安装:

本教程有两个版本的源代码(请参阅下载 )。 一种版本包括所有代码和所需的依赖关系(Restlet框架,JUnit,XMLUnit和DbUnit)。 具有低带宽连接的读者可能更喜欢从各自的站点下载Restlet框架,JUnit,XMLUnit和DbUnit(请参阅参考资料 ),并使用不包含依赖项的Download软件包版本。

本教程推荐的系统配置为:

  • 支持带有至少500MB主内存的Sun JDK 1.5.0_09(或更高版本)或IBM JDK 1.5.0 SR3的系统
  • 至少20MB的磁盘空间用于安装软件组件和示例

本教程中的说明和示例均基于Microsoft®Windows®操作系统。 本教程中介绍的所有工具也可以在Linux®和UNIX®系统上使用。

什么是REST?

REST是一种设计依赖名称资源的松散耦合Web应用程序的样式,例如以统一资源定位符(URL),统一资源标识符(URI)和统一资源名称(URN)的形式而不是消息。 REST巧妙地在已经验证和成功的Web基础结构HTTP上搭载。 也就是说,REST利用了HTTP协议的各个方面,例如GETPOST请求。 这些请求很好地映射到标准业务应用程序需求,例如创建读取,更新和删除(CRUD),如表1所示:

表1. CRUD / HTTP映射
申请任务 HTTP命令
创造 POST
GET
更新资料 PUT
删除 DELETE

通过将类似于动词的请求与类似于名词的资源相关联,您最终得到了行为的逻辑表达—例如, GET该文档并DELETE该记录。

REST的真正父亲Roy Fielding在其博士论文中指出,REST“强调组件交互的可伸缩性,接口的通用性,组件的独立部署以及中间组件,以减少交互延迟,增强安全性并封装旧系统”(请参见相关主题 )。 构建RESTful系统并不困难,系统具有高度的可扩展性,同时还可以松散地耦合到基础数据。 他们还很好地利用了缓存。

Web上的所有内容(页面,图像等)本质上都是一种资源。 REST对命名资源而不是消息的依赖促进了应用程序设计中的松散耦合,因为它限制了基础技术的暴露。 例如,以下URL公开了资源,而不暗含任何有关基础技术的信息:http://thediscoblog.com/2008/03/20/unambiguously-analyzing-metrics/

该URL代表一种资源-名为“明确分析指标”的文章。 对此资源的请求利用了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-for-for-you和deleteArticle?name = rest-is-for-for-you,但此类调用会劫持HTTP GET命令和,在大多数情况下,请忽略已经可用(且成功的)HTTP基础结构。 换句话说,它们不是RESTful的。

REST的优点在于资源可以是任何东西,并且资源的表示方式也可以不同。 在前面的示例中,资源是HTML文件。 因此,响应的格式为HTML。 但是资源很可能是XML文档,序列化对象或JSON表示形式。 真的没关系。 重要的是资源已命名,并且与资源的通信不会影响其状态。 不影响状态很重要,因为无状态交互有助于扩展性。

你为什么要在乎呢?

用达芬奇的话说:“简单是最终的复杂性。” 万维网的实现非常简单,而且取得了不可否认的成功。 REST利用了Web的简单性,因此产生了高度可扩展的,松散耦合的系统,事实证明,该系统易于构建。

如您所见,构建RESTful应用程序最困难的部分是确定要公开的资源。 完成此操作后,使用Restlet框架即可轻松构建RESTful Web服务。

竞赛:构建RESTful API

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

RESTful种族

想象一下一个在线应用程序,该应用程序可以管理比赛者在不同距离内进行的比赛(例如,芝加哥马拉松比赛)。 该应用程序管理比赛(或事件)以及与之相关的跑步者。 并报告特定跑步者的时间(进行比赛需要多长时间)和排名(跑步者进入哪个位置)。 种族管理公司Acme Racing希望您构建一个RESTful Web服务,该服务使赞助商可以为特定种族创建新的种族和种族,并可以为特定种族提供正式结果。

Acme Racing已经有一个旧的胖客户端应用程序,该应用程序支持类似的要求,并利用简单的数据库和域模型。 因此,公开此功能的工作就是所有要做的事情。 请记住,REST的优点在于其与基础应用程序的隐式松散耦合。 因此,目前,您的工作不是担心数据模型或与之相关的技术-而是构造一个支持公司要求的RESTful API。

种族URI

Acme Races希望赞助商能够:

  • 查看现有比赛的详细信息
  • 创建新的种族
  • 更新现有种族
  • 删除种族

因为REST归结为命名资源,所以API变成了一系列URI模式,并且与资源相关联的行为是通过标准HTTP命令调用的。

如您所见,客户的需求很好地映射到CRUD。 从表1中可以知道,REST分别通过HTTP POSTGETPUTDELETE请求支持CRUD。 因此,支持这些要求的基本RESTful URI可以为http://racing.acme.com/race。 请注意,在这种情况下,race是客户端将使用的资源。

使用HTTP GET调用此URI将返回比赛列表。 (现在不必担心响应的格式。)要添加新的种族,您可以使用包含适当信息的HTTP POST调用相同的URI(例如,包含所需种族信息(例如名称)的XML文档。 ,日期和距离)。

要更新和删除现有种族,您需要对种族的特定实例进行操作。 因此,可以使用http://racing.acme.com/race/ race_id的URI来解决各个种族。 在这种情况下, race_id代表任何种族标识符(例如1或600米)的占位符。 因此,查看现有的比赛实例将是该URI的HTTP GET ; 更新或删除比赛分别是PUTDELETE请求。

Acme Racing还希望公开与比赛相关的跑步者的数据。 他们希望他们的服务提供支持:

  • 获取特定种族的所有跑步者。 此数据还应包括运行时间和已经完成的比赛的排名。
  • 为特定种族创建一个或多个跑步者。
  • 更新特定种族的跑步者信息(例如年龄)。
  • 删除特定种族的跑步者。

Acme还希望该服务允许用户查看特定种族中特定运动员的个人数据。

与种族一样,将RESTful URI应用于与种族相关联的跑步者也是一种逻辑练习。 例如,将通过对http://racing.acme.com/race/ race_id / runner的GET请求来查看特定种族的所有跑步者。

在比赛中获得个人数据的亚军将作为http://racing.acme.com/race/ race_id /跑步/ 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_id的 PUTDELETE请求。

因此,这些URI(均支持四个标准HTTP请求中的一些或全部)满足了Acme Racing的要求:

  • /种族
  • / 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 races Web服务将支持的资源。

种族URI

您在上一节中为Acme Racing构建的RESTful API涵盖了网络端点或URI,但没有涵盖资源。 就REST而言,资源的格式无关紧要,正如我之前提到的。 例如,您可以来回传递XML或二进制流。

XML可以说是在业务事务上下文中机器对机器通信的通用语言 ,因此构造RESTful服务将支持的一系列XML文档是有意义的。 赛车领域非常简单,您可以使用现有的数据模型,因此定义一些代表比赛和跑步者的XML文档的任务很简单。

例如,可以在XML中定义种族,如清单1所示:

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

注意, <race>有一个id ,清单1包含URI作为种族定义的一部分。 这是REST的关键方面,实际上是Web的重要方面-资源是相关的,应该链接在一起。 因此, <race>始终包含描述其RESTful表示形式的<uri>元素。 清单1中的XML可以说是GET请求对/ races / 1的响应。

要创建新的种族,您可以省略id方面(因为管理唯一ID是您正在此处构建的应用程序所控制的东西)。 这意味着您也可以排除<uri>元素。 因此,一个POST请求类似于清单2:

清单2.竞赛创建XML
<race name="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.与比赛相关的跑步者
<race name="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>
  <runner first_name="Linda" last_name="Smith" age="25" id="21">
   <uri>/races/9/runner/21</uri>
  </runner>
  <runner first_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
<race name="Mclean 1/2 Marathon" date="2008-05-12" distance="13.1" id="1">
 <uri>/races1</uri>
 <description />
 <runner first_name="Andrew" last_name="Glover" age="32" id="1">
  <uri>/races/1/runner/1</uri>
  <result time="100.04" place="45" />
 </runner>
</race>

请注意,如果比赛已经完成,则跑步者的成绩可以包含在XML文档中。 请记住,使用POST请求意味着创建跑步者。 因此, <runner>元素的id属性将不存在。

小便池

您已经定义了一个RESTful API,可以很好地映射到CRUDing竞赛和跑步者。 您已经定义了通信格式:XML文档。 在本节中,您将开始使用以servlet为模型的创新框架将所有内容整合在一起。

Restlet框架

Restlet应用程序类似于servlet应用程序,因为它们驻留在容器中,但实际上它们在两个主要方面有很大的不同。 首先,Restlets本身不直接使用HTTP或其状态指示(例如Cookie或会话)的直接概念。 其次,Restlet框架非常轻巧。 如您所见,可以使用从几个核心Restlet基类扩展的几个类来构建功能齐全的RESTful应用程序。 配置和部署利用现有容器模型,因此您只需更新常规的web.xml文件并部署标准的Web存档(WAR)文件。

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

比赛申请

通过从框架的Application类扩展,可以使用Restlet框架创建起点。 在此类中,您定义了响应URI的Resource 。 该定义过程由框架的Router类完成。 例如,如果您具有URI(例如order / order_id) ,则需要指定哪个对象可以处理这些请求。 该对象是框架的Resource类型的实例。 通过将对象附加到Router实例,可以将对象与URI链接起来,如清单5所示:

清单5.创建Router实例和映射URI
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_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
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类型称为Restlets 。 它们是使用Restlet框架开发的所有RESTful应用程序的核心。 与Application类型不同,基本Resource类不是抽象的。 它更像是具有默认行为的模板,您可以根据需要覆盖它。

在较高的层次上, Resource有四种需要覆盖的方法。 并非巧合的是,它们映射到是REST的试金石基本HTTP命令- GETPOSTPUTDELETE 。 因为Resource类是非抽象类,所以框架要求使用成对的方法来实现所需的行为。 例如,如果您希望特定的资源响应DELETE请求,则应首先实现delete()方法。 其次,您还必须实现allowDelete()方法,并使该方法返回true (默认为false )。 默认情况下,相应的PUTPOSTDELETE allow方法返回false ,而allowGet()方法返回true 。 这意味着对于只读Resource ,您只需要重写一个方法(在其他三种情况下,则不必重写两个方法)。 您也可以在Resource类中调用setModifcation(true) ,因此不必重写单个HTTP谓词allow方法。

例如, RacesResource旨在使用描述系统中种族的XML文档来响应GET请求。 用户还可以通过此Resource类型创建新的比赛。 因此, RacesResource类从Resource基类中重写至少三个方法:

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

请记住,默认情况下, Resource的实例是只读的。 因此,无需重写allowGet()方法。

生成XML文档

格式化资源中 ,我们决定利用XML作为在客户端和服务之间共享信息的数据机制。 因此,您的Restlets必须处理XML:在GET的情况下构建它,在POSTPUTDELETE的情况下使用它。 在本节中,您将利用Groovy脚本语言来减轻生成和处理XML文档的麻烦(请参阅参考资料 )。

利用Groovy

使用XML并非易事。 至少可以这样说,这可能很乏味且容易出错。 幸运的是,Groovy使使用XML变得更加容易。

您将利用Groovy的功能来生成XML,并完成处理XML文档的繁琐工作。 在Groovy中使用XML从未如此简单。 例如,解析XML文档是一件容易的事。 清单7中的XML文档:

清单7.一个简单的XML文档进行解析
<acme-races>
  <race name="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属性的值。 您所需要做的就是将XML文档的实例传递给Groovy的XMLSlurper类,调用parse()方法,然后导航到所需的元素或属性,如清单8所示:

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

如果需要描述,就像调用root.race.description.text()一样简单。

创建XML也很容易。 如果要在清单7中创建XML代码段,您要做的就是创建Groovy的MarkupBuilder类的实例,并向其添加节点,如清单9所示:

清单9.创建XML从未如此简单
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映射创建的,该映射将两者链接在一起(例如, name:"Alaska 200 below" )。

数据层

本节描述构成RESTful服务将重用的up数据层的现有域对象。

域对象

如您所知,从竞赛开始:构建RESTful API ,Acme Racing投资了先前项目的数据层,并希望将其重新用于新的Web服务。 当然,这使您的工作更加轻松。 简而言之,数据层由三个业务对象组成: RaceRunnerResult 。 它们由Spring和Hibernate有效管理; 但是,这些框架对您而言是隐藏的。 您只需拥有一个运行良好的JAR文件(即,可以轻松创建新的比赛,找到现有的跑步者,等等)。

业务对象支持一系列查找器方法,这些方法使获得种族和跑步者实例变得非常容易。 可以分别通过save()update()remove()方法对基础数据库进行持久化,更新和删除对象。

例如, Race对象支持一系列查找器方法,并有助于很好地处理持久数据。 Race对象的API很简单,如清单10所示:

清单10. Race的API
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的属性
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()方法
public void addParticipant(final Runner participant) ;

如您所见,使用此域模型非常容易。

构建和测试服务

既然您知道如何使用XML并且已经有要使用的数据层,那么该是时候继续使用Restlets构建RESTful应用程序并进行一些测试准备了。

比赛服务

回想一下,Acme Racing希望其服务能够使客户查看现有比赛并创建新比赛。 您已经概述了支持此行为的RESTful URI:/ race。

通过Router在类RaceApplication类,你挂这个URI到RacesResource类。 您已经知道必须实现三种方法:

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

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

清单13. RacesResource三参数构造RacesResource
public class RacesResource extends Resource {
 public RacesResource(Context context, Request request, Response response) {
  super(context, request, response);
 }
}

必须指示Restlets如何正确传达资源表示形式。 因为XML将用作资源格式,所以您必须通过添加XML变体类型来引导Restlet。 Restlets中的Variant代表Resource的格式。 基类Resource包含一个getVariants()方法,该方法有助于添加各种Variant类型。 因此,将清单14中的行添加到构造函数中:

清单14.将XML标记为变体
this.getVariants().add(new Variant(MediaType.TEXT_XML));

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

处理GET请求

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

清单15.重写getRepresentation()
public Representation getRepresentation(Variant variant) {
 return null;
}

如您所见,此方法返回一个Representation类型,该类型有多个实现。 一种实现方式-恰当地称为StringRepresentation表示字符串,将满足您的需求。

如您所知,您已经具有支持使用数据库的旧版域模型。 事实证明,有人已经编写了一个名为RaceReporter的实用程序类, RaceReporter将域对象转换为XML文档。 此类的racesToXml()方法采用Race实例的集合,并返回一个表示XML文档的String如清单16所示:

清单16. XML响应
<acme-races>
 <races>
  <race name="Leesburg 5K" date="2008-05-12" distance="3.1" id="5">
  <uri>/races/5</uri>
  <description/>
 </race>
 <race name="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,您可以创建两个可以正确初始化XMLUnit和DbUnit的装置,如清单17所示:

清单17.设置XMLUnit和DbUnit
@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方法中,通过CLEAN_INSERT命令将数据库的XML表示形式插入数据库中。 该XML文件有效地插入了六个不同的种族。 因此,对GET的响应将是一个包含六个种族的XML文档。

接下来,您可以创建一个测试用例,该用例在/ race URI上调用HTTP GET ,获取响应XML,然后使用XMLUnit的Diff类将其与控件XML文件进行Diff ,如清单18所示:

清单18.使用XMLUnit验证GET响应
@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文件
<acme-races>
 <races>
  <race name="Mclean 1/2 Marathon" date="2008-05-12" distance="13.1" id="1">
   <uri>http://localhost:8080/races/1</uri>
   <description/>
  </race>
  <race name="Reston 5K" date="2008-09-13" distance="3.1" id="2">
   <uri>http://localhost:8080/races/2</uri>
   <description/>
  </race>
  <race name="Herndon 10K" date="2008-10-22" distance="6.2" id="3">
   <uri>http://localhost:8080/races/3</uri>
   <description/>
  </race>
  <race name="Leesburg 1/2 Marathon" date="2008-01-02" distance="13.1" id="4">
   <uri>http://localhost:8080/races/4</uri>
   <description/>
  </race>
  <race name="Leesburg 5K" date="2008-05-12" distance="3.1" id="5">
   <uri>http://localhost:8080/races/5</uri>
   <description/>
  </race>
  <race name="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请求是一件轻而易举的事。 所需RaceReporter的就是在Race域对象上调用findAll方法,然后将该调用的结果传递给RaceReporterracesToXml()方法。 因此,您需要在构造函数中使用新的成员变量和新的初始化来更新RacesResource实例,如清单20所示:

清单20.不要忘记添加RaceReporter
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
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文件
<?xml version="1.0" encoding="UTF-8"?>
<web-app id="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类。

RESTful测试

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

对测试的另一种观察也开始解释一些事情。 Restlet的Client对象支持GETPUTPOSTDELETE的基本HTTP命令。 Client对象可以采用不同协议的形式-在这种情况下,您恰好依赖于HTTP。

您的GET请求已经有效(请参见图1),因此您可以编写另一个测试。 这次,充实所需的POST行为; 也就是说,通过RacesResource类测试新种族的创建。

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

要测试POST ,您需要使用相关信息来构建XML请求文档,并确保服务发送回成功的响应。 当然,事实证明,编写此测试非常简单。 您需要做的就是向现有的JUnit类添加一些其他代码,如清单23所示:

清单23. createRace测试用例
private static String raceXML = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
 "<acme-races>\n" +
 " <race name='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方法
public boolean allowPost() {
 return true;
}

public void post(Representation representation) {}

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

清单25.添加RaceConsumer
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()
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方法中发生了很多事情。 但是,经过仔细检查,事情并不像看起来那样聪明。 首先,通过Form对象获得传入的XML。 然后将XML(以String的形式)传递给consumer实例的createRace()方法。 如果事情工作(即,比赛被持久保存)时,产生一个响应,它包含一个成功的状态,然后传入的XML的重散列,加所得到的URI(即, race/43 ,其中43是id的新创建的种族)。

如果一切进展不顺利,则基本上执行相同的过程,只是返回失败状态并显示一条失败消息:不返回URI,因为未创建任何内容。

继续并重新运行createRace测试。 Assuming you've redeployed the RESTful Web application, things should work quite nicely!

A point RESTed

This tutorial managed to implement only a modest number of Acme Racing's requirements. But in the process of doing so, you've seen that working with Restlets is quite simple. The hardest part of the whole exercise was figuring out the logical RESTful API. The source code for this tutorial has all features implemented for your learning pleasure (see Download ).

The poet Alexander Pope said, "There is a certain majesty in simplicity which is far above all the quaintness of wit." This couldn't be more true when it comes to REST. Remember, REST is a way of thinking — a style of designing loosely coupled applications that rely on named resources rather than messages. And by piggybacking on the already validated and successful infrastructure of the Web, REST makes these applications simple to design and implement. And REST applications scale quite well.

This tutorial covered only a handful of the Restlet framework's features, but don't let that fool you. The framework does a lot, including adding security when you need it. Restlets are a joy to code, and the code base is easy understand once you see a few Restlets coded.

Albert Einstein said, "Everything should be made as simple as possible, but not simpler." I hope you agree with me that the Restlet framework and REST itself exemplify this mantra's wisdom.


翻译自: https://www.ibm.com/developerworks/java/tutorials/j-rest/j-rest.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值