rest-简介

 一说到REST,我想你们的第一反应就是“啊,就是那种先后台通讯方式。”可是在要求详细讲述它所提出的各个约束,以及如何开始搭建REST服务时,却不多有人可以清晰地说出它究竟是什么,须要遵照什么样的准则。html

  在您将看到的这一篇文章中,咱们将对REST,尤为是基于HTTP的REST服务进行详细地介绍。经过这些文章,您不只能够了解到什么是REST,更能清晰地了解到您在编写REST服务时所须要遵照的各个守则,设计RESTful API时须要考虑的各类因素以及实现过程当中可能遇到的问题等内容。web

 

REST示例算法

  我想,不少读者可能并不太清楚REST究竟是一个什么概念。那么,首先让咱们来看一个简单的基于HTTP的REST服务示例。数据库

  假设用户正在访问一个电子商务网站www.egoods.com。该网站对其所销售的各个物品进行了详细分类。当用户登陆该网站进行购物时,他首先须要在该网站上选择其所须要寻找物品的分类,进而列出属于该分类的各个物品。json

  固然,虽然从业务逻辑的角度来讲这个流程很是简单,但实际上浏览器向后台发送了多个请求:页面逻辑在页面加载时将首先获得全部的商品分类,并将这些分类显示在了页面中。在用户选择了一个分类的时候,页面逻辑将发送一个请求获得该分类的详细信息,并发送另一个请求来获得该分类的商品列表:api

  在经过浏览器的调试功能查看这些请求的时候,咱们能够看到其首先向www.egoods.com/api/categories发送一个GET请求,以取得全部的商品分类:数组

1 GET /api/categories
2 Host: www.egoods.com
3 Authorization: Basic xxxxxxxxxxxxxxxxxxx
4 Accept: application/json

  而服务端将返回全部的类别:浏览器

 1 HTTP/1.1 200 OK
 2 Content-Type: application/json
 3 Content-Length: xxx
 4  
 5 [
 6    {
 7       "label" : "食品",
 8       "url" : "/api/categories/1"
 9    }, {
10       "label" : "服装",
11       "url" : "/api/categories/2"
12    }
13    ...
14    {
15       "label" : "电子设备",
16       "url" : "/api/categories/25"
17    }
18 ]

  该响应返回了一个用JSON表示的数组。该数组中的每一个元素包含了两部分信息:用户可以读懂的表示分类名称的label以及相应分类所对应的URL。其中Label所记录的分类名称将在页面中显示给用户。而在用户根据label所标示的分类名选择了一个分类的时候,页面逻辑会取得该分类所对应的URL并向该URL 发送请求,以获得该分类的详细信息。例如在用户点击了“食品”这个分类的时候,浏览器将会向服务器发送以下的请求:缓存

1 GET /api/categories/1
2 Host: www.egoods.com
3 Authorization: Basic xxxxxxxxxxxxxxxxxxx
4 Accept: application/json

  这一次,页面逻辑根据用户对分类的选择“食品”来获得了其所对应的URL,并向该URL发送了一个GET请求。而该请求所获得的响应则为:性能优化

HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: xxx
 
{
   "url" : "/api/categories/1",
   "label" : "Food",
   "items_url" : "/api/items?category=1",
   "brands" : [
         {
            "label" : "友臣",
            "brand_key" : "32073",
            "url" : "/api/brands/32073"
         }, {
            "label" : "乐事",
            "brand_key" : "56632",
            "url" : "/api/brands/56632"
         }
         ...
   ],
   "hot_searches" : …
}

  该响应略为复杂。首先,响应中的URL标示了“食品”分类所对应的URL。而label属性则和前面同样,用来在页面上显示分类的名称。一个较为特殊的属性则是items_url。其用来标示获取属于食品分类的各个产品的URL。而属性brands则用来列出在“食品”分类中的著名品牌,例如友臣,乐事等。这些品牌被组织为一个对象数组,而数组中的每一个对象都拥有label,url等属性。在这些属性的帮助下,页面能够列出这些著名品牌的名称,并容许用户经过点击跳转到这些品牌所对应的页面上。除了这些属性以外,Food分类还包含了其它一系列属性,如表示当前其它用户正在搜索的hot_searches属性等,这里就再也不赘述。

  该响应有一个问题,那就是符合用户筛选条件的各个产品并无包含在该响应中。这是由于页面所列出的各个产品是根据用户所设置的筛选条件,即其选择的品牌以及搜索关键字而变化的。所以,页面逻辑会根据属性items_url以及用户所设定的搜索条件组合成为目标URL,再次发送请求到后台,以请求须要在页面中展示的各个物品。

  例如用户在只想浏览属于乐事品牌的食品时,其能够钩选乐事这个品牌,那么此时的URL将由食物分类的items_url以及表示按照品牌进行筛选的URL参数共同组成:

1 GET /api/items?category=1&brand_key=56632
2 Host: www.egoods.com
3 Authorization: Basic xxxxxxxxxxxxxxxxxxx
4 Accept: application/json

  如今让咱们来总结一下上面所展现的基于HTTP的REST系统的整个运行流程。在开始的时候,咱们拿到了全部分类的列表。列表中的各个条目不只仅包含了用户能够看到的分类名称等信息,更拥有一个额外的URL属性。在用户选择该列表中的一项时,页面逻辑将会向对应的URL发送一个请求,以得到该项目的详细信息。在这个详细信息中,一些内容又包含了一些其它的URL,从而使得页面逻辑又能经过该URL属性发送请求。

  您也许会说,哎,这不和咱们现有系统的运行流程同样的嘛。是的。在上面所举出的例子中,咱们也更偏重地描述了REST系统所须要具备的HATEOAS(Hypermedia As The Engine Of Application State)特性。正是因为这个特性已经在你们所建立的系统里面普遍地使用了,所以我更但愿从熟悉的地方入手,而不是开始就很是教条地说REST必定要这样,必定要那样,徒增了学习的难度。

  反过来讲,上面所展现的REST服务并不具备典型性。在充分了解了REST后,您会发现,REST在系统设计上的视角将再也不把流程放在了最优先的位置。

  而在后面的章节中,咱们则会逐渐展开,详细地介绍如何建立一个纯正的基于HTTP的REST服务。

 

REST的定义

  OK,如今让咱们来看看REST的定义。Wikipedia是这样描述它的:

 

Representational State Transfer (REST) is a software architecture style consisting of guidelines and best practices for creating scalable web services. REST is a coordinated set of constraints applied to the design of components in a distributed hypermedia system that can lead to a more performant and maintainable architecture.

 

  从上面的定义中,咱们能够发现REST实际上是一种组织Web服务的架构,而并非咱们想象的那样是实现Web服务的一种新的技术,更没有要求必定要使用HTTP。其目标是为了建立具备良好扩展性的分布式系统。

  反过来,做为一种架构,其提出了一系列架构级约束。这些约束有:

  1. 使用客户/服务器模型。客户和服务器之间经过一个统一的接口来互相通信。
  2. 层次化的系统。在一个REST系统中,客户端并不会固定地与一个服务器打交道。
  3. 无状态。在一个REST系统中,服务端并不会保存有关客户的任何状态。也就是说,客户端自身负责用户状态的维持,并在每次发送请求时都须要提供足够的信息。
  4. 可缓存。REST系统须要可以恰当地缓存请求,以尽可能减小服务端和客户端之间的信息传输,以提升性能。
  5. 统一的接口。一个REST系统须要使用一个统一的接口来完成子系统之间以及服务与用户之间的交互。这使得REST系统中的各个子系统能够独自完成演化。

  若是一个系统知足了上面所列出的五条约束,那么该系统就被称为是RESTful的。

  下面咱们再次经过电子商务网站egoods这个示例来帮助咱们理解这些约束。首先,egoods是一个电子商务网站。用户须要经过浏览器,手机或者网站所发布的浏览应用来访问该网站的内容。所以其使用的天然是客户/服务器模型。而在浏览过程当中,用户须要访问不一样类型的数据,如商品描述、购物车等信息。这些信息可能由egoods网站服务中不一样的服务器来提供的,所以在用户浏览过程当中可能须要与不止一个服务器进行交互。若是在服务端保存了有关客户的任何状态,那么在用户与不一样服务器进行交互的时候,客户的状态就须要在这些服务之间进行同步,大大地增长了系统的复杂度。所以,REST要求客户端自行维护状态,并在每次发送请求的时候提供自身所储存的处理该请求所必需的信息。而恰当地使用缓存这一条也很是容易理解。在客户端请求一个自上次请求后没有发生过变化的信息时,如产品分类列表,服务端仅仅须要返回一个304响应便可。

  这里您能够看到,前四条约束中除了无状态这条约束较为特别以外,其它三条约束在基于HTTP的Web服务中都很常见,也较容易达成。而无状态约束在其它类型的Web服务中并不十分常见,所以如何避免违反该约束是在实现REST服务时最常讨论的话题。其不只仅会影响到不少功能的设计,更是REST系统扩展性的关键。所以在后面的章节中,咱们会对无状态约束单独进行讲解。

  在简单地介绍了前四个约束以后,咱们就须要着重讲解统一接口这个约束了。能够说,前面的四个约束实际上都较为容易达成。惟一须要注意的无非是是否某些技术实现违反了这些约束。而第五条约束,统一接口,能够说是REST服务设计的核心所在,也是决定REST服务设计的成败之处。在实现一个基于HTTP的REST服务时,软件开发人员不只仅须要考虑REST所设置的一系列约束,更须要考虑HTTP各组成的语意,HTTP相关技术如何与REST服务约束结合,如何保持先后向兼容性以及如何进行版本管理等问题,才能给出一个天然的,具备较高易用性和较强生命力的REST系统。

  而在介绍统一接口约束以前,咱们则须要了解一下和REST密切相关的两个名词:资源和状态。能够说,资源是REST系统的核心概念。全部的设计都会以资源为中心,包括如何对资源进行添加,更新,查找以及修改等。而资源自己则拥有一系列状态。在每次对资源进行添加 ,删除或修改的时候,资源就将从一个状态转移到另一个状态。

  好比说,在egoods中,商品的分类就是一种资源。该资源有不少实例,包括表示食品的分类,其所对应的URL是“/api/categories/1”。一样地,食品的品牌也是一种资源。这些资源的实例都对应着一个当前的状态。在修改了一个资源实例以后,好比修改了食品分类中的热搜关键字,那么其将对应着一个新的状态。这种状态之间的变化被称为是状态的转移。

  在大概了解了REST系统中的资源和状态的定义后,咱们来看看统一接口这个约束。该约束又包含了四个子约束:

  1. 每一个资源都拥有一个资源标识。每一个资源的资源标识能够用来惟一地标明该资源。
  2. 消息的自描述性。在REST系统中所传递的消息须要可以提供自身如何被处理的足够信息。例如该消息所使用的MIME类型,是否能够被缓存等。
  3. 资源的自描述性。一个REST系统所返回的资源须要可以描述自身,并提供足够的用于操做该资源的信息,如如何对资源进行添加,删除以及修改等操做。也就是说,一个典型的REST服务不须要额外的文档对如何操做资源进行说明。
  4. HATEOAS。即客户只能够经过服务端所返回各结果中所包含的信息来获得下一步操做所须要的信息,如究竟是向哪一个URL发送请求等。也就是说,一个典型的REST服务不须要额外的文档标示经过哪些URL访问特定类型的资源,而是经过服务端返回的响应来标示到底能在该资源上执行什么样的操做。一个REST服务的客户端也不须要知道任何有关哪里有什么样的资源这种信息。

  如今,让咱们仍然以egoods做为示例来解释一下上面四个子约束。

  在前面的章节中,咱们已经看到了从egoods所返回的表示食品这个分类的响应:

 1 HTTP/1.1 200 OK
 2 Content-Type: application/json
 3 Content-Length: xxx
 4 
 5 {
 6    "url" : "/api/categories/1",
 7    "label" : "Food",
 8    "items_url" : "/api/items?category=1",
 9    "brands" : [
10          {
11             "label" : "友臣",
12             "brand_key" : "32073",
13             "url" : "/api/brands/32073"
14          }, {
15             "label" : "乐事",
16             "brand_key" : "56632",
17             "url" : "/api/brands/56632"
18          }
19          ...
20    ],
21    "hot_searches" : …
22 }

  首先咱们看到的是,该响应经过Content-Type响应头来标示响应中所包含的信息是按照JSON格式来组织的。在看到了该响应头中所标示的格式以后,消息的接收方就能够按照JSON的格式理解或分析该响应中的负载。这也即是消息的自描述性。

  固然,消息的自描述性不只仅包含如何解析其所携带的负载。在一个基于HTTP的REST系统中,咱们能够经过使用大部分HTTP标准所提供的功能来提升消息的自描述性。因为这些功能已经拥有了完备的文档,被广大的软件开发人员所熟知,并获得了众多浏览器厂商以及Web类库的支持,所以根据这些标准实现REST服务具备较高的消息自描述性。举例来讲,若是在请求中标明了If-Modified-Since头,那么服务端将可能返回一个304 Not Modified响应。在看到该响应的时候,浏览器或其它浏览工具能够从缓存中取得上一次获得的结果。所以,在一个基于HTTP的REST系统中,如何准确地使用HTTP协议是一项很是重要的内容。

  在获知了如何对响应所携带的负载进行解析以后,咱们就来看看资源的自描述性。在上面的示例中,服务端响应使用了JSON表示了食品分类。该表示首先经过label属性描述了本身是一个什么分类。接下来,其经过brands属性表示了该分类中的著名品牌,并经过hot_searches标示了在该分类中的热搜关键字。能够看到,该负载中的全部属性都清晰地描述了自身所表达的含义。

  那在该资源表示中的url属性是什么意思?实际上这是为子约束“每一个资源都拥有一个资源标识”所添加的一个属性。该子约束要求每一个资源的资源标识能够用来惟一地标明该资源。对于网络应用来讲,资源标识就是URI。而在一个基于HTTP的系统中,最天然的资源标示即是URL。在表示单个资源的时候,这个URL经常会包含着资源在该类资源中的ID。

  在本文的其它章节中,咱们就将以这种方式来区分URL和ID:URL用来指向资源所在的地址,而ID则表示该资源在该类型资源中的ID。请读者必定要记得这两个术语所对应的不一样意义,以防止理解错误。

  如今还有一部分食品分类表示中的属性没有被讲解,那就是在该表示中的各个URL。这是为子约束HATEOAS服务的。在用户看到items_url属性时,其就能够经过向该URL发送GET消息获得属于食品分类中的全部商品的列表。而在商品品牌的表示中也拥有一个url属性。也就是说,向该URL发送一个GET请求也可以获得相应品牌的详细信息。

  您可能会问:既然在介绍HATEOAS时说REST服务并不须要文档来告诉用户哪里拥有什么样的资源,那用户应该如何知道向/api/categories发送GET请求就能获得全部的分类呢?标准的作法则是向/api直接发送一个GET请求:

1 GET /api
2 Host: www.egoods.com
3 Authorization: Basic xxxxxxxxxxxxxxxxxxx
4 Accept: application/json

  而在返回的响应中将标示出REST API的版本以及全部能够访问的资源等信息:

 1 HTTP/1.1 200 OK
 2 Content-Type: application/json
 3 Content-Length: xxx
 4 
 5 {
 6    "version": "1.0",
 7    "resources": [
 8       {
 9          "label" : "Categories",
10          "description" : "Product categories",
11          "uri": "/api/categories"
12       }, {
13          "label" : "Items",
14          "description" : "All items on sell",
15          "uri": "/api/items"
16       }
17    ]
18 }

  能够看到,在该响应中列出了能够被访问的两种资源:表示商品分类的Categories以及表示商品的Items。在须要访问特定类型的资源时,软件开发人员能够经过直接向这两种资源所对应的URI发送GET请求便可。

  OK,相信如今读者已经了解了REST服务所提供的各类约束。那么在后面的章节中,咱们将会逐步讲解如何设计一个基于HTTP的REST服务。

 

资源识别

  在通常状况下,对资源的识别一般都是REST服务设计的第一步。在准确地识别出了各资源以后,怎么用HTTP规范中的各组成来表示这些资源即是瓜熟蒂落的事情。在本节中,咱们将对如何识别REST系统中的资源进行讲解。

  在一般的软件开发过程当中,咱们经常须要分析达成某个目标所须要使用的业务逻辑,并为业务逻辑的执行提供一系列运行接口。在一些Web服务中,这些接口经常表达了某个动做,如将商品放入购物车,提交订单等。这一系列动做组合在一块儿就能够组成完成目标所须要执行的业务逻辑。在须要调用这些接口的时候,软件开发人员须要向这些接口所在的URL发送一个请求,从而驱使服务执行该动做。

  而在REST服务中,咱们所提供的各个接口则须要是一系列资源,而业务逻辑须要经过对资源的操做来完成。也就是说,REST服务中的API将再也不以执行了什么动做为中心,而是以资源为中心。一些对资源的通用操做有添加,取得,修改,删除,以及对符合特定条件的资源进行列表操做。

  仍然让咱们以上面所举的“将商品放入购物车”这个操做为例。在一个REST系统中,购物车将被抽象为一个资源,而“将商品放入购物车”这个操做将被解释为对购物车这个资源的更新:更新购物车,以使特定商品包含在购物车内。

  可能对于刚刚学习REST的各位读者而言,这种以资源为中心的描述方法有些别扭。这种描述方法的确有别于不少Web服务那样以动做为中心。而与之对应的则是系统设计步骤的改变:咱们将再也不首先是别完成业务逻辑所需的各动做,而是支持业务逻辑所须要的各资源。那么咱们应该如何抽象出这些资源呢?首先,咱们对某个操做不要再关注它所执行的动做,而是关心它所操做的宾语。一般状况下,该宾语就会是REST系统中的资源。

  在这里,咱们就以“提交订单”做为示例来展现如何抽象资源。

  首先,在“提交订单”这个动做中,订单是宾语。所以对于该业务逻辑,其将做为一个资源存在。除此以外,在订单中还须要包含一系列信息,例如订单中所包含的商品,订单所属人等。一旦这些均可以被该REST系统中的其它资源使用,那么它们也将成为独立的资源。

  可是有时候,一个动做可能并不存在着它所操做的宾语。在这种状况下,咱们就须要考虑该动做产生或消除了哪一个实体,或者哪一个实体的状态发生了变化。这个发生了变化的实体实际上就是一种资源。例如对于登录这一行为,其实际上在服务端建立了一个会话实例。该会话实例中则包含了登录IP,登录时间,以及登录时所用的凭证等。再好比对于用户更改密码这种行为,其所操做的资源就是用户资料。

  在抽象资源的过程当中,咱们须要按照自顶向下的方式,即首先辨识出系统中的最主要资源,而后再辨识这些主要资源的子资源,并依次进行迭代。

  对主资源的抽取主要经过分析业务逻辑来完成。在获得功能需求之后,咱们首先要分析这些业务逻辑所操做的宾语。这些宾语可能有两种状况:主资源或者其它资源的子资源。主资源实际上就是可以独立存在的一系列资源。而子资源则须要依附于主资源之上才能表达实际的意义。同时各个子资源也可能拥有自身的子资源。

  判断一个资源是不是子资源的一个方法就是看它是否能独立地表示其具体含义。例如对于一个egoods上所销售的商品,其名称,价格,简介等属性能够清晰地描述该商品究竟是什么,到底如何销售。所以这些商品其实是一个主资源。可是每种商品所支持的邮递服务须要是一个子资源:一个商品能够支持多种邮递服务。这些邮递服务根据派送距离等须要不一样的价格,也提供了不一样的邮递速度。因为这些邮递服务与商家和邮递服务公司所达成的服务价格有关,而且会因为商品重量的变化而变化,所以这些邮递服务并不能为其它商家所提供的邮递服务做为参考,所以其应该做为该商品的一个子资源。

  或者也能够说,若是一个资源是主资源,那么其能够被不一样的资源实例包含引用而不会产生歧义。而若是一个资源是子资源,那么被不一样的资源实例引用可能会产生歧义。

  可是须要注意的是,一种资源可能有多种不一样的表现形式。例如对于在使用列表展现各个商品的时候,egoods只须要展现商品的名称,一个对该商品的简单描述,商品的价格以及一张商品的照片。而在用户打开了该商品页以后,页面则须要显示更详尽的信息,如商品的重量,商品所在地等等。

  除此以外,资源列表也有可能拥有多种不一样的表现形式。举例来讲,若是egoods上属于某个分类的商品太多,须要分页显示,那么这种分页是否也应该是一种资源?答案是,这些分页并非一种资源,而其只是资源列表的一种表现方式。在每页所包含商品数量,排序规则等条件发生变化的时候,该资源列表中所包含的各个商品也会发生变化。

  那么如何判断咱们为REST服务所定义的资源是否合理呢?通常状况下,我都使用下面的一些判断方法:

  首先,咱们须要考虑对该资源的CRUD是否有意义,从而验证资源的定义是否合理。就以刚刚说到的列表的分页显示为例,咱们能够想象一下如何对分页进行添加和删除?一旦删除了该分页,那么属于该分页中的各个商品也应该被删除么?并且删除了分页X的数据后,本来X + 1分页的数据将展现在X分页中。很显然,将商品的分页定义为资源并不合理。

  其次,咱们须要检查资源是否须要除CRUD以外的动词来操做。该方法用来检查资源中是否还有子资源没有被抽象。若是该资源还须要额外的动词,那么咱们就须要考虑这些操做到底引发了什么样的状态变化,进而抽象出该资源的子资源。

  除此以外,咱们还须要检查这些资源是不是被总体使用,建立和删除。该方法用来探测是否一个子资源应该是一个主资源。若是在删除一个资源的时候,其子资源还能够被其它资源重用,那么该子资源实际上具备较高的重用性,应该是一个主资源。

 

资源的URL设计

  在前面已经提到过,统一接口约束中的第一条子约束就是每一个资源都拥有一个资源标识。在正确地辨识出了一个资源以后,咱们就须要为这些资源分配其所对应的URI。一个资源所对应的URI可能有多种表示方式,如究竟是用单数仍是复数表示资源等。所以在一个基于HTTP的REST系统中,如何组织针对各个资源的URL其实是最重要的一部分。毕竟一个明确的,有意义而且稳定的API接口其实是对服务对用户的一种承诺。

  在HTTP中,一个URL主要由如下几个部分组成:

  1. 协议。即HTTP以及HTTPS。
  2. 主机名和端口。如www.egoods.com:8421
  3. 资源的相对路径。如/api/categories。
  4. 请求参数。即由问号开始的由键值对组成的字符串:?page=1&page_size=20

  在为一个资源设计其所对应的URL时,咱们须要着重考虑第三部分和第四部分组成。

 

经过URL来表示资源

  在辨识出了REST系统中的各个资源之后,咱们就须要开始为这些资源设计各自所对应的URL了。

  首先要介绍的是,全部的资源都应该存在于一个相对路径之下。请读者回忆以前咱们介绍的经过向/api发送一个GET请求获得全部能够被访问的资源这个示例:

 1 GET /api
 2 Host: www.egoods.com
 3 Authorization: Basic xxxxxxxxxxxxxxxxxxx
 4 Accept: application/json
 5 
 6 HTTP/1.1 200 OK
 7 Content-Type: application/json
 8 Content-Length: xxx
 9 
10 {
11    "version": "1.0",
12    "resources": [
13       {
14          "label" : "Categories",
15          "description" : "Product categories",
16          "uri": "/api/categories"
17       }, {
18          "label" : "Items",
19          "description" : "All items on sell",
20          "uri": "/api/items"
21       }
22    ]
23 }
 

  所以对于从向该相对路径发送请求才能获得的各个主资源来讲,将它们置于相对路径/api之下是很是合理的。

  除了这个缘由以外,API的版本更迭也是一个考虑。假如软件开发人员须要开发一个新版本的REST API,那么他可能就须要从新抽象并定义系统中的各个资源。可是若是两个版本的API中都拥有一个categories资源,而且系统为了保持后向兼容性同时保留了两个版本的API,那么将只有一个资源可使用/categories这个相对路径。也正由于如此,将这些资源置于相对路径/api之下,并在第二个版本的API出现以后将新的资源抽象置于/api-v2下是一种较为流行的作法。

  在明确了全部的资源都应该置于/api这样一个相对路径下以后,咱们就来说解如何为资源定义对应的URL。一个最简单的状况是:指定主资源所对应的URL。因为主资源是一类独立的资源,所以它应该直接置于/api下。例如egoods网站中的产品分类就是一个主资源,咱们会为其分配以下URL:

1 /api/categories

  而对于其它主资源,如egoods网站中的产品,咱们也会为其赋予一个具备相似结构的URL:

1 /api/items

  这样,每类主资源都将拥有一个特定于该类资源的URL。这些URL就对应着相应资源实例的集合。

  若是须要表示某个主资源类型中的特定实例,那么咱们就须要在该类主资源所对应的URL以后添加该实例的ID。如egoods网站中的食品分类的ID为1,那么其所对应的URL就将是:

1 /api/categories/1

  一个较为特殊的状况则是,对于某种类型的主资源,整个系统将有且仅有一个该类型资源的实例。那么该资源将再也不须要经过ID来访问。我能想到的一个例子就是对整个系统进行介绍的资源。该资源实例所对应的URL将是:

1 /api/about

  而一个资源实例中还可能拥有子资源。这些子资源与资源实例之间的关系主要有两种状况:资源实例包含了一个子资源的集合,以及资源实例仅仅能够包含一个子资源。对于资源实例包含了一个子资源集合的状况,咱们须要将该子资源集合的URL置于该资源的相对路径下。例如对于egoods上所销售的ID为23456的商品所提供的邮递服务,咱们将使用以下的URL:

1 /api/items/23456/shipments

  在该URI中,/api/items/23456对应的就是商品自己,而该商品所提供的邮递服务则是该商品的子资源。与主资源特定实例所具备的URI相似,其中一个ID为87256的邮递服务所对应的URI则为:

1 /api/items/23456/shipments/87256

  若是资源实例仅仅能够包含一个子资源,那么对该子资源的访问也将再也不须要ID。如当前商品的折扣信息:

1 /api/items/23456/discount

 

单数 vs. 复数

  接下来要考虑的一点是,资源在URL中须要由单数表示仍是复数表示?这在stackoverflow等众多论坛上已经成为了一个经久不衰的话题。咱们知道,在一个基于HTTP的REST系统中,一个资源所对应的URL实际上也就是对其进行操做的URL。所以适当地使用单数和复数对于该系统的用户而言有必定的指示做用。在stackoverflow上的一个常见观点是:若是一个URL所对应的资源是使用复数表示的,那么该类型的资源可能有多个。对该URL发送Get请求可能返回该资源的一个列表。反之,若是一个URL所对应的资源是使用单数表示的,那么该类型的资源将只有一个,所以对该URL发送Get请求将只返回该资源的一个实例。

  以egoods中的商品分类为例。因为一个网站所售卖的商品可能有多种类别,所以其须要在URL中使用复数形式:/api/categories。而对于一个该网站的用户而言,因为其只会有一个我的偏好设置,所以其URL则须要使用单数形式:/api/users/{user_id}/preference。

  你可能会问:若是须要获得具备特定ID的某个实例时,咱们应该对该资源使用单数仍是复数呢?答案是复数。这是由于在经过特定ID访问某个资源的实例实际上就是从该资源的集合中取出特定实例。所以表示该资源集合的URL实际上仍然须要使用复数形式,而其后所使用的ID则标明了其所访问的是资源中的单一实例,所以向这个URL发送Get请求将返回该资源的单一实例。

  就以“食品”分类为例。该分类所对应的URL为/api/categories/1。该URL中的前半部分/api/categories表示egoods网站中全部分类的集合,而1则表示在该分类集合中的ID为1的分类。

 

相对路径 vs. 请求参数

  另外一个常常致使疑惑的地方就是针对资源的某一种特征,咱们究竟是将其定义为URL中相对路径的一部分仍是做为请求参数。

  请考虑下面一个例子。在egoods网站中,咱们售卖的手机主要有苹果,三星等品牌。那么在为这些手机设计URL的时候,咱们是否须要按照品牌对这些手机进行细分,从而用户只要经过向/api/mobiles/brands/apple发送请求就能列出全部的苹果手机?仍是说,直接将手机的品牌置于请求参数中,从而经过/api/mobiles?brand=apple来列出全部的苹果手机?

  在判断究竟是使用请求参数仍是相对路径时,咱们通常分为下面几步。

  首先,可选参数通常都应置于请求参数中。仍以egoods中的手机为例。在选择手机时,用户能够选择品牌以及颜色。若是将品牌和颜色都定义在相对URL中,那么具备特定品牌和颜色的手机将能够经过两个不一样的URL访问:/api/mobiles/brand/{brand}/color/{color}以及/api/mobiles/color/{color}/brand/{brand}。就用户而言,其并没有法了解这两个URL所表示的是同一类资源仍是不一样类型的资源。固然,您能够说,咱们只用/api/mobiles/brand/{brand}/color/{color}。可是该URL将没法处理用户仅仅选择了颜色,却没有选择品牌的状况。

  其次,不是全部字符均可以在URL中被使用,如汉字,标点。为了处理这种状况,包含这些字符的筛选条件须要置于请求参数中。

  最后,若是该特征下包含子资源,那么它自身也就是一个资源,所以须要以相对路径的方式展示它。例如在egoods网站中,每件商品所属于的分类仅仅是它的一个特征。可是一个分类更包含了属于它的各个品牌以及热搜关键字等众多信息。所以它实际上是一个资源,须要在URI路径中表示它。

  总的来讲,既然使用HTTP来构建REST系统,那么咱们就须要遵照URL各组成中的含义:URL中的相对路径将用来标示“What I want”,也既对应着资源;而请求参数则用来标示“How I want”,即查看资源的方式。

 

使用合适的动词

  在知道了如何为每种资源定义URI以后,咱们来看看如何操做这些资源。

  首先,在一个资源的生命周期以内经常会发生一系列通用事件(CRUD)。一开始,一个资源并不存在。只有用户或REST服务建立了该资源之后其才存在,也便是上面所列出的通用事件中的C,Create。在一个资源建立完毕之后,用户可能会从服务端请求该资源的表示,也就是上面所列出的通用事件的R,Retrieve。在特定状况下,用户可能决定要更新该资源,所以会使用上面的通用事件中的U,即Update来更新资源。而在资源再也不须要的时候,用户可能须要经过通用事件D,即Delete来删除该资源。同时用户有时也须要列出属于特定类型资源的资源实例,即经过List操做来获得属于特定类型的资源的列表。

  在前面的讲解中咱们已经提到过,在REST系统中的每一个资源都有一个特定的URI与之对应。HTTP协议提供了多种在URI上操做的动词,如GET,PUT,POST以及DELETE等。所以在一个基于HTTP的REST服务中,咱们须要使用这些HTTP动词来表示如何对这些资源进行CRUD操做。而在什么状况下到底使用哪一个动词则是由这些动词自己在HTTP协议中的意义所决定的。

  这其中GET和DELETE两个动词的含义较为清晰:

 

The GET method means retrieve whatever information (in the form of an entity) is identified by the Request-URI.

The DELETE method requests that the origin server delete the resource identified by the Request-URI.

 

  也就是说,在须要读取某个资源的时候,咱们向该资源所对应的URI发送一个GET请求便可。相似的,在须要删除一个资源的时候,咱们只须要向该资源所对应的URI发送一个DELETE请求便可。而在但愿获得某类型资源的列表的时候,咱们能够直接向该类型资源所对应的URI发送一个GET请求。

  而动词PUT和POST则是较为容易混淆的两个动词。在HTTP规范中,POST的定义以下所示:

 

  The POST method is used to request that the origin server accept the entity enclosed in the request as a new subordinate of the resource identified by the Request-URI in the Request-Line

 

  也就是说,POST动词会在目标URI之下建立一个新的子资源。例如在向服务端发送下面的请求时,REST系统将建立一个新的分类:

1 POST /api/categories
2 Host: www.egoods.com
3 Authorization: Basic xxxxxxxxxxxxxxxxxxx
4 Accept: application/json
5 
6 {
7    "label" : "Electronics",
8    ……
9 }

  

  而PUT的定义则更为晦涩一些:

 

The PUT method requests that the enclosed entity be stored under the supplied Request-URI. If the Request-URI refers to an already existing resource, the enclosed entity SHOULD be considered as a modified version of the one residing on the origin server. If the Request-URI does not point to an existing resource, and that URI is capable of being defined as a new resource by the requesting user agent, the origin server can create the resource with that URI."

 

  也就是说,PUT则是根据请求建立或修改特定位置的资源。此时向服务端发送的请求的目标URI须要包含所处理资源的ID:

1 POST /api/categories/8fa866a1-735a-4a56-b69c-d7e79896015e
2 Host: www.egoods.com
3 Authorization: Basic xxxxxxxxxxxxxxxxxxx
4 Accept: application/json
5 
6 {
7    "label" : "Electronics",
8    ……
9 }

  能够看到,二者都有建立的含义,可是意义却不一样。在决定究竟是使用PUT仍是POST来建立资源的时候,软件开发人员须要考虑一系列问题:

  首先就是资源的ID是如何生成的。若是但愿客户端在建立资源的时候显式地指定该资源的ID,那么就须要使用PUT。而在由服务端为该资源自动赋予ID的时候,咱们就须要在建立资源时使用POST。在决定使用PUT建立资源的时候,防止资源URI与其它资源所具备的URI重复的任务须要由客户端来保证。在这种状况下,客户端经常使用GUID/UUID做为将资源的ID。可是到底使用GUID/UUID仍是由服务端来生成ID不只仅和REST有关,更会对数据库性能等多个方面产生影响。所以在决定使用它们以前要仔细地考虑清楚。

  同时须要注意的是,由于REST要求客户只能够经过服务端返回结果中所包含的信息来获得下一步操做所须要的信息,所以客户端仅仅能够决定资源的ID,而URI中的其它部分则须要从以前获得的响应中取得。

  可是软件开发人员经常会进入另一个误区不少人认为REST服务中的HATEOAS只能经过Hyperlink完成。实际上在Roy对REST的定义中使用的是Hypermedia,即响应中的全部多媒体信息。就像Roy在其我的网站上所说(http://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven):

 

A REST API must not define fixed resource names or hierarchies (an obvious coupling of client and server). Servers must have the freedom to control their own namespace. Instead, allow servers to instruct clients on how to construct appropriate URIs, such as is done in HTML forms and URI templates, by defining those instructions within media types and link relations.

 

  另一个须要考虑的因素则是PUT的等幂性是否对REST系统的设计有所帮助。因为在同一个URI上调用两次PUT所获得的结果相同。所以用户在没有接到PUT请求响应时能够放心地重复发送该响应。这在网络丢包较为严重时是一个很是好的功能。反过来,在同一个URI上调用两次POST将可能建立两个独立的子资源。

  除此以外,还须要考虑是否将资源的建立和更新归结为一个API能够简化用户对REST服务的使用。用户能够经过PUT动词来同时完成建立和更新一个资源这两种不一样的任务。这样的好处在于简化了REST服务所提供的接口,可是反过来也让一个API执行了两种不一样的任务,在必定程度上违反了API设计时每一个API都须要有明确的意义这一原则。

  所以在决定到底使用POST仍是PUT来完成资源的建立以前,请考虑上面所列出的三条问题,以肯定到底哪一个动词更加适合。

  除此以外,另一对相似的动词则是PUT和PATCH。二者之间的不一样则在于PUT是对整个资源的更新,而PATCH则是对部分资源的更新。而该动词的局限性则在于对该动词的支持程度。毕竟在某些类库中并无提供原生的对PATCH动词的支持。

 

使用标准的状态码

  在与REST服务进行交互的时候,用户须要经过服务所返回的信息决定其所发送的请求是否被适当地处理。这部分功能是由REST服务实现时所使用的协议所决定的,与REST架构无关。而在基于HTTP的REST服务中,该功能就由HTTP响应的状态码(Status Code)来完成。所以在设计一个REST服务时,咱们须要额外地注意是否返回了正确的状态码。

  可是这些预约义的HTTP状态码并不能知足全部的状况。有时候一个REST服务所但愿返回的错误信息可以更加精确地描述问题,例如在用户重设密码时,咱们须要在用户所输入原密码与系统中所记录的密码不匹配时返回“您所输入的密码有误”这样的消息。在HTTP协议中,咱们并无办法找到一个可以精确地表示该意义的状态码。

  所以在一般状况下,REST服务都会在响应中额外地提供一个说明性的负载来告知用户到底产生了什么问题。例如对于上面的重设密码失败的状况,服务端可能会返回以下响应:

1 HTTP/1.1 400 Bad Request
2 Content-Type: application/json
3 Content-Length: xxx
4 
5 {
6    "error_id" : "100045",
7    "header" : "Reset password failed",
8    "description" : "The original password is not correct"
9 }

  上面的示例响应中主要包含如下的说明性信息:

  1. 服务端响应的状态码。页面逻辑能够经过判断该状态码是不是4XX或5XX来判断是否请求出错,从而在页面中展现一个警告对话框。
  2. 服务所提供的内部错误ID。一般状况下,该内部错误ID也须要在警告对话框中展现出来。从而容许软件用户根据内部错误ID来获取支持服务。
  3. 错误的标题及简述。经过该错误的标题及简述,软件用户可以了解系统内部到底发生了什么,并在是用户输入错误的时候容许用户自行修改错误并从新发送正确的请求。

  在该错误中,最关键的当属服务端的响应代码。一个响应代码不只仅标示了请求是否成功,更有用户该如何操做的含义。例如对于401 Unauthorized响应代码而言,其表示该响应没有提供一个合法的身份凭证,所以须要用户首先执行登录操做以获得一个合法的身份凭证,而后该资源可能就能够被访问了。而403 Forbidden响应代码则表示当前请求已经提供了一个合法的身份凭证,可是该身份凭证并无访问该资源的权限,所以使用该身份凭证登录从新登录系统等操做并不能解决问题。

  所以在返回错误信息以前,软件开发人员首先须要考虑清楚在响应中到底应该使用什么样的响应代码。而正确地选择响应代码则创建在软件开发人员对这些响应代码拥有一个正确的理解的前提下。

  固然,要将全部的响应代码彻底理解也须要大量的工做,并且REST服务的用户也可能并无那么多的领域知识来了解全部的响应代码的含义。所以在不少基于HTTP的REST系统中,系统在标示错误时只使用一系列经常使用的响应代码,如400,401,403,404,405,500,503等。在用户请求被处理时,系统将返回200 OK,表示请求已经被处理。而在处理时发生错误时则尽可能使用这些响应代码来表示。若是一个错误较为复杂,那么直接返回400或500,并在响应的负载中提供具体的错误信息。

  不得不说的是,这种作法有时显得简单粗暴,尤为是对于一个开放平台而言则更是致命的。当一个第三方厂商为一个开放平台开发一个应用软件,却每次只能获得一个400错误,那么其内部应用逻辑将没法判断究竟是哪里出了问题。为了能让用户知道这里产生了错误,该第三方软件只能将开放平台所给出的信息直接显示给用户。可是这些信息其实是创建在开放平台这个语境下的,所以对于第三方厂商的用户而言,这些信息晦涩难懂,甚至可能一点帮助也没有。

  也就是说,到底如何组织这些响应代码须要用户根据所编写的项目决定,尤为是该产品的使用者来决定。在定义一个平台时,尽可能使用更多的HTTP响应代码,由于用户极有可能经过该平台编写本身的第三方软件。而在为一个普通的产品定义REST API时,将响应代码定得很是专业可能反而致使易用性的降低。

  另一点须要说明的是,我的不建议使用Wikipedia查找各个状态码的含义,而应该使用RFC所描述的各状态码的定义。 IANA提供了一张各个状态码所对应的RFC协议的列表,从而能够很容易地找到各个状态码所对应的RFC协议以及其所在的章节。该列表的地址为:http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml

  之因此不建议使用Wikipedia的缘由主要有两点:

  1. 描述不够详细。在RFC定义中,每一个状态码都对应着一段或多段文字,而且解释很是清晰。而在Wikipedia中,每一个状态码经常只有一句话。
  2. 不够准确。在Wikipedia的Reference节中,咱们能够看到一系列特定平台所定义的状态码,如Spring Framework所定义的420 Method Failure等。这很是具备误导性。

 

选择适当的表示结构

  接下来咱们要讲解的就是如何为资源定义一个恰当的表示。

  首先须要强调的是,REST并无规定其服务中须要使用什么格式来表示资源。表示资源时所能够选取的表示形式其实是由实现REST所使用的协议决定的。而在一个基于HTTP的REST服务中,咱们可使用JSON,也可使用XML,甚至是自定义的MIME类型来表示资源。这些表现形式经常是等效的。相信读者已经看到,本系列文章会使用JSON来表示这些资源。

  一个REST服务经常会同时支持多种客户端。这些客户端可能会使用不一样的协议来与服务进行沟通。并且就算是使用相同的协议,不一样的客户端所能够接受的负载表示形式也会有所不一样。所以客户端须要与REST服务协商在通信过程当中所使用的负载。

  客户端和服务端对所使用负载类型的协商一般都按照协议所规定的标准协商过程来完成。例如对于一个基于HTTP的REST服务,咱们就须要使用Accept头来标示客户端所能够接受的负载类型:

1 GET /api/categories
2 Host: www.egoods.com
3 Authorization: Basic xxxxxxxxxxxxxxxxxxx
4 Accept: application/json

  而在服务端支持的状况下,返回的响应就将使用该MIME类型组织其负载:

1 HTTP/1.1 200 OK
2 Content-Type: application/json
3 Content-Length: xxx

  在这里咱们再重复一次:REST是一种组织Web服务的架构,其只在架构方面提出了一系列约束。能够说,全部对REST的讲解都已经在前两个章节,即“REST的定义”以及“资源识别”中完成了。而有关客户端和服务端如何进行沟通,为资源定义什么样的URI,使用什么格式的数据进行沟通等讨论都是在阐述如何将REST架构所提出的各类约束和基于HTTP协议的Web服务结合在一块儿。毕竟在一般状况下,实现一个单纯的技术不难,可是如何将多种技术规范天然地混合在一块儿,构成一个天然的,成熟稳定的解决方案才是项目开发中的难点。HTTP协议并非为REST架构所定义的,所以如何用HTTP协议来恰当地描述一个REST服务才是本文所着重介绍的。

 

负载的自描述性

  在前面对REST提出的几个约束的讲解中咱们已经提到过,REST系统中所传递的各个消息的负载须要提供足够的用于操做该资源的信息,如如何对资源进行添加,删除以及修改等操做,并能够根据负载中所包含的对其它各资源的引用来访问各个资源。这也对负载的自描述性提出了更高的要求。

  首先让咱们回头看看egoods电子商务网站对食品分类的描述:

 1 {
 2    "uri" : "/api/categories/1",
 3    "label" : "Food",
 4    "items_url" : "/api/items?category=1",
 5    "brands" : [
 6          {
 7             "label" : "友臣",
 8             "brand_key" : "32073",
 9             "url" : "/api/brands/32073"
10          }, {
11             "label" : "乐事",
12             "brand_key" : "56632",
13             "url" : "/api/brands/56632"
14          }
15          ...
16    ],
17    "hot_searches" : …
18 }

  我想读者在看到该响应以后可能就已经明白了不少域的含义。但仍是让咱们依次对这些域进行讲解。

  第一个要讲解的是url域。该域用来标示该资源所对应的URL。可能您会问:既然咱们就是从这个URL返回的该资源,那么为何咱们还须要在该资源中保存一个它所对应的URL呢?首先这是由于在统一接口约束中要求每一个资源都拥有一个资源标识。在这里咱们使用URL做为标识。而另外一些基于HTTP的REST系统中,用来做为资源标识的经常是该资源的ID。我的更倾向于使用URL的缘由则是:在某些状况下,如对某个资源定时刷新以进行监控的时候,URL能够直接被使用。

  接下来是label域。其用来记录用于展现给用户的分类名。

  items_url域则用来表示取得属于该分类物品列表的URL。注意这里我使用了后缀_url以明确标明其是一个URL,须要经过跳转来取得实际的数据。

  下一个域brands则用来表示属于该分类的著名商品品牌。这里咱们使用了一个数组,而数组中的每一个元素都表示了一个品牌。每一个品牌的表示都包含了一个展现给用户的label,在搜索时所使用的键,以及该品牌所对应的url。您可能会怀疑为何咱们仅仅提供了这么少的域。这是由于他们仅仅是对这个品牌的引用,而并不是是把该资源的详细信息都包含进来了的缘故。在用户但愿查看该品牌的详细信息的时候,他须要向该品牌引用中所标明的品牌的URL发送一个GET请求。

  而因为hot_searches域的组成及使用基本上与brands域相似,所以这里再也不赘述。

  在大体地了解了食品分类的JSON表示中各个域的含义后,咱们就将开始讲解如何自行定义资源的JSON表示。对于一个简单的,不包含任何子资源以及对其它资源的引用的资源,咱们只须要经过一个包含简单属性的JSON来表示它。例如对于一个品牌,咱们可能仅仅提供了一系列描述性信息:品牌的名称,以及对品牌的简单描述。那么它所对应的JSON表示能够表示为:

1 {
2    "uri" : "/api/brands/32059",
3    "label" : "Dole",
4    "description" : "An American-based agricultural multinational corporation."
5 }

  而在另外一个资源中,可能包含了对其它资源的引用。在这种状况下,咱们就须要在表示对其它资源进行引用的域中经过URL来标明被引用资源的位置。例如一件Dole果汁中,可能就须要包含对品牌Dole的引用:

 1 {
 2    "uri" : "/api/items/1438299",
 3    "label" : "Dole Grape Juice",
 4    "price" : "$3.99",
 5    "brand" : {
 6       "label" : "Dole"
 7       "uri" : "/api/brands/32059"
 8    }
 9    ……
10 }

  在上面的Dole果汁的表示中,咱们能够看到它的brand域就是对品牌的引用。该引用中包含了该品牌的品牌名称以及一个指向该品牌的URL。

  在一个基于HTTP的REST系统中,咱们经常在资源的引用中包含必定量的描述信息。这主要由于两点:

  1. 提升性能。在一个对资源的引用中添加了用于显示的属性后,客户端页面能够避免再次经过url发送请求获得资源的具体描述,以获得用于显示的信息。
  2. 自描述性的要求。若是一个资源中包含了一个对其它资源进行引用的数组,那么用户就须要经过该标签来决定到底访问哪一个被引用的资源。

  固然,若是须要在展现Dole果汁的页面中须要Dole这个品牌的完整信息,咱们也能够将它直接嵌到Dole果汁的表示中:

 1 {
 2    "uri" : "/api/items/1438299",
 3    "label" : "Dole Grape Juice",
 4    "price" : "$3.99",
 5    "brand" : {
 6       "uri" : "/api/brands/32059",
 7       "label" : "Dole",
 8       "description" : "An American-based agricultural multinational corporation."
 9    }
10    ……
11 }

  固然,若是一个资源的表示太过复杂,并且有些属性其实是相互关联的,那么咱们也能够经过一个属性将它们归结在一块儿:

 1 {
 2    "uri" : "/api/items/1438299",
 3    "label" : "Dole Grape Juice",
 4    "price" : "$3.99",
 5    "brand" : {
 6       "uri" : "/api/brands/32059",
 7       "label" : "Dole",
 8       "description" : "An American-based agricultural multinational corporation."
 9    }
10    "nutrient component" : {
11       "sugar" : "14.5",
12       "protein" : "0.3",
13       "fat" : "0.1"
14    }
15    ……
16 }

  在上面的Dole果汁的表示中,咱们使用域nutrient component来表示全部的养分成分,而该域内部的各个子域则用来表示一系列相关的养分成分所占比例。

  另外,在不一样的状况下,咱们还可能对同一个资源提供不一样的表现形式。例如在一个资源极为复杂,其JSON表示甚至能够达到几百K的时候,咱们能够为该资源提供一个简化版本,以在非必要的状况下减小传输的数据量。

  例如在egoods中,咱们会将某些物美价廉的商品置于它的首页上,以吸引用户购买。在用户将鼠标移动到某个商品上并停留一段时间时,咱们会为用户展现一个Tooltip,并在该Tooltip中展现该商品的一部分信息。在这种状况下,向服务端请求该商品的全部信息以展现Tooltip便显得有些效率低下了。

  有时候,一个资源可能并不支持特定用户执行某个操做。例如一个管理员所建立的资源可能对普通用户只读。在这种状况下,咱们须要禁止普通用户对该资源的修改和删除。为了能明确地告知用户他所具备的权限,咱们须要一个能显式地标示用户能够在一个资源上所执行操做的组成。在REST响应中,这种组成被称为Hypermedia Controls。例如对于一个普通用户,其从egoods中所返回的分类列表将以下所示:

 1 HTTP/1.1 200 OK
 2 Content-Type: application/json
 3 Content-Length: xxx
 4  
 5 [
 6    {
 7       "label" : "Food",
 8       "uri" : "/api/categories/1",
 9       "actions" : ["GET"]
10    }, {
11       "label" : "Clothes",
12       "uri" : "/api/categories/2",
13       "actions" : ["GET"]
14    }
15    ...
16    {
17       "label" : "Electronics",
18       "uri" : "/api/categories/25",
19       "actions" : ["GET"]
20    }
21 ]

  能够看到,在上面的分类列表中,咱们经过actions域显式地标示了用户能够在各个类别上所能执行的操做。而对于管理员,其还能够执行修改,删除等操做:

 1 HTTP/1.1 200 OK
 2 Content-Type: application/json
 3 Content-Length: xxx
 4  
 5 [
 6    {
 7       "label" : "Food",
 8       "uri" : "/api/categories/1",
 9       "actions" : ["GET", "PUT", "DELETE"]
10    }, {
11       "label" : "Clothes",
12       "uri" : "/api/categories/2",
13       "actions" : ["GET", "PUT", "DELETE"]
14    }
15    ...
16    {
17       "label" : "Electronics",
18       "uri" : "/api/categories/25",
19       "actions" : ["GET", "PUT", "DELETE"]
20    }
21 ]

  而在一系列较为著名的REST系统中,如Sun Cloud API,其更是经过Hypermedia Controls定义了除CRUD以外的动词。如对于一个虚拟机,其在运行状态下能够执行中止命令,而在中止状态下能够执行启动命令:

 1 {
 2    "vms" : [
 3       {
 4          "id" : "1",
 5          ......
 6          "status" : "stopped",
 7          "links" : [
 8             {
 9                "rel" : "start",
10                "method" : "post",
11                "uri" : "vms/1?op=start"
12             }
13          ]
14       }, {
15          "id" : "2",
16          ......
17          "status" : "started",
18          "links" : [
19             {
20                "rel" : "stop",
21                "method" : "post",
22                "uri" : "vms/2?op=stop"
23             }
24          ]
25       }
26    ]
27 }

  可是一个常见的观点是:若是一个资源须要除CRUD以外的额外的动词,那么这种需求经常表示咱们对于某个资源的定义并非十分合理。所以在遇到这种状况时,软件开发人员首先须要考虑为资源添加额外的动词是否合适。

 

无状态约束

  在Roy Fielding的论文中,其为REST添加了一个无状态约束:

 

We next add a constraint to the client-server interaction: communication must be stateless in nature … such that each request from client to server must contain all of the information necessary to understand the request, and cannot take advantage of any stored context on the server. Session state is therefore kept entirely on the client.

 

  从上面的陈述中能够看到,在一个REST系统中,用户的状态会随着请求在客户端和服务端之间来回传递。这也即是REST这个缩写中ST(State Transfer)的来历。

  为REST系统添加这个约束有什么好处呢?主要仍是基于集群扩展性的考虑。若是REST服务中记录了用户相关的状态,那么在集群中,这些用户相关的状态就须要及时地在集群中的各个服务器之间同步。对用户状态的同步将会是一个很是棘手的问题:当一个用户的相关状态在一个服务器上发生了更改,那么在何时,什么状况下对这些状态进行同步?若是该状态同步是同步进行的,那么同时刷新多个服务器上的用户状态将致使对用户请求的处理变得异常缓慢。若是该同步是异步的,那么用户在发送下一个请求时,其它服务器将可能因为用户状态不一样步的缘由没法正确地处理用户的请求。除此以外,若是集群进行了不停机的横向扩展,那么用户状态的同步须要如何完成?这些实际上都是很是难以处理的问题。

  可是现有的不少较为流行的技术及规范实际上都没有限制用户的请求是无状态的。相信您知道,一个技术或规范实际上都拥有一个生态圈。在该生态圈以内的各技术之间能够较好地契合在一块儿。尤为是,有些技术实际上就会以该生态圈中的核心技术或规范所创建的假设之上来实现本身的功能。若是但愿禁止该假设,那么让某些技术工做起来就是很是困难的事情了。

  就以搭建基于HTTP的REST服务为例。在HTTP中,一个重要的功能就是Cookie和Session的使用(RFC6265)。该功能会在服务器里保留一个状态。所以在一个基于HTTP的REST系统中,咱们经常须要避免使用这些在服务器里面保留状态的技术。可是某些技术,如用户的登录,实际上经常须要在服务器中添加一个状态。

  因此在stackoverflow中,咱们经常会看到有人问:我如今使用了这样一种解决方案。这样实现是否是RESTful?此时一些人就会说,这不是RESTful。可是pure RESTful和almost RESTful之间的区别主要仍是在于一个是理论,一个是工程。在工程中,轻微地违反了一个准则并不必定表明这个解决方案一无可取。而是要看遵照该准则和轻微地违反了该准则以后工做量的大小以及后期的维护成本:之因此提出一系列准则,那是由于遵照该准则拥有必定的好处。若是对该准则的轻微违反能够减小大量的工做量,并且遵照准则的好处并无消失,或者是经过另外一样技术能够快速地从新得到该好处,那么对准则的轻微违反是值得的。

 

Authentication

  其实在上一节中,咱们已经提出了无状态约束给REST实现带来的麻烦:用户的状态是须要所有保存在客户端的。当用户须要执行某个操做的时候,其须要将全部的执行该请求所须要的信息添加到请求中。该请求将可能被REST服务集群中的任意服务器处理,而不须要担忧该服务器中是否存有用户相关的状态。

  可是在现有的各类基于HTTP的Web服务中,咱们经常使用会话来管理用户状态,至少是用户的登录状态。所以,REST系统的无状态约束实际上并非一个对传统用户登陆功能友好的约束:在传统登录过程当中,其自己就是经过用户所提供的用户名和密码等在服务端建立一个用户的登录状态,而REST的无状态约束为了横向扩展性却不想要这种状态。而这也就是为基于HTTP的REST服务添加身份验证功能的困难之处。

  为了解决该问题,最为经典也最符合REST规范的实现是在每次发送请求的时候都将用户的用户名和密码都发送给服务器。而服务器将根据请求中的用户名和密码调用登录服务,以从该服务中获得用户所对应的Identity和其所具备的权限。接下来,在REST服务中根据用户的权限来访问资源。

  这里有一个问题就是登录的性能。随着系统当前的加密算法愈来愈复杂,登录已经再也不是一个轻量级的操做。所以用户所发送的每次请求都要求一次登录对于整个系统而言就是一个巨大的瓶颈。

  在当前,解决该问题的方法主要是一个独立的缓存系统,如整个集群惟一的登录服务器。可是缓存系统自己所存储的仍然是用户的登录状态。所以该解决方案将仍然轻微地违反了REST的无状态约束。

  还有一个相似的方法是经过添加一个代理来完成的。该代理会完成用户的登录并得到该用户所拥有的权限。接下来,该代理会将与状态有关的信息从请求中删除,并添加用户的权限信息。在通过了这种处理以后,这些请求就能够转发到其后的各个服务器上了。转发目的地所在的服务器则会假设全部传入的请求都是合法的并直接对这些请求进行处理。

  能够看到,不管是一个独立的登录服务器仍是为整个集群添加一个代理,系统中都将有一个地方保留了用户的登录状态。这实际上和在集群中对会话集中进行管理并无什么不一样。也就是说,咱们所尝试的经过禁止使用会话来达成彻底的无状态并不现实。所以在一个基于HTTP的REST服务中,为登录功能使用集中管理的会话是合理的。

  既然咱们放松了对REST系统的无状态约束,那么一个REST系统所可使用的登录机制将主要分为如下两种:

  1.   基于HTTPS的Basic Access Authentication

其好处是其易于实现,并且主流的浏览器都提供了对该功能的支持。可是因为登录窗口都是由浏览器所提供的,所以其与产品外观有很大不一样。除此以外,浏览器都没有提供登出的功能,也没有提供找回密码等功能。

  2.   基于Cookie及Session的管理

在使用Cookie来管理用户的注册状态的时候,其实际上就是将服务端所返回的Cookie在每次发送请求的时候添加到请求中。虽说这个Cookie并不是存储了用户应用的状态,可是其实际存储了用户的登录状态。所以客户端的角度来说,由服务端管理的Session并不符合REST所倡导的无状态的要求。

  能够说,上面的两种方法各有优劣。可能第二种方法从客户端的角度看来并非RESTful的,可是其优点则在于不少类库都直接提供了对该功能的支持,从而简化了会话管理服务器的实现。

  在这里顺便提一句,若是项目足够大,将一些SSO产品集成到服务中也是不错的选择。

 

版本管理

  在前面已经提到过,一个REST系统为资源所抽象出的URI其实是对用户的一种承诺。但反过来讲,软件开发人员也很难预知一个资源的各方面特征如何在将来发生变化,从而提供一个永远不变的URI。

  在一个REST系统逐渐发展的过程当中,新的属性,新的资源将逐渐被添加到该系统中。在这些更改过程当中,资源的URI,访问资源的动词,响应中的Status Code将不能发生变化。此时软件开发人员所作的工做就是在现有系统上维护REST API的后向兼容性。

  当资源发生了过多的变化,原有的URI设计已经很难兼容现有资源应有的定义时,软件开发人员就须要考虑是否应该提供一个新版本的REST API。那么咱们该如何对资源的版本进行管理呢?

  首先要考虑的就是,新API的版本信息是否应当包含在资源的URI中。这在各著名论坛中仍然是一个争议较大的话题。一种观点认为在不一样版本的API中,一个资源拥有不一样的地址在必定程度上违反了HATEOAS:URI只是用来指定一个资源所在的位置,而不是该资源如何被抽象。若是一个资源由不一样的URI标示其不一样的表现形式,那么用户将没法经过一个响应中所标示的URI获得其它URI所指向的表示形式。并且在URI中添加了有关版本的信息也就标示着其可能会随着时间的推移发生变化。

  一种使用独立URI的方法是基于Accept头。在一个请求中,咱们经常标明了Accept头,以标示客户端但愿获得的表现形式。在该头中,用户能够添加所请求的资源的版本信息:

1 GET /api/categories/1
2 Host: www.egoods.com
3 Authorization: Basic xxxxxxxxxxxxxxxxxxx
4 Accept: application/vnd.ambergarden.egoods-v3+json

  而在接收到该请求以后,服务端将返回该资源的第三个版本:

1 HTTP/1.1 200 OK
2 Content-Type: application/vnd.ambergarden.egoods-v3+json
3 Content-Length: xxx
4  
5 {
6    "uri" : "/api/categories/1",
7    "label" : "Food",
8    ……
9 }

  能够看到,该方法是很是严格地遵照REST系统所提出的约束的。但其也并非没有缺点:添加一个自定义MIME类型(Custom MIME Type)也是一个很麻烦的流程,并且在不少现有技术中都没有很好地支持它,如HTML5中的Form。所以这种方案的缺点是对REST API用户并不那么友好。

  除此以外,另外一种基于重定向的解决方案也被提出。该方案容许一个REST系统提供多个版本的API,并在URI中标明版本号:

1 /api/v2/categories
2 /api/v1/categories

  这样用户能够选择使用特定版本的REST API来实现客户端功能。因为其使用固定版本的API,所以并不存在着一个资源有多种表示,进而违反了HATEOAS约束的问题。

  在REST系统的API随时间逐渐发展出众多版本的时候,系统对API的维护也将成为一个较大的问题。此时就须要逐渐退役一些年代久远的API 版本。对这些版本的退役主要分为两步:首先将其标为过时的,可是还在一段时间内支持。在这种状况下,对这些已通过期的API的访问将获得3XX响应,如301 Moved Permanently,以通知用户该URI所标示的资源须要使用新版本的URI进行访问。而再通过一段时间后,则将过时的REST API标记为废弃的。此时用户在访问这些URI时将返回4XX响应,如410 Gone。

  接下来,该REST系统还能够提供一个通用的REST API接口,并与最新版本的API保持一致:

1 /api/categories

  这样用户还能够选择一直使用最新版本的API,只是同时也须要一直对其进行维护,以保持与最新版本API的兼容性。在REST系统的API随着时间的推移逐渐发生变化的时候,该客户端也须要逐渐更新自身的功能。

  可是该方法有一个问题:由通用URI所辨识出的各个资源须要是稳定的,不能在必定时间以后被废弃,不然会给用户带来很是大的维护性的麻烦。举例来讲,假设客户端逻辑添加了一系列操做分类的功能。当REST系统决定再也不采用分类做为商品归类的标准,那么客户端逻辑中与分类相关的各个功能都须要进行大幅度地修改。过于频繁的这种改动很容易致使用户对该系统所提供的API失去维护的信心。所以在抽象资源时必定要努力地将各个资源的边界辨识清楚。虽说这听起来很吓人,可是在通过仔细考虑后这种状况仍是较为容易避免的。

  可是反过来讲,理论经常与实际有些脱钩,更况且REST是在2000年左右提出的,没法作到可以预见到十余年后所使用的各项技术。所以在尽可能符合REST所提出的各约束上提供一个最直观的,具备最高易用性的API才是王道。无限制地提供后向兼容性是一个很是困难,成本很是高的事情。所以在版本管理这一方面上来讲,咱们也须要尽可能兼顾项目需求和彻底听从理论这二者之间的平衡。

  而在同一个版本之中,咱们则须要保证API的后向兼容性。也就是说,在添加新的资源以及为资源添加新的属性的时候,原有的对资源进行操做的API也应该是工做的。

  对于一个基于HTTP的REST服务而言,软件开发人员须要遵照以下的守则以保持API的后向兼容性:

  1. 不能在请求中添加新的必须的参数。
  2. 不能更改操做资源的动词。
  3. 不能更改响应的HTTP status。

  而前向兼容性则显得没有那么重要了。REST服务的前向兼容性要求现有的服务兼容将来版本服务的客户端。可是因为服务提供商所提供的服务经常是最新版本,所以对前向兼容性有要求的状况不多出现。另一点是,为一个服务提供前向兼容性其实并不那么容易。由于这要求软件开发人员对产品的将来方向进行很是多的假设,并且这些假设不能有错误。反过来,这种对服务的前向兼容性的要求主要由客户端自身经过保持后向兼容性来完成。

 

性能

  接下来咱们就来简单地说说基于HTTP的REST服务中的性能问题。在基于HTTP的REST服务中,性能提高主要分为两个方面:REST架构自己在提升性能方面作出的努力,以及基于HTTP协议的优化。

  首先要讨论的就是对登录性能的优化。在前面咱们已经介绍过,在一个基于HTTP的REST服务中,每次都将用户的用户名和密码发送到服务端并由服务端验证这些信息是否合法是一个很是消耗资源的流程。所以咱们经常须要在登录服务中使用一个缓存,或者是使用第三方单点登录(SSO)类库。

  除此以外,软件开发人员还能够经过为同一个资源提供不一样的表现形式来减小在网络上传输的数据量,从而提升REST服务的性能。

  而在集群内部服务之间,咱们则能够再也不使用JSON,XML等这种用户能够读懂的负载格式,而是使用二进制格式。这样能够大大地减小内部网络所须要传输的数据量。这在内部网络交换数据频繁而且所传输的数据量巨大时较为有效。

  接下来就是REST系统的横向扩展。在REST的无状态约束的支持下,咱们能够很容易地向REST系统中添加一个新的服务器。

  除了这些和REST架构自己相关的性能提高以外,咱们还能够在如何更高效地使用HTTP协议上努力。一个最多见的方法就是使用条件请求(Conditional Request)。简单地说,咱们可使用以下的HTTP头来有条件地存取资源:

  1. ETag:一个对用户不透明的用来标示资源实例的哈希值
  2. Data-Modified:资源被更改的时间
  3. If-Modified-Since:根据资源的更改时间有条件地Get资源。这将容许客户端对未更改的资源使用本地缓存。
  4. If-None-Match:根据ETag的值有条件地Get资源。
  5. If-Unmodified-Since:根据资源的更改时间有条件地Put或Delete资源。
  6. If-Match:根据ETag的值有条件地Put或Delete资源。

  固然,这里所提到的一系列性能优化方案实际上仅仅是比较常见的,与基于HTTP的REST服务关联较大的方案。只是顾虑到过多地陈述和REST关联不大的话题一方面显得比较没有效率,另外一方面也是由于经过写另外一个系列博客能够将问题陈述得更加清楚,所以在这里咱们将再也不继续讨论性能相关的话题。

 

相关资源

AtomPub:http://atomenabled.org/。其是最为普遍讨论的并借鉴的RESTful服务。其由众多HTTP和REST专家所编写,甚至包括Roy Fielding本人也参与于其中

Roy Fielding的REST论文:http://www.ics.uci.edu/~fielding/pubs/dissertation/top.htm

Roy Fielding的我的网站:http://roy.gbiv.com/untangled/

RFC列表:http://www.ietf.org/rfc/

 

转载请注明原文地址并标明转载:http://www.cnblogs.com/loveis715/p/4669091.html

相关文章
相关标签/搜索