RESTful API 设计最佳实践(5)
在上篇文章中,主要介绍了Roy Fielding论文中,关于统一接口的四个约束,并对其中资源定义相关部分阐述了自己的一些观点。本章节主要介绍一下我们现实中最为常见的,也是REST统一接口的最佳实践者——HTTP规范,着重介绍设计RESTful API时,对于HTTP方法选择,以及REST服务实现过程中需要注意的点。目前,基本上所有的REST服务都是基于HTTP协议来实现统一接口的。
一、HTTP
对于HTTP协议的具体细节,这里不多讲。回顾REST中统一接口的四个约束,可以发现,http协议均满足。总的说来,HTTP提供了下面几方面的内容:
- 对于资源的有限操作集合: GET/POST/PUT/DELETE/HEAD/PATCH/OPTIONS等。
- HTTP头信息(可自定义):
- HTTP响应状态代码(可自定义):
参考wiki中的详细介绍:http状态码详细介绍 - 一套标准的内容协商机制:
如,客户端发送的消息头中,可以告诉服务器自己接受内容格式、语言以及编码等。
Accept Content-type
Accept-Language Content-Language
Accept-Encoding Content-Encoding
- 一套标准的缓存机制,如:
If-Modified-Since Thu, 26 Nov 2009 13:50:19 GMT
If-None-Match "8fb8b-14-4794674acdcc0"
- 一套标准的客户端身份认证机制,如:
HTTP/1.1 401 Unauthorized
WWW-Authenticate: ...
二、幂等性和安全性
1. 幂等性
在Wikipedia中,关于幂等性的定义如下:
In computer science, the term idempotent is used more comprehensively to describe an operation that will produce the same results if executed once or multiple times. This may have a different meaning depending on the context in which it is applied. In the case of methods or subroutine calls with side effects, for instance, it means that the modified state remains the same after the first call.
简单说来,就是多次执行的结果都一样,称之为幂等。在REST中,则指多次请求与一次请求产生的副作用是的一样。需要注意,我们没有强调说是每次请求返回的结果一样,因为具有幂等性的请求操作,只要其产生的作用是一样的,即使每次请求返回的结果不一样,那也是幂等的。
2. 安全性
wikipedia中,对于安全性的描述如下:
Some methods (for example, HEAD, GET, OPTIONS and TRACE) are defined as safe, which means they are intended only for information retrieval and should not change the state of the server. In other words, they should not have side effects, beyond relatively harmless effects such as logging, caching, the serving of banner advertisements or incrementing a web counter. Making arbitrary GET requests without regard to the context of the application’s state should therefore be considered safe.
简单说来,就是调用一个函数而不会产生副作用,那么这个函数就是安全的(如:纯函数)。因此,在REST中,如果我们协定了GET,HEAD等函数为安全的操作,那么在REST服务的设计和实现中,就需要确保这些操作的安全性。否则,不仅使用时会产生混乱,而且还可能影响诸如web缓存等功能。比如,使用者可能会缓存某GET操作的结果,但其实后台服务中,GET操作已经改变了服务器资源的状态。
从定义上看,安全的操作肯定是幂等操作。通常,在REST中,安全的操作应该都是只读操作,但是安全操作并不代表每次返回的数据都是相同的。
三、HTTP方法
在基于HTTP协议实现的RESTful API中,URI是用来标定资源的,而操作语义的表达,则是通过HTTP提供的那几个操作方法来表达的,这些方法是实现统一接口的主要部分。这里,只详细介绍几个我们常用的方法:GET、POST、PUT、DELETE(CRUD)。
1. GET
在HTTP协议中,GET被约定为:检索(或读取)一个资源的表述,不会对资源做任何修改(GET操作应该是安全的)。因此,最理想的情况下,在实现REST服务时,对于GET操作,不应该修改任何服务器中的资源。但是,就像“安全性”中所说,对于那些无害的“副作用”(比如记录日志,更新结果缓存,增加访问量计数等),在GET操作中也是可以做的。
2. PUT
PUT通常被用于更新已有资源的相关信息,也能用于创建新资源的场景,比如说客户端已知资源ID的情况。但是,这在使用过程中,可能会困惑使用者。在REST中,是否真的需要用PUT来创建资源?这个留到后面讨论。
PUT不是一个安全的操作,但却是一个幂等操作。也就是说,它会产生副作用,但是每次相同的请求,产生的副作用结果相同。
如果更新成功,返回200,body中附带返回信息;若body中没有返回信息,则返回204。如果创建新资源成功,则返回201,body中可携带返回信息,也可不携带(可考虑带宽要求)。
3. POST
POST被实现为创建一个新的资源,严格来说,是创建一个新的子级资源。当使用POST创建资源时,服务器端应该将资源放在其父级“目录”下,并将新资源与父级资源建立联系,标记新资源的ID。例如:
POST http://www.example.com/users
其语义应该理解为:在指定的资源集合users中,创建一个子资源user。这也就很好地解决了一个问题:
创建一个客户,不应该是“POST /user”吗?为什么是“POST /users”?
就是因为对POST的语义理解不清楚所造成的。
POST操作是不安全的,也是非幂等的。如果多次调用同一个POST请求,那么会在父级资源集合中,新建多个内容相同的子资源(除了资源ID不一样)。这也是POST与PUT在创建新资源时的本质区别,多次调用PUT请求,只会创建一个新资源。
如果创建成功,则返回201,在返回消息的HEADER中,location的值为指向新建资源的URI。
4. DELETE
DELETE用于删除一个指定的资源或资源集合。DELETE操作是幂等的,因为不管调用多少次,它的影响结果就是:删除掉一个资源。关于这一点,可能存在一些争论:
当成功删除一个资源之后,会返回200(或204),再一次调用相同的DELETE操作,会返回404错误(资源存在),这是不是说明DELETE是非幂等的?
关于这个,我的观点是:幂等性并不是说返回给客户端的消息是一样的,而是相同的多次操作,对于服务器端的资源作用结果是一样的,多次调用DELETE请求,对服务器段结果是一样的:资源被删除了。因此,DELETE操作应该是幂等的
另外,服务器端在处理DELETE操作时,可能是真正地从物理上删除资源,另外也可能是打上已被删除的标志。但不管是何种处理,被DELETE的资源都应该是不可访问的,都应该返回404错误。
如果删除成功,则返回200,body中带上回应信息;如body中没有信息,则返回204。
四、新建资源——PUT vs POST
在设计RESTful API时,经常要确定该用哪个HTTP方法来表达语义。这个应该更加不同的场景来决定,前提是你真正理解了HTTP规范中,各个方法的语义。在大多数场景之下,对于CRUD对应的方法,应该很容易就确定了,对于一些“这个也可以,那个也差不多”的场景,就需要具体情况具体分析了。
新建资源时,是用PUT方法还是POST方法?对于这一点,个人觉得很简单:明确只用POST新建资源,PUT只用于更新资源。理由也很简单:明确边界,杜绝混乱,一旦你将PUT也用于资源的创建,那么选择困难症会一直缠绕着你。若要说具体的理由,那也有:
假设这样一个场景:前端页面展示用户信息,并允许用户修改,用户在前端修改的过程中,后台管理员将这个用户删除了。此时,客户端发送一个PUT请求——请求修改该用户信息,那我们是应该新建该用户的信息呢,还是该返回404,通知客户端该用户已不存在?
答案显然是返回404错误咯。非常关键的一点是,如果PUT可扮演新建资源和更新资源两种角色,在REST的client-server模式下,服务器端是很难界定到底是要创建资源,还是更新资源的,单纯地“不存在就创建”对于很多场景可能都不适用,我们为何要承担这种痛苦呢?
如果客户端已知资源ID,想创建一个该资源ID指定的资源时,用PUT表达如下:
PUT /users/{id}
body内容: {:username "snailiu"}
但也可以统一为用POST表达:
POST /users
body内容:{:username "snailiu", :id 123}
这样服务器端的职责就更加明确了。
还有另一个理由:既然URI是用来标定资源的,当服务器端资源不存在的情况下,即使客户端知道资源的ID,但也是无法知道它在服务端的具体位置的。如果支持“不存在就新建”这种设计,那么也就是说客户端可以指定在服务器的任意位置新建资源了,这是很不合理的。从这一点来看,PUT支持新建资源,真是一件可怕的事情。
基于上面两点,我的建议是:忘掉PUT用于新建资源的功能吧。
五、后续
本章节主要介绍了基于HTTP协议的RESTful统一接口,着重介绍了我们常用的HTTP方法,并对其方法选择提出了一些建议。至此,REST架构理论开始照进现实。在下面的章节中,会介绍在设计具体的RESTful API时的一些具体建议。