REST简介
一说到REST我想大家的第一反应就是“啊就是那种前后台通信方式。”但是在要求详细讲述它所提出的各个约束以及如何开始搭建REST服务时却很少有人能够清晰地说出它到底是什么需要遵守什么样的准则。在您将看到的这一篇文章中我们将对REST尤其是基于HTTP的REST服务进行详细地介绍。通过这些文章您不仅可以了解到什么是REST更能清晰地了解到您在编写REST服务时所需要遵守的各个守则设计RESTful API时需要考虑的各种因素以及实现过程中可能遇到的问题等内容。REST示例我想很多读者可能并不太清楚REST到底是一个什么概念。那么首先让我们来看一个简单的基于HTTP的REST服务示例。假设用户正在访问一个电子商务网站www.egoods.com。该网站对其所销售的各个物品进行了详细分类。当用户登录该网站进行购物时他首先需要在该网站上选择其所需要寻找物品的分类进而列出属于该分类的各个物品。当然虽然从业务逻辑的角度来说这个流程非常简单但实际上浏览器向后台发送了多个请求页面逻辑在页面加载时将首先得到所有的商品分类并将这些分类显示在了页面中。在用户选择了一个分类的时候页面逻辑将发送一个请求得到该分类的详细信息并发送另外一个请求来得到该分类的商品列表在通过浏览器的调试功能查看这些请求的时候我们可以看到其首先向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?category1, 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则用来列出在“食品”分类中的著名品牌例如友臣乐事等。这些品牌被组织为一个对象数组而数组中的每个对象都拥有labelurl等属性。在这些属性的帮助下页面可以列出这些著名品牌的名称并允许用户通过点击跳转到这些品牌所对应的页面上。除了这些属性之外Food分类还包含了其它一系列属性如表示当前其它用户正在搜索的hot_searches属性等这里就不再赘述。该响应有一个问题那就是符合用户筛选条件的各个产品并没有包含在该响应中。这是因为页面所列出的各个产品是根据用户所设置的筛选条件即其选择的品牌以及搜索关键字而变化的。因此页面逻辑会根据属性items_url以及用户所设定的搜索条件组合成为目标URL再次发送请求到后台以请求需要在页面中展现的各个物品。例如用户在只想浏览属于乐事品牌的食品时其可以钩选乐事这个品牌那么此时的URL将由食物分类的items_url以及表示按照品牌进行筛选的URL参数共同组成1 GET /api/items?category1brand_key56632 2 Host: www.egoods.com 3 Authorization: Basic xxxxxxxxxxxxxxxxxxx 4 Accept: application/json现在让我们来总结一下上面所展示的基于HTTP的REST系统的整个运行流程。在开始的时候我们拿到了所有分类的列表。列表中的各个条目不仅仅包含了用户可以看到的分类名称等信息更拥有一个额外的URL属性。在用户选择该列表中的一项时页面逻辑将会向对应的URL发送一个请求以获得该项目的详细信息。在这个详细信息中一些内容又包含了一些其它的URL从而使得页面逻辑又能通过该URL属性发送请求。您也许会说哎这不和我们现有系统的运行流程一样的嘛。是的。在上面所举出的例子中我们也更偏重地描述了REST系统所需要具有的HATEOASHypermedia 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。其目标是为了创建具有良好扩展性的分布式系统。反过来作为一种架构其提出了一系列架构级约束。这些约束有使用客户/服务器模型。客户和服务器之间通过一个统一的接口来互相通讯。层次化的系统。在一个REST系统中客户端并不会固定地与一个服务器打交道。无状态。在一个REST系统中服务端并不会保存有关客户的任何状态。也就是说客户端自身负责用户状态的维持并在每次发送请求时都需要提供足够的信息。可缓存。REST系统需要能够恰当地缓存请求以尽量减少服务端和客户端之间的信息传输以提高性能。统一的接口。一个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系统中的资源和状态的定义后我们来看看统一接口这个约束。该约束又包含了四个子约束每个资源都拥有一个资源标识。每个资源的资源标识可以用来唯一地标明该资源。消息的自描述性。在REST系统中所传递的消息需要能够提供自身如何被处理的足够信息。例如该消息所使用的MIME类型是否可以被缓存等。资源的自描述性。一个REST系统所返回的资源需要能够描述自身并提供足够的用于操作该资源的信息如如何对资源进行添加删除以及修改等操作。也就是说一个典型的REST服务不需要额外的文档对如何操作资源进行说明。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?category1, 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和IDURL用来指向资源所在的地址而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主要由以下几个部分组成协议。即HTTP以及HTTPS。主机名和端口。如www.egoods.com:8421资源的相对路径。如/api/categories。请求参数。即由问号开始的由键值对组成的字符串?page1page_size20在为一个资源设计其所对应的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 }