读书笔记,原文链接:http://www.cnblogs.com/loveis715/p/4669091.html,感谢作者!
版本管理
在前面已经提到过,一个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的后向兼容性:
- 不能在请求中添加新的必须的参数。
- 不能更改操作资源的动词。
- 不能更改响应的HTTP status。
而前向兼容性则显得没有那么重要了。REST服务的前向兼容性要求现有的服务兼容未来版本服务的客户端。但是由于服务提供商所提供的服务常常是最新版本,因此对前向兼容性有要求的情况很少出现。另外一点是,为一个服务提供前向兼容性其实并不那么容易。因为这要求软件开发人员对产品的未来方向进行非常多的假设,而且这些假设不能有错误。反过来,这种对服务的前向兼容性的要求主要由客户端自身通过保持后向兼容性来完成。
性能
接下来我们就来简单地说说基于HTTP的REST服务中的性能问题。在基于HTTP的REST服务中,性能提升主要分为两个方面:REST架构本身在提高性能方面做出的努力,以及基于HTTP协议的优化。
首先要讨论的就是对登陆性能的优化。在前面我们已经介绍过,在一个基于HTTP的REST服务中,每次都将用户的用户名和密码发送到服务端并由服务端验证这些信息是否合法是一个非常消耗资源的流程。因此我们常常需要在登陆服务中使用一个缓存,或者是使用第三方单点登陆(SSO)类库。
除此之外,软件开发人员还可以通过为同一个资源提供不同的表现形式来减少在网络上传输的数据量,从而提高REST服务的性能。
而在集群内部服务之间,我们则可以不再使用JSON,XML等这种用户可以读懂的负载格式,而是使用二进制格式。这样可以大大地减少内部网络所需要传输的数据量。这在内部网络交换数据频繁并且所传输的数据量巨大时较为有效。
接下来就是REST系统的横向扩展。在REST的无状态约束的支持下,我们可以很容易地向REST系统中添加一个新的服务器。
除了这些和REST架构本身相关的性能提升之外,我们还可以在如何更高效地使用HTTP协议上努力。一个最常见的方法就是使用条件请求(Conditional Request)。简单地说,我们可以使用如下的HTTP头来有条件地存取资源:
- ETag:一个对用户不透明的用来标示资源实例的哈希值
- Data-Modified:资源被更改的时间
- If-Modified-Since:根据资源的更改时间有条件地Get资源。这将允许客户端对未更改的资源使用本地缓存。
- If-None-Match:根据ETag的值有条件地Get资源。
- If-Unmodified-Since:根据资源的更改时间有条件地Put或Delete资源。
- 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/