为什么要以 RESTful 设计为目标?
RESTful 原则为 Web 服务 API 设计带来了使网站变得容易(让随机的人类用户“冲浪”它们)的特性,因此它们对程序员来说很容易使用。REST 不好,因为它是 REST,它很好,因为它很好。它很好,主要是因为它很简单。
普通 HTTP 的简单性(没有 SOAP 信封和单 URI 重载POST
服务),有些人可能称之为“缺乏功能”,实际上是它最大的优势。马上,HTTP 要求您具有可寻址性和无状态性:这两个基本设计决策使 HTTP 可扩展到当今的大型站点(和大型服务)。
但 REST 并不是万能的:有时 RPC 样式(“远程过程调用” - 例如 SOAP)可能是合适的,有时其他需求优先于 Web 的优点。这可以。我们真正不喜欢的是不必要的复杂性。程序员或公司经常引入 RPC 样式的服务来完成普通旧 HTTP 可以处理的工作。其效果是 HTTP 被简化为用于解释“真正”发生的事情的巨大 XML 有效负载的传输协议(不是 URI 或 HTTP 方法提供有关它的线索)。生成的服务过于复杂,无法调试,并且除非您的客户具有开发人员预期的确切设置,否则将无法正常工作。
同样,Java/C# 代码不能是面向对象的,仅使用 HTTP 不会使设计成为 RESTful 。人们可能会急于根据操作和应该调用的远程方法来考虑他们的服务。难怪这将主要以 RPC 风格的服务(或 REST-RPC 混合)结束。第一步是换个角度思考。RESTful 设计可以通过多种方式实现,一种方式是从资源而非操作的角度来考虑您的应用程序:
而不是考虑它可以执行的操作(“在地图上搜索地点”)......
...尝试根据这些操作的结果进行思考(“地图上与搜索条件匹配的地点列表”)。
我将在下面举例。(REST 的另一个关键方面是 HATEOAS 的使用——这里我不刷它,但我会在另一篇文章中快速谈论它。)
第一个设计的问题
让我们看一下建议的设计:
ACTION http://api.animals.com/v1/dogs/1/
首先,我们不应该考虑创建一个新的 HTTP 动词( ACTION
)。一般来说,这是不可取的,原因如下:
- (1)仅给定服务 URI,“随机”程序员如何知道
ACTION
动词存在?
- (2)如果程序员知道它存在,他怎么知道它的语义?那个动词是什么意思?
- (3)人们应该期望那个动词具有哪些属性(安全性、幂等性)?
- (4)如果程序员有一个只处理标准 HTTP 动词的非常简单的客户端怎么办?
- (5) ...
现在让我们考虑使用POST
(我将在下面讨论为什么,现在就相信我的话):
POST /v1/dogs/1/ HTTP/1.1
Host: api.animals.com
{"action":"bark"}
这可能没问题......但前提是:
{"action":"bark"}
是一份文件;和
/v1/dogs/1/
是一个“文档处理器”(类似工厂的)URI。“文档处理器”是一个 URI,您只需“扔东西”并“忘记”它们 - 处理器可能会在“扔东西”之后将您重定向到新创建的资源。例如,用于在消息代理服务上发布消息的 URI,在发布后会将您重定向到显示消息处理状态的 URI。
我对您的系统了解不多,但我已经打赌两者都不是真的:
{"action":"bark"}
不是一个文件,它实际上是你试图忍者潜入服务的方法;和
/v1/dogs/1/
URI 代表“狗”资源(可能是带有 的狗)id==1
而不是文档处理器。
所以我们现在所知道的是,上面的设计并不是那么 RESTful,但那到底是什么?它有什么不好?基本上,这很糟糕,因为这是具有复杂含义的复杂 URI。你无法从中推断出任何东西。程序员怎么会知道狗有一个bark
动作可以秘密地注入POST
它呢?
设计问题的 API 调用
因此,让我们切入正题,尝试从资源方面考虑以RESTful 的方式设计这些树皮。请允许我引用Restful Web Services书:
请求是尝试从POST
现有资源创建新资源。在数据结构的意义上,现有资源可能是新资源的父级,就像树的根是其所有叶节点的父级一样。或者现有资源可能是一个特殊的“工厂”
资源,其唯一目的是生成其他资源。与POST
请求一起发送的表示描述了新资源的初始状态。与 PUT 一样,POST
请求根本不需要包含表示。
根据上面的描述,我们可以看到bark
可以建模为a 的子资源dog
(因为 abark
包含在狗中,也就是说,吠叫被狗“吠”)。
从这个推理我们已经得到:
- 方法是
POST
- 资源是
/barks
dog:的子资源/v1/dogs/1/barks
,代表一个bark
“工厂”。该 URI 对于每只狗都是唯一的(因为它在 下/v1/dogs/{id}
)。
现在,您列表中的每个案例都有特定的行为。
##1。bark 只是发送了一封电子邮件,dog.email
没有记录任何内容。
首先,吠叫(发送电子邮件)是同步任务还是异步任务?其次,bark
请求是否需要任何文件(可能是电子邮件)还是空的?
1.1 bark 发邮件到dog.email
什么都不记录(作为同步任务)
这个案例很简单。对工厂资源的调用会立即barks
产生一个吠声(发送一封电子邮件),并且会立即给出响应(如果确定与否):
POST /v1/dogs/1/barks HTTP/1.1
Host: api.animals.com
Authorization: Basic mAUhhuE08u724bh249a2xaP=
(entity-body is empty - or, if you require a **document**, place it here)
200 OK
因为它没有记录(更改)任何内容,200 OK
就足够了。它表明一切都按预期进行。
1.2 bark 发邮件到dog.email
什么都不记录(作为异步任务)
在这种情况下,客户端必须有一种方法来跟踪bark
任务。然后,该bark
任务应该是具有自己 URI 的资源。:
POST /v1/dogs/1/barks HTTP/1.1
Host: api.animals.com
Authorization: Basic mAUhhuE08u724bh249a2xaP=
{document body, if needed;
NOTE: when possible, the response SHOULD contain a short hypertext note with a hyperlink
to the newly created resource (bark) URI, the same returned in the Location header
(also notice that, for the 202 status code, the Location header meaning is not
standardized, thus the importance of a hipertext/hyperlink response)}
202 Accepted
Location: http://api.animals.com/v1/dogs/1/barks/a65h44
这样,每一个bark
都是可追溯的。然后,客户端可以向 URI 发出 aGET
以bark
了解其当前状态。甚至可以使用 aDELETE
来取消它。
2. bark 发送电子邮件至dog.email
,然后递增dog.barkCount
1
如果您想让客户端知道dog
资源已更改,这可能会更棘手:
POST /v1/dogs/1/barks HTTP/1.1
Host: api.animals.com
Authorization: Basic mAUhhuE08u724bh249a2xaP=
{document body, if needed; when possible, containing a hipertext/hyperlink with the address
in the Location header -- says the standard}
303 See Other
Location: http://api.animals.com/v1/dogs/1
在这种情况下,location
标头的目的是让客户知道他应该查看dog
. 来自HTTP RFC 关于303
:
此方法的存在主要是为了允许
POST
-activate 脚本的输出将用户代理重定向到选定的资源。
如果任务是异步的,bark
就像1.2
情况一样需要一个子资源,并且303
应该在GET .../barks/Y
任务完成时返回。
3. bark在 bark 发生时创建一个bark
带有记录的新“ ”记录。bark.timestamp
它也以 1 递增dog.barkCount
。
POST /v1/dogs/1/barks HTTP/1.1
Host: api.animals.com
Authorization: Basic mAUhhuE08u724bh249a2xaP=
(document body, if needed)
201 Created
Location: http://api.animals.com/v1/dogs/1/barks/a65h44
在这里,bark
由于请求而创建,因此201 Created
应用状态。
如果创建是异步的,202 Accepted
则需要 a (如 HTTP RFC 所述)。
保存的时间戳是bark
资源的一部分,可以用GET
它来检索。更新后的狗也可以在其中“记录” GET dogs/X/barks/Y
。
4. bark 运行系统命令从 Github 拉取最新版本的狗代码。然后它会发送一条短信dog.owner
告诉他们新的狗代码正在生产中。
这个的措辞很复杂,但它几乎是一个简单的异步任务:
POST /v1/dogs/1/barks HTTP/1.1
Host: api.animals.com
Authorization: Basic mAUhhuE08u724bh249a2xaP=
(document body, if needed)
202 Accepted
Location: http://api.animals.com/v1/dogs/1/barks/a65h44
然后客户端会发出GET
s 来/v1/dogs/1/barks/a65h44
了解当前状态(如果代码被提取,则电子邮件已发送给所有者等)。每当狗改变时,a303
是适用的。
包起来
引用罗伊菲尔丁的话:
REST 对方法的唯一要求是它们必须为所有资源统一定义(即,中介不必知道资源类型即可理解请求的含义)。
在上面的例子中,POST
是统一设计的。它会使狗“ bark
”。这是不安全的(意味着 bark 对资源有影响),也不是幂等的(每个请求产生一个 new bark
),这很适合POST
动词。
程序员会知道: aPOST
产生barks
a bark
。响应状态代码(必要时还带有实体主体和标题)可以解释发生了什么变化以及客户端可以和应该如何继续。
注意:使用的主要来源是:“ Restful Web Services ”一书、HTTP RFC和Roy Fielding 的博客。
编辑:
自首次创建以来,问题和答案已经发生了很大变化。最初的问题询问了 URI 的设计,例如:
ACTION http://api.animals.com/v1/dogs/1/?action=bark
以下是为什么它不是一个好选择的解释:
客户端如何告诉服务器如何处理数据是方法信息。
- RESTful Web 服务在 HTTP 方法中传递方法信息。
- 典型的 RPC 样式和 SOAP 服务将它们保存在实体主体和 HTTP 标头中。
[客户端希望服务器]操作的数据的哪一部分是范围信息。
- RESTful 服务使用 URI。SOAP/RPC 样式服务再次使用实体主体和 HTTP 标头。
以 Google 的 URI 为例http://www.google.com/search?q=DOG
。在那里,方法信息是GET
,作用域信息是/search?q=DOG
。
长话短说:
- 在RESTful 架构中,方法信息进入 HTTP 方法。
- 在面向资源的架构中,范围信息进入 URI。
和经验法则:
如果 HTTP 方法与方法信息不匹配,则服务不是 RESTful。如果范围信息不在 URI 中,则服务不是面向资源的。
您可以将“树皮” “动作”放在 URL(或实体正文中)并使用POST
. 没问题,它可以工作,并且可能是最简单的方法,但这不是 RESTful。
为了使您的服务真正保持 RESTful,您可能必须退后一步,考虑一下您在这里真正想要做什么(它会对资源产生什么影响)。
我无法谈论您的具体业务需求,但让我举个例子:考虑一个 RESTful 订购服务,其中订单位于 URI 之类的example.com/order/123
.
现在说我们要取消订单,我们该怎么做呢?有人可能会认为这是一个“取消” “动作”并将其设计为POST example.com/order/123?do=cancel
.
正如我们上面所说,这不是 RESTful。相反,我们可能会使用发送到的元素PUT
的新表示形式:order
canceled
true
PUT /order/123 HTTP/1.1
Content-Type: application/xml
<order id="123">
<customer id="89987">...</customer>
<canceled>true</canceled>
...
</order>
就是这样。如果订单无法取消,可以返回特定的状态码。(为简单起见,也可以使用POST /order/123/canceled
与实体主体类似的子资源设计。)true
在您的特定情况下,您可以尝试类似的方法。这样,例如,当狗在吠叫时,GET
at/v1/dogs/1/
可以包含该信息(例如<barking>true</barking>
)。或者......如果这太复杂了,放宽你的 RESTful 要求并坚持使用POST
.
更新:
我不想让答案太大,但是需要一段时间才能掌握将算法(一个动作)公开为一组资源的窍门。与其从行动的角度思考(“在地图上搜索地点”),不如从行动的结果(“地图上与搜索条件匹配的地点列表”)的角度思考。
如果您发现您的设计不适合 HTTP 的统一接口,您可能会发现自己回到了这一步。
查询变量是 范围信息,但不表示新资源(/post?lang=en
显然是与 相同的资源/post?lang=jp
,只是表示不同)。相反,它们用于传达客户端状态(如?page=10
,因此状态不会保存在服务器中;?lang=en
这里也是一个示例)或算法资源的输入参数(, )。同样,不是不同的资源。/search?q=dogs
/dogs?code=1
HTTP 动词的(方法)属性:
URI中显示?action=something
的另一个明确点不是 RESTful,是 HTTP 动词的属性:
GET
并且HEAD
是安全的(并且是幂等的);
PUT
并且DELETE
仅是幂等的;
POST
也不是。
安全性:GET
或HEAD
请求是读取某些数据的请求,而不是更改任何服务器状态的请求。客户端可以发出GET
或HEAD
请求 10 次,这与发出一次或根本不发出是一样的。
幂等性:一种幂等操作,无论您应用一次还是多次应用它都具有相同的效果(在数学中,乘以零是幂等的)。如果你DELETE
一个资源一次,再次删除将具有相同的效果(资源GONE
已经)。
POST
既不安全也不幂等。向“工厂”资源发出两个相同POST
的请求可能会导致两个从属资源包含相同的信息。使用重载(URI 或实体主体中的方法)POST
,所有的赌注都没有了。
这两个属性对于 HTTP 协议的成功都很重要(通过不可靠的网络!):您有多少次更新(GET
)页面而不等到它完全加载?
创建一个动作并将其放在 URL 中显然违反了 HTTP 方法的约定。再一次,技术允许你,你可以做到,但这不是 RESTful 设计。