最佳实践:如何更好地设计REST API

日期: 2011-04-06 作者:陈岩秦君 来源:TechTarget中国 英文

  由于REST可以降低开发的复杂度,提高系统的可伸缩性,增强系统的可扩展性,简化应用系统之间的集成,因而得到了广大开发人员的喜爱,同时得到了业界广泛的支持。比如IBM,Google等公司的很多产品都提供了REST API给开发人员;与此同时,大量的开源项目和云计算服务都提供了REST API接口。

  而在最近,一些新产品的开发甚至已经几乎完全抛弃了传统的类似JSP的技术, 转而大量使用REST风格的构架设计, 即在服务器端所有商业逻辑都以REST API的方式暴露给客户端, 所有浏览器用户界面使用widget、Ajax、HTML5 等技术,用HTTP的方式与后台直接交互。

  那么, 在REST API爆炸式增长的今天, 我们应该如何更好的设计我们的接口, 来提高我们的API的可用性,易用性,可维护性与可扩展性呢?本文将从以下方面与您探讨REST API设计方面的最佳实践:

  •   如何规划资源标识结构与URI模式
  •   如何根据应用场景提供内容协商
  •   如何正确的使用HTTP响应代码
  •   如何处理缓存和并发请求
  •   如何利用数据冗余和链接元素

  先决条件

  如果您具有如下知识与经验,将有助于您阅读和理解本文章的内容 。

  •   REST相关的基本知识;
  •   HTTP协议的基本知识;
  •   一定的Web开发经验。

  REST简介

  REST是英文Representational State Transfer的缩写,是近年来迅速兴起的,一种基于HTTP、URI以及XML这些现有协议与标准的,针对网络应用的设计和开发方式。它可以降低开发的复杂度,提高系统的可伸缩性。

  REST的核心是可编辑的资源及其集合,用符合Atom文档标准的Feed和Entry表示。每个资源或者集合有一个惟一的URI。系统以资源为中心,构建并提供一系列的Web服务。REST的基本概念和原则包括:系统上的所有事物都被抽象为资源;每个资源对应唯一的资源标识;对资源的操作不会改变资源标识本身;所有的操作都是无状态的;等等。

  在REST中,开发人员显式地使用HTTP方法,对系统资源进行创建、读取、更新和删除的操作:

  •   使用POST方法在服务器上创建资源
  •   使用GET方法从服务器检索某个资源或者资源集合
  •   使用PUT方法对服务器的现有资源进行更新
  •   使用DELETE方法删除服务器的某个资源

图 1. 用 HTTP 方法操作相册系统资源的简单范例

图 1. 用 HTTP 方法操作相册系统资源的简单范例

  更好的规划你的资源标识结构与URI模式

  REST 中,最基本的莫过于资源标识结构和URI模式了。更好的规划他们,是我们的API设计取得成功的最关键的一步。

  首先,我们来看看基本资源类型

  上文中提到,在REST构架的设计中,系统中的所有事物都被抽象为资源。

  在一个文档系统中,文档、目录、注释、草稿等等,是组成系统的资源。

  在一个银行系统中,客户信息、理财产品、利率信息、网点信息等等,是组成系统的资源。

  在一个航空客票系统中,旅客信息、机票订单、航班信息、机场信息等等,是组成系统的资源。

  这些资源,通常在系统中以“Entry”的形式出现。下面的代码样例向您展示了一个常见的“Entry”资源。

  清单 1. 一个简单的Entry资源样例
    

以下是引用片段:
 REST API 请求:
 GET http://example.com/航班信息/CA827/entry 
 <entry> 
 <id> CA827 </id> 
 <link href=”航班信息 /CA827/entry” rel=”self”/> 
 <title type=”text”>CA827 </title> 
 <published>2010-08-24T02:35:40.937Z </published> 
 <updated>2010-08-24T02:35:40.937Z </updated> 
 <ca:from>Beijing </ca:from> 
 <ca:to>Changsha </ca:to> 
 <ca:time>09:30:00 </ca:time> 
 <ca:aircraft>AB330 </ca:aircraft> 
 <summary type=”text”/> 
 </entry> 

  我们会注意到,这些资源,在描述了某种事物的同时,还有可能存在一定的层次结构关系。比如,文档从属于某个目录,注释从属于文档;旅客信息可以从属于机票订单,也可以从属于某个航班。

  当我们的资源有这种层次关系的时候,我们不妨在URI模式的设计中,用复合的URI来帮助开发者更好的理解和设计资源。

  比如, 针对一个文档的评论, 他的URI模式可以设计成如下:/文件夹/[文件夹名]/文件/[文件名]/评论/[ 评论唯一标示 ]。 这样,在构造和解析URI的过程中, 可以帮助开发者更好的理解系统,设计程序。

  其次,我们来看看集合资源类型

  资源,除了作为独立个体可以被访问,还可以由多个个体组合成一个集合,在系统中,通常以“feed”的形式存在。资源的集合, 可以是处于相同层次上,有相同从属关系的一组资源,比如一个文件夹下的所有文件; 也可以是根据某种条件查询出来的查询结果的资源集合,比如所有30岁以上40岁以下,拥有100万资产以上客户的名单。

  下面,我们来讨论一下设计集合类型资源的REST API时需要考虑的问题。

  使用过滤条件来帮助用户更准确地获取数据

  我们要返回的资源集合,无论是否有相同从属关系,大部分时候都需要进行必要的过滤,提供足够的过滤参数,查询参数, 能够帮助开发者高效的,准确地获取所需要的数据。 在服务器端过滤数据通常比客户端高效,并且减少了不必要的数据传输,可以大大减少网络开销,提高执行效率。

  最常见的过滤条件,是通过URL参数实现,比如/环境工程系/学生? 籍贯=北京&性别=女。

  很多时候,我们需要制定更加复杂的过滤条件,那么我们可以有两种选择:

  首先,我们可以使用正则表达式或者服务器可以理解的语法,比如/环境工程系/学生?filter=age between (15, 18)

  其次,我们还可以使用POST方法,携带一个文件来描述复杂的查询条件,文件的格式与语法通常需要在服务器端有相应的设计与定义。不过通常POST方法没有缓存机制,因此不是查询数据的首选。

  使用排序来帮助客户端更好的展现数据

  虽然进行客户端排序对于开发者来说是件轻而易举的事情,但是直接得到已经排序的返回结果,仍然是大部分开发者所期望的。尤其是很多时候,我们在浏览器,使用 Widget 展示结果,不适宜在客户端存储大量数据进行内存排序。

  排序, 通常有2个参数,一个是用来排序的字段,一个是排序的升序降续方式。比如我们可以用支持这样的参数组合的手段,提供基本的排序能力:?sortOrder=asc&sortField=age

  使用分页来帮助客户端处理大量数据

  由于返回的结果可能有几百几千条记录,将这些记录一次性的返回给客户端是不现实的,巨大的网络流量开销和客户端数据区的内存开销,都是我们在应用开发的时候不希望看到的,因此,如果你的集合资源有可能有大量的数据返回,请务必提供分页的功能支持。

  我们通常用一个以上参数来制定一个返回结果的区域,比较常见的有下面两种:

  一种常见于用固定行数的表格来展示数据,用当前处于第几页和每页返回多少行数据来确定需要的数据, 比如/所有学生?page=5&pagesize=50

  另外一种常见于用更加灵活的界面展示数据,用从第几行开始,一共返回多少行数据来确定需要的数据, 比如/所有学生?startIndex=27&count=22

  下面是一个来自IBM developerWorks的API样例,尝试请求该API,你可以看到该集合很好的支持了结果的分页与排序。同时我们从返回的信息中可以看到,每个文档Entry的URI都按照/社区库/[社区库ID]/文档/[文档ID]的复合URI的模式设计的。

  清单 2. IBM developerWorks的某个社区文件库的集合资源的API
    

以下是引用片段:
 REST API 请求:
 GET  https://www.ibm.com/developerworks/mydeveloperworks
 /files/form/anonymous/api/communitylibrary 
 /0a7c97bb-6cf9-4ddb-a918-80994e7b444d/feed? 
 pageSize=5&page=1&sK=modified&sO=dsc 

  最后,我们来讨论一些特殊资源类型

  理想的REST世界,一切事物都抽象为资源,一切操作都抽象为增删改查。然而,所有事物与操作都可以很容易的按照这个规则作抽象吗?让我们看看这个例子:

  检入和检出一个文档—-这个时候,我们要处理的资源是一个文档,然而增删改查似乎都无法与“检入检出”这个动作进行对应。当然,我们可以在文档资源中,设计一个检入检出状态的元素,通过编辑文档资源来实现。但是,这种设计从自然语义上看,并不是很贴切;并且增加了资源编辑操作的复杂度。

  如果我们来定义一个新的集合—-“我检出的文档”,用创建一个集合资源来对应检出(创建一个文档锁),用删除一个集合资源来对应检入(删除一个文档锁), 是不是逻辑上可以变得更加清楚?

  在REST这个以名词为核心的构架结构中,当你遇到一些动词特性比较强的操作,而又很难用原始资源的增删改查来匹配的时候,不妨换个思路, 通过引入新的逻辑资源集合的方式, 来进行API的设计与规划。

  理解和使用内容协商

  我们的开发者在发送一个REST API请求的同时,根据应用场景,针对相同的资源,可能会期待不同的返回形式。

  比如,我希望根据用户客户端语言,同一个资源的内容可以返回不同的语言。又比如,当我使用Java编程的时候,我希望得到ATOM格式的返回结果,而当我使用JavaScript编程的时候,我希望得到Json格式的返回结果。

  因此,我们在设计REST API的时候,应该提供完备的内容协商能力。

  使用URL参数进行内容协商

  最容易想到的自然是通过URL参数进行控制,我们经常看到形如/航班号/entry? format=JSON这样的URL。这种方式的优势就是简单灵活, 你可以通过任何 URL 参数来组合你的输出格式。

  下面是一个来自IBM developerWorks的API样例,尝试请求该API,你可以看到该集合是如何支持不同的输出格式请求的。

  清单 3. IBM developerWorks的文件服务标签云的API
    

以下是引用片段:
 REST API请求,要求返回XML格式数据:
 GET  https://www.ibm.com/developerworks/mydeveloperworks
 /files/form/anonymous/api/tags/feed?format=xml 
 &scope=document&pageSize=30&sK=cloud&sO=dsc 
 REST API请求,要求返回JSON格式数据:
 GET  https://www.ibm.com/developerworks/mydeveloperworks
 /files/form/anonymous/api/tags/feed?format=json 
 &scope=document&pageSize=30&sK=cloud&sO=dsc 

  使用Accept头进行内容协商

  使用URL参数,简单灵活,但是也由此带来了设计上的随意和不标准。并且,过多的参数会导致URL的可读性变差,更有甚者,可能会导致URL过长,超出规范,API请求无法执行。

  更为标准的内容协商方式是使用HTTP头。我们通常使用Accept来设置我们接受的返回结果的内容格式,用Accept-Charset来设置字符集,用Accept-Encoding来设置数据传输格式,用Accept-Language来设置语言。

  使用URI模式进行内容协商

  还有一种模式,就是将协商设置直接作为URI的一部分,将不同的返回视为不同的资源,比如/航班号/json来返回JSON格式的结果,用/航班号/atom来返回ATOM格式的结果。

  正确的使用HTTP响应代码

  作为API的设计者,正确的将API执行结果和失败原因用清晰简洁的方式传达给客户程序是十分关键的一步。 我们确实可以在HTTP的相应内容中描述是否成功,如果出错是因为什么, 然而, 这就意味着用户需要进行内容解析,才知道执行结果和错误原因。因此,HTTP响应代码可以保证客户端在第一时间用最高效的方式获知API运行结果,并采取相应动作。 下表列出了比较常用的响应代码。

  表 1. 常用HTTP响应代码含义

  使用HTTP头处理缓存和并发

  缓存和并发处理,从来是大型软件系统设计中的重要组成部分。

  使用HTTP头进行缓存处理

  在REST的构架中,我们除了在与后台的数据交换中,需要有一个良好的缓存机制外,针对REST API请求都是在远端用HTTP发起这一特点,还需要为网络缓存进行更多考虑。通过减少HTTP响应内容,避免不必要的HTTP连接等方式,达到提高REST API使用效率的目的。

  HTTP头中,有多个字段可以用于缓存处理。比较常用的有缓存控制和条件请求。

  缓存控制:

  缓存控制通常是需要客户端,缓存服务器 / 代理服务器与业务服务器一起发生作用。

  HTTP头中有“Cache-control”字段来控制如何使用缓存,常见的取值有private、no-cache、max-age、must-revalidate等。比如当你给返回的数据内容设置max-age=600,那么当用户隔了30秒再次请求的时候,就不会导致重新请求后台数据。

  另外,也可以通过“Expires”字段来指定内容过期时间,在此时间前的请求都不会导致后台程序重新请求数据。

  下图展示了max-age是如何工作的。

图 2. 缓存控制工作方式的简单范例

图 2. 缓存控制工作方式的简单范例

  条件请求与电子标签:

  很多时候,数据内容可能会几个小时甚至几天都不会发生变动,这个时候根据请求时间间隔来控制缓存,就不能满足系统的需求了。通过支持条件请求与电子标签,可以帮助我们来解决这个问题。

  当用户请求数据内容时,系统在返回数据的同时,在HTTP头中,将返回根据服务器内容的最后修改时间Last-Modified,或者根据服务器内容生成电子标签ETag。 当用户再次请求数据时,就可以在HTTP请求中使用If-Modified-Since或者If-None-Match头信息,把上次请求得到的时间戳或者电子标签传给服务器。当收到一个有条件请求的HTTP头的REST请求的时候,我们的程序需要将收到的时间戳或者电子标签与当前内容作比较,就可以很容易的知道用户请求的数据内容在这段时间是否发生过修改,并根据比较结果返回给用户最新内容,或者用HTTP响应码304告知用户,内容没有变化。

  下面是一个来自IBM developerWorks的API样例,尝试请求该API,你可以看到该API会在HTTP头中返回电子标签和缓存处理信息。

  清单 4. IBM developerWorks的带有电子标签的文件服务API
    

以下是引用片段:
 REST API 请求:
 GET  https://www.ibm.com/developerworks/mydeveloperworks
 /form/anonymous/api/communitylibrary 
 /7e2e8015-bf72-43b6-bacd-36565b67febc/document 
 /ddc0ef4e-224e-449c-bb2c-f919fafb17d2 
 /entry?acls=true&includeRecommendation=true 
 &includeTags=false&includeLibraryInfo=true&format=xml 

  使用HTTP头进行并发处理

  上文我们提到了使用条件请求控制缓存,其实我们还可以使用条件请求进行并发处理。

  比如当用户Alice和Bob通过REST获取了一篇文档。Bob阅读文档之后,通过PUT来修改文档;而此前几分钟,Alice刚刚修改了这篇文档,于是Bob就在毫不知情的情况下不慎覆盖了Alice的修改。

  通过在写操作中支持条件请求,我们可以更好的处理并发修改。用户在发出修改请求的同时,在HTTP请求中使用If-Not-Modified-Since或者If-Match头信息,把获取数据时得到的时间戳或者电子标签传给服务器;我们的程序通过与服务器当前内容的比较,就可以知道,这个修改请求是否是针对当前内容提出的。当服务器发现内容已经被其他用户修改过了,就不会执行修改请求,并返回HTTP响应码412(未满足前提条件)给用户。

  下图展示了使用条件请求和电子标签进行并发处理是如何工作的

图 3. 支持条件请求时的并发处理简单范例

图 3. 支持条件请求时的并发处理简单范例

  更好的使用数据冗余和链接元素

  在ATOM文档中,我们用各种数据元素来传递信息。其中有一类元素叫做链接,可以用于开发者的进一步访问。通常,我们会提供编辑当前资源的链接,访问当前资源的链接,等等。通过更加灵活的使用这类链接元素,以及提供必要的数据冗余,我们可以大大简化开发者的编程逻辑,提高REST API的使用效率 。

  首先,让我们来看看数据冗余的例子:

  我们在一个航班信息的文档中,通常会包括飞机的型号;而我们可能经常需要在显示航班信息的时候,同时显示更多的飞机信息(如单通道还是双通道,载客人数等)。这个时候,我们就需要对飞机型号的资源再发起一次请求,才能获得我们需要的信息。如果我们可以在请求航班信息的时候,返回飞机型号的同时获得更多的该型号的信息,就可以减少一次网络连接。为了保证API的灵活与效率,我们可以提供一个开关参数,如includeAircraftDetail=true。

  其次,让我们来看看链接元素的例子:

  我们要展示一个文件夹下面所有的文件,并允许用户察看每个文件都允许哪些人编辑,哪些人下载以及将某文件放入收藏夹。这时候,我们可以考虑将这些可以执行的操作的API都用链接元素的方式返回给客户端,这样,开发者无需自己拼接API调用的URL,就可以使用,从而降低代码复杂度。

  清单 5. 一个简单的使用链接元素的样例
    

以下是引用片段:
 REST API 请求:
 GET http://example.com/doc/docID12345/entry
 <entry> 
 <id> docID12345</id> 
 <link href=”doc/docID12345/entry” rel=”self”/> 
 <title type=”text”>Doc Title </title> 
 <published>2010-08-28T02:35:40.937Z </published> 
 <updated>2010-08-28T02:35:40.937Z </updated> 
 <atom:link doc:rel=”reader” 
 href=”/doc/docID12345/reader/feed” 
 rel=”related” type=”application/atom+xml”/> 
 <atom:link doc:rel=”editor” 
 href=”/doc/docID12345/editor/feed” 
 rel=”related” type=”application/atom+xml”/> 
 <atom:link ca:rel=”collect” 
 href=”/doc/collect?Add=docID12345″ 
 rel=”related” type=”application/atom+xml”/></entry> 

  更多的需要注意的细节与技巧

  除了以上提到的方面,还有大量的细节与技巧,可以帮助我们更好的设计REST API:

  批量更新:

  当用户需要更新多个资源的时候,你打算让开发者一次次的发送HTTP请求逐个更新吗?你可以考虑在设计API的时候允许客户同时创建或者更新多个资源。

  REST安全:

  除了使用固有的HTTP基本验证,你还可以考虑通过支持表单验证,LTPA验证,Open ID验证等方式,来满足更多的企业安全要求。

  文档服务:

  是否由于API持续更新,使得客户端连接不同版本服务的时候疲于奔命?尝试着把你的API定义规范成XML文档,这样客户端很容易理解当前服务可以提供哪些功能,以及如何使用这些功能。

  你还可以通过阅读其他文档得到更多这方面的指导,本文无法将所有的细节与技巧一一穷尽。

  总结

  通过以上的经验介绍和技巧举例,我们学习到了如何应用最佳实践来更好的设计REST API。我们注意到,由于REST API主要针对网络应用, 并且大量调用来自于浏览器脚本,因此在细节上有很多自己独特的需要注意的技巧。此外,由于REST越来越成为一种系统设计的原则与构架,也要求我们的程序开发人员在设计API的时候需要用构架师的视角与高度来思考。希望本文能够帮助您打开REST API设计的思路,摸索和总结出更多的技巧,与广大开发人员分享。

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

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

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

微信公众号

TechTarget微信公众号二维码

TechTarget

官方微博

TechTarget中国官方微博二维码

TechTarget中国

相关推荐