基于REST的Web服务:基础

日期: 2009-04-13 作者:Alex Rodriguez 来源:TechTarget中国 英文

  代表性状态传输(Representational State Transfer,REST)在Web领域已经得到了广泛的接受,是基于SOAP和Web服务描述语言(Web Services Description Language,WSDL)的Web服务的更为简单的替代方法。接口设计方面这一转变的关键证据是主流Web 2.0服务提供者(包括Yahoo、Google和Facebook)对REST的采用,这些提供者弃用或放弃了基于SOAP和WSDL的接口,而采用了更易于使用、面向资源的模型来公开其服务。在本文中,Alex Rodriguez将向您介绍REST的基本原理。

  基础

  REST定义了一组体系架构原则,您可以根据这些原则设计以系统资源为中心的Web服务,包括使用不同语言编写的客户端如何通过HTTP处理和传输资源状态。如果考虑使用它的Web服务的数量,REST近年来已经成为最主要的Web服务设计模型。事实上,REST对Web的影响非常大,由于其使用相当方便,已经普遍地取代了基于SOAP和WSDL的接口设计。

  REST这个概念于2000年由Roy Fielding在就读加州大学欧文分校期间在学术论文“Architectural Styles and the Design of Network-based Software Architectures”首次提出,他的论文中对使用Web服务作为分布式计算平台的一系列软件体系结构原则进行了分析,而其中提出的REST概念并没有获得现在这么多关注。多年以后的今天,REST的主要框架已经开始出现,但仍然在开发中,因为它已经被广泛接纳到各个平台中,例如通过JSR-311成为了Java? 6不可或缺的部分。

  本文认为,对于今天正在吸引如此多注意力的最纯粹形式的REST Web服务,其具体实现应该遵循四个基本设计原则:

  ·显式地使用HTTP方法。
  ·无状态。
  ·公开目录结构式的URI。
  ·传输XML、JavaScript Object Notation(JSON),或同时传输这两者。

  下面几个部分将详述这四个原则,并提供技术原理解释,说明为什么这些原则对REST Web服务设计人员非常重要。

  显式地使用HTTP方法

  基于REST的Web服务的主要特征之一是以遵循RFC 2616定义的协议的方式显式使用HTTP方法。例如,HTTP GET被定义为数据产生方法,旨在由客户端应用程序用于检索资源以从Web服务器获取数据,或者执行某个查询并预期Web服务器将查找某一组匹配资源然后使用该资源进行响应。

  REST要求开发人员显式地使用HTTP方法,并且使用方式与协议定义一致。这个基本REST设计原则建立了创建、读取、更新和删除(create, read, update, and delete,CRUD)操作与HTTP方法之间的一对一映射。根据此映射:

  ·若要在服务器上创建资源,应该使用POST方法。
  ·若要检索某个资源,应该使用GET方法。
  ·若要更改资源状态或对其进行更新,应该使用PUT方法。
  ·若要删除某个资源,应该使用DELETE方法。

  许多Web API中所固有的一个令人遗憾的设计缺陷在于将HTTP方法用于非预期用途。例如,HTTP GET请求中的请求URI通常标识一个特定的资源。或者,请求URI中的查询字符串包括一组参数,这些参数定义服务器用于查找一组匹配资源的搜索条件。至少,HTTP/1.1 RFC是这样描述GET方法的。但是在许多情况下,不优雅的Web API使用HTTP GET来触发服务器上的事务性操作——例如,向数据库添加记录。在这些情况下,GET请求URI属于不正确使用,或者至少不是以基于REST的方式使用。如果Web API使用GET调用远程过程,则应该类似如下:

  GET /adduser?name=Robert HTTP/1.1

  这不是非常优雅的设计,因为上面的Web方法支持通过HTTP GET进行状态更改操作。换句话说,该HTTP GET请求具有副作用。如果处理成功,则该请求的结果是向基础数据存储区添加一个新用户——在此例中为Robert。这里的问题主要在语义上。Web服务器旨在通过检索与请求URI中的路径(或查询条件)匹配的资源,并在响应中返回这些资源或其表示形式,从而响应HTTP GET请求,而不是向数据库添加记录。从该协议方法的预期用途的角度看,然后再从与HTTP/1.1兼容的Web服务器的角度看,以这种方式使用GET是不一致的。

  除了语义之外,GET的其他问题在于,为了触发数据库中的记录的删除、修改或添加,或者以某种方式更改服务器端状态,它请求Web缓存工具(爬网程序)和搜索引擎简单地通过对某个链接进行爬网处理,从而意外地做出服务器端更改。克服此常见问题的简单方法是将请求URI上的参数名称和值转移到XML标记中。这样产生的标记是要创建的实体的XML表示形式,可以在HTTP POST的正文中进行发送,此HTTP POST的请求URI是该实体的预期父实体(请参见清单1和2):

  清单1. 之前

  GET /adduser?name=Robert HTTP/1.1
            
  清单2. 之后

  POST /users HTTP/1.1
  Host: myserver
  Content-Type: application/xml
  <?xml version=”1.0″?>
  <user>
    <name>Robert</name>
  </user>
            
  上述方法是基于REST的请求的范例:正确使用HTTP POST并将有效负载包括在请求的正文中。在接收端,可以通过将正文中包含的资源添加为请求URI中标识的资源的从属资源,从而处理该请求;在此例下,应该将新资源添加为/users的子项。POST请求中指定的这种新实体与其父实体之间的包含关系类似于某个文件从属于其父目录的方式。客户端设置实体与其父实体之间的关系,并在POST请求中定义新实体的URI。

  然后客户端应用程序可以使用新的URI获取资源的表示形式,并至少逻辑地指明该资源位于/users之下,如清单3所示。

  清单3. HTTP GET请求

  GET /users/Robert HTTP/1.1
  Host: myserver
  Accept: application/xml
 
  以这种方式使用GET是显式的,因为GET仅用于数据检索。GET是应该没有副作用的操作,即所谓的等幂性属性。

  当支持通过HTTP GET执行更新操作时,也需要应用类似的Web方法重构,如清单4所示。

  清单4. 通过HTTP GET进行更新

  GET /updateuser?name=Robert&newname=Bob HTTP/1.1
 
  这更改了资源的name特性(或属性)。虽然可以将查询字符串用于此类操作,清单4就是一个简单的例子,但是在用于较复杂的操作时,这种将查询字符串作为方法签名的模式往往会崩溃。由于您的目标是显式使用HTTP方法,鉴于上述的相同原因(请参见清单5),更符合REST的方法是发送HTTP PUT请求以更新资源,而不是发送HTTP GET。

  清单5. HTTP PUT请求

  PUT /users/Robert HTTP/1.1
  Host: myserver
  Content-Type: application/xml
  <?xml version=”1.0″?>
  <user>
    <name>Bob</name>
  </user>
 
  使用PUT取代原始资源可以提供更清洁的接口,这样的接口与REST的原则以及与HTTP方法的定义一致。清单5中的PUT请求是显式的,因为它通过在请求URI中标识要更新的资源来指向该资源,并且它在PUT请求的正文中将资源的新表示形式从客户端传输到服务器,而不是在请求URI上将资源属性作为参数名称和值的松散集合进行传输。清单5还具有将资源从Robert重命名为Bob的效果,这样做会将其URI更改为/users/Bob。在REST Web服务中,使用旧的URI针对该资源的后续请求会产生标准的404 Not Found错误。

  作为一般设计原则,通过在URI中使用名词而不是动词,对于遵循有关显式使用HTTP方法的REST指导原则是有帮助的。在基于REST的Web服务中,协议已经对动词(POST、GET、PUT和DELETE)进行了定义。在理想的情况下,为了保持接口的通用化,并允许客户端明确它们调用的操作,Web服务不应该定义更多的动词或远程过程,例如/adduser或/updateuser。这条通用设计原则也适用于HTTP请求的正文,后者旨在用于传输资源状态,而不是用于携带要调用的远程方法或远程过程的名称。

  无状态

  REST Web服务需要扩展以满足日益提高的性能要求。具有负载平衡和故障转移功能、代理和网关的服务器集群通常以形成服务拓扑的方式进行组织,从而允许根据需要将请求从一个服务器路由到另一个服务器,以减少Web服务调用的总体响应时间。要使用中间服务器扩大规模,REST Web服务需要发送完整、独立的请求;也就是说,发送的请求包括所有需要满足的数据,以便中间服务器中的组件能够进行转发、路由和负载平衡,而不需要在请求之间在本地保存任何状态。

  完整、独立的请求不要求服务器在处理请求时检索任何类型的应用程序上下文或状态。REST Web服务应用程序(或客户端)在HTTP Header和请求正文中包括服务器端组件生成响应所需要的所有参数、上下文和数据。这种意义上的无状态可以改进Web服务性能,并简化服务器端组件的设计和实现,因为服务器上没有状态,从而消除了与外部应用程序同步会话数据的需要。

  图1演示了一个有状态的服务,某个应用程序可能向其请求多页结果集中的下一个页面,并假设该服务跟踪应用程序在结果集中导航时的离开位置。在这个有状态的设计中,该服务递增并在某个位置存储previousPage变量,以便能够响应针对下一个页面的请求。

             

  图1. 有状态的设计
 
  类似如此的有状态的服务变得复杂化了。在Java Platform, Enterprise Edition(Java EE)环境中,有状态的服务需要大量的预先考虑,以高效地存储会话数据和支持整个Java EE容器集群中的会话数据同步。在此类环境中,存在一个Servlet/JavaServer Pages(JSP)和Enterprise JavaBeans(EJB)开发人员非常熟悉的问题,他们经常在会话复制过程中艰难地查找引发java.io.NotSerializableException的根源。无论该异常是由Servlet容器在HttpSession复制过程中引发的,还是由EJB容器在有状态的EJB复制过程中引发的,这都是个问题,会耗费开发人员几天的时间,尝试在构成服务器状态并且有时非常复杂的对象图表中查明没有实现Serializable的对象。此外,会话同步增加了开销,从而影响服务器性能。

  另一方面,无状态的服务器端组件不那么复杂,很容易跨进行负载平衡的服务器进行设计、编写和分布。无状态的服务不仅性能更好,而且还将大部分状态维护职责转移给客户端应用程序。在基于REST的Web服务中,服务器负责生成响应,并提供使客户端能够独自维护应用程序状态的接口。例如,在针对多页结果集的请求中,客户端应该包括要检索的实际页编号,而不是简单地要求检索下一页(请参见图2)。

       

  图2. 无状态的设计
 
  无状态的Web服务生成的响应链接到结果集中的下一个页编号,并允许客户端完成所需的相关工作以便保留此值。可以作为大致的分离将基于REST的Web服务设计的这个方面划分为两组职责,以阐明如何维护无状态的服务:

  服务器

  ·生成响应,其中包括指向其他资源的链接,以使得应用程序可以在相关资源之间导航。此类响应嵌入了链接。类似地,如果请求是针对父或容器资源,则基于REST的典型响应还可能包括指向父资源的子资源或从属资源的链接,以便这些资源保持连接在一起。
  ·生成响应,其中指明了是否可缓存,以通过减少针对重复资源的请求数量或通过完全消除某些请求来改进性能。服务器通过包括Cache-Control和Last-Modified(日期值)HTTP响应Header实现此目的。

  客户端应用程序

  ·使用Cache-Control响应Header确定是否缓存资源(创建资源的本地副本)。客户端还读取Last-Modified响应Header,并在If-Modified-Since Header中发回日期值,以向服务器询问资源是否已更改。这称为条件GET(Conditional GET),两个Header同时进行,因为服务器的响应为标准304代码(Not Modified),如果请求的资源自从该时间以后尚未更改,则省略实际的资源。HTTP响应代码304意味着客户端可以安全地将资源表示形式的缓存本地副本作为最新版本使用,从而实际上跳过了后续GET请求,直到资源更改为止。
  ·发送可独立于其他请求得到服务的完整请求。这要求客户端充分利用Web服务接口指定的HTTP Header,并在请求正文中发送完整的资源表示形式。客户端发送的请求极少对先前的请求、某个会话在服务器上的存在性、服务器向请求添加上下文的能力或请求之间保留的应用程序状态做出假设。

  客户端应用程序与服务之间的这种协作对于基于REST的Web服务中的无状态性极为重要。它通过节省带宽和最小化服务器端应用程序状态改进了性能。

  公开目录结构式的URI

  从对资源寻址的客户端应用程序的角度看,URI决定了REST Web服务将具有的直观程度,以及服务是否将以设计人员能够预测的方式被使用。基于REST的Web服务的第三个特征完全与URI相关。

  REST Web服务URI的直观性应该达到很容易猜测的程度。将URI看作是自身配备文档说明的接口,开发人员只需很少(如果有的话)的解释或参考资料即可了解它指向什么,并获得相关的资源。为此,URI的结构应该简单、可预测且易于理解。

  实现这种级别的可用性的方法之一是定义目录结构式的URI。此类URI具有层次结构,其根为单个路径,从根开始分支的是公开服务的主要方面的子路径。根据此定义,URI并不只是斜杠分隔的字符串,而是具有在节点上连接在一起的下级和上级分支的树。例如,在一个收集从Java到报纸的各种主题的讨论线程服务中,您可能定义类似如下的结构化URI集合:

  http://www.myservice.org/discussion/topics/{topic}

  根/discussion之下有一个/topics节点。该节点之下有一系列主题名称,例如闲谈、技术等等,每个主题名称指向某个讨论线程。在此结构中,只需在/topics/后面输入某个内容即可容易地收集讨论线程。

  在某些情况下,指向资源的路径尤其适合于目录式结构。例如,以按日期进行组织的资源为例,这种资源非常适合于使用层次结构语法。

  此示例非常直观,因为它基于规则:

  http://www.myservice.org/discussion/2008/12/10/{topic}

  第一个路径片段是四个数字的年份,第二个路径片断是两个数字的日期,第三个片段是两个数字的月份。这样解释它可能有点愚蠢,但这就是我们追求的简单级别。人类和计算机能够容易地生成类似如此的结构化URI,因为这些URI基于规则。在语法的空隙中填入路径部分就大功告成了,因为存在用于组合URI的明确模式:

  http://www.myservice.org/discussion/{year}/{day}/{month}/{topic}

  在考虑基于REST的Web服务的URI结构时,需要指出的一些附加指导原则包括:

  ·隐藏服务器端脚本技术文件扩展名(.jsp、.php、.asp)——如果有的话,以便您能够移植到其他脚本技术而不用更改URI。
  ·将所有内容保持小写。
  ·将空格替换为连字符或下划线(其中一种或另一种)。
  ·尽可能多地避免查询字符串。
  ·如果请求URI用于部分路径,与使用404 Not Found代码不同,应该始终提供缺省页面或资源作为响应。

  URI还应该是静态的,以便在资源发生更改或服务的实现发生更改时,链接保持不变。这可以实现书签功能。URI中编码的资源之间的关系与在存储资源的位置表示资源关系的方式无关也是非常重要的。

  传输XML、JSON或同时传输这两者

  资源表示形式通常反映了在客户端应用程序请求资源时的资源当前状态及其属性。这种意义上的资源表示形式只是时间上的快照。这可以像数据库中的记录表示形式一样简单,其中包括列名称与XML标记之间的映射,XML中的元素值包含行值。或者,如果系统具有数据模型,那么根据此定义,资源表示形式是系统的数据模型中的对象之一的属性快照。这些对象就是您希望您的REST Web服务为客户端提供的资源。

  基于REST的Web服务设计中的最后一组约束与应用程序和服务在请求/响应有效负载或HTTP正文中交换的数据的格式有关。这是真正值得将一切保持简单、可读和连接在一起的方面。

  数据模型中的对象通常以某种方式相关,应该以在将资源传输到客户端应用程序时表示资源的方式,反映数据模型对象(资源)之间的关系。在讨论线程服务中,连接的资源表示形式的示例可能包括根讨论主题及其属性,以及指向为该主题提供的响应的嵌入链接。

  清单6. 讨论线程的XML表示形式

  <?xml version=”1.0″?>
  <discussion date=”{date}” topic=”{topic}”>
    <comment>{comment}</comment>
    <replies>
      <reply from=”joe@mail.com” href=”/discussion/topics/{topic}/joe”/>
      <reply from=”bob@mail.com” href=”/discussion/topics/{topic}/bob”/>
    </replies>
  </discussion>
 
  最后,为了赋予客户端请求最适合它们的特定内容类型的能力,您的服务的构造应该利用内置的HTTP Accept Header,其中该Header的值为MIME类型。基于REST的服务使用的一些常见MIME类型如表1所示。

       

  表1. 基于REST的服务使用的常见MIME类型

  这使得服务可由运行在不同平台和设备上并采用不同语言编写的各种各样的客户端所使用。使用MIME类型和HTTP Accept Header是一种称为内容协商的机制,这种机制允许客户端选择适合于它们的数据格式,并最小化服务与使用服务的应用程序之间的数据耦合。

  结束语

  REST并非始终是正确的选择。它作为一种设计Web服务的方法而变得流行,这种方法对专有中间件(例如某个应用程序服务器)的依赖比基于SOAP和WSDL的方法更少。在某种意义上,通过强调URI和HTTP等早期Internet标准,REST是对大型应用程序服务器时代之前的Web方式的回归。正如您已经在所谓的基于REST的接口设计原则中研究过的一样,XML over HTTP是一个功能强大的接口,允许内部应用程序(例如基于Asynchronous JavaScript+XML(Ajax)的自定义用户界面)轻松连接、定位和使用资源。事实上,Ajax与REST之间的完美配合已增加了当今人们对REST的注意力。

  通过基于REST的API公开系统资源是一种灵活的方法,可以为不同种类的应用程序提供以标准方式格式化的数据。它可以帮助满足集成需求(这对于构建可在其中容易地组合(Mashup)数据的系统非常关键),并帮助将基于REST的基本服务集扩展或构建为更大的集合。本文仅略微谈到了基础,但愿本文的讨论会诱发您继续探索该主题。

  关于作者

  Alex Rodriguez是在IBM工作的一名软件工程师,主要从事分布式Java技术和REST Web服务方面的工作。他从JDK 1.1.7B发布以来就一直在进行Java编程工作,擅长于设计和开发基于Java EE的软件。

我们一直都在努力坚持原创.......请不要一声不吭,就悄悄拿走。

我原创,你原创,我们的内容世界才会更加精彩!

【所有原创内容版权均属TechTarget,欢迎大家转发分享。但未经授权,严禁任何媒体(平面媒体、网络媒体、自媒体等)以及微信公众号复制、转载、摘编或以其他方式进行使用。】

微信公众号

TechTarget微信公众号二维码

TechTarget

官方微博

TechTarget中国官方微博二维码

TechTarget中国

相关推荐