JSON 合并修补更简单一些。 修补文档的结构与原始 JSON 资源相同,但只包含要更改或添加的字段的子集。 此外,可以通过在修补文档中为字段值指定 null
,来删除该字段。 (这意味着,如果原始资源包含显式 null 值,则不适合使用合并修补。)
例如,假设原始资源采用以下 JSON 表示形式:
"name":"gizmo",
"category":"widgets",
"color":"blue",
"price":10
下面是此资源的可能 JSON 合并修补代码:
"price":12,
"color":null,
"size":"small"
此代码告知服务器要更新 price
、删除 color
以及添加 size
,而不修改 name
与 category
。 有关 JSON 合并修补的具体详细信息,请参阅 RFC 7396。 JSON 合并修补的媒体类型是 application/merge-patch+json
。
由于修补文档中的 null
具有特殊的含义,如果原始资源包含显式 null 值,则不适合使用合并修补。 此外,修补文档不会指定服务器应用更新的顺序。 此限制是否造成影响具体取决于数据和域。 RFC 6902 中定义的 JSON 修补更灵活。 它以操作序列的形式指定要应用的更改。 操作包括添加、删除、替换、复制和测试(以验证值)。 JSON 修补的媒体类型是 application/json-patch+json
。
下面是在处理 PATCH 请求时可能遇到的典型错误状态,以及相应的 HTTP 状态代码。
HTTP 状态代码
DELETE 方法
如果删除操作成功,Web 服务器应以 HTTP 状态代码 204(无内容)做出响应,指示已成功处理该过程,但响应正文不包含其他信息。 如果资源不存在,Web 服务器可以返回 HTTP 404(未找到)。
有时,POST、PUT、PATCH 或 DELETE 操作可能需要一段处理时间才能完成。 如果需要等待该操作完成后才能向客户端发送响应,可能会造成不可接受的延迟。 在这种情况下,请考虑将该操作设置为异步操作。 返回 HTTP 状态代码 202(已接受),指示该请求已接受进行处理,但尚未完成。
应公开一个可返回异步请求状态的终结点,使客户端能够通过轮询状态终结点来监视状态。 在 202 响应的 Location 标头中包含状态终结点的 URI。 例如:
HTTP/1.1 202 Accepted
Location: /api/status/12345
如果客户端向此终结点发送 GET 请求,响应中应包含该请求的当前状态。 (可选)响应中还可以包含预计完成时间,或者用于取消操作的链接。
HTTP/1.1 200 OK
Content-Type: application/json
"status":"In progress",
"link": { "rel":"cancel", "method":"delete", "href":"/api/status/12345" }
如果异步操作创建了新资源,则该操作完成后,状态终结点应返回状态代码 303(查看其他)。 在 303 响应中,包含一个 Location 标头用于提供新资源的 URI:
HTTP/1.1 303 See Other
Location: /api/orders/12345
有关如何实现此方法的详细信息,请参阅为长时间运行的请求提供异步支持和异步请求-答复模式。
消息正文中的空集
任何时候成功响应的正文为空,状态码都应为 204(无内容)。 对于空集,例如对没有项目的筛选请求的响应,状态代码仍应为 204(无内容),而不是 200(正常)。
数据筛选和分页
通过单个 URI 公开资源的集合可能会导致应用程序在只需一部分信息时提取大量数据。 例如,假设某个客户端应用程序需要查找成本超过特定值的所有订单。 它可以从 /orders URI 检索所有订单,然后在客户端筛选这些订单。 显然,此过程的效率非常低下。 它浪费了托管 Web API 的服务器的网络带宽和处理能力。
API 可以允许在 URI 的查询字符串中传递筛选器,例如 /orders?minCost=n。 然后,Web API 负责分析和处理查询字符串中的 minCost
参数并在服务器端返回筛选后的结果。
对集合资源执行的 GET 请求可能返回大量的项。 应将 Web API 设计为限制任何单个请求返回的数据量。 请考虑支持查询字符串指定要检索的最大项数和集合中的起始偏移量。 例如:
/orders?limit=25&offset=50
此外,请考虑对返回的项数指定上限,以防拒绝服务攻击。 若要帮助客户端应用程序,返回分页数据的 GET 请求还应包含某种形式的元数据,以指示集合中可用的资源总数。
可以通过提供一个将字段名称用作值的 soft 参数(例如 /orders?sort=ProductID),使用类似的策略对提取的数据排序。 但是,此方法会对缓存产生负面影响,因为查询字符串参数构成许多缓存实现用作缓存数据的键的资源标识符的一部分。
如果每个项包含大量数据,可以扩展此方法来限制针对每个项返回的字段。 例如,可以使用接受以逗号分隔的字段列表的查询字符串参数,例如 /orders?fields=ProductID,Quantity。
为查询字符串中的所有可选参数提供有意义的默认值。 例如,如果实现分页,将 limit
参数设为 10,将 offset
参数设为 0;如果实现排序,将排序参数设为资源的键;如果支持投影,将 fields
参数设为资源中的所有字段。
支持大型二进制资源的部分响应
资源可能包含大型二进制字段,例如文件或图像。 若要解决不可靠和间歇性连接导致的问题并缩短响应时间,请考虑分块检索此类资源。 为此,对于针对大型资源发出的 GET 请求,Web API 应支持 Accept-Ranges 标头。 此标头指示 GET 操作支持“部分”请求。 客户端应用程序可以提交返回指定为字节范围的资源子集的 GET 请求。
此外,请考虑对这些资源实现 HTTP HEAD 请求。 HEAD 请求与 GET 请求类似,不过,前者只返回描述资源的 HTTP 标头和空消息正文。 客户端应用程序可以发出 HEAD 请求以确定是否要通过使用部分 GET 请求获取某个资源。 例如:
HEAD https://adventure-works.com/products/10?fields=productImage HTTP/1.1
下面是响应消息的示例:
HTTP/1.1 200 OK
Accept-Ranges: bytes
Content-Type: image/jpeg
Content-Length: 4580
Content-Length 标头指定资源的总大小,Accept-Ranges 标头指示相应的 GET 操作支持部分结果。 客户端应用程序可以使用此信息以较小的区块检索图像。 第一个请求通过使用 Range 标头提取前 2500 个字节:
GET https://adventure-works.com/products/10?fields=productImage HTTP/1.1
Range: bytes=0-2499
响应消息通过返回 HTTP 状态代码 206 指示这是部分响应。 Content-Length 标头指定消息正文中返回的实际字节数(不是资源的大小),Content-Range 标头指示这是该资源的哪一部分(第 0 到 2499 字节,总共 4580 个字节):
HTTP/1.1 206 Partial Content
Accept-Ranges: bytes
Content-Type: image/jpeg
Content-Length: 2500
Content-Range: bytes 0-2499/4580
[...]
来自客户端应用程序的后续请求可以检索资源的剩余部分。
REST 背后的主要动机之一是它应能够导航整个资源集,而无需事先了解 URI 方案。 每个 HTTP GET 请求应通过响应中包含的超链接返回查找与所请求的对象直接相关的资源所需的信息,还应为它提供描述其中每个资源提供的操作的信息。 此原则称为 HATEOAS 或作为应用程序状态引擎的超文本。 该系统实际上是有限状态机,每个请求的响应包含从一种状态转为另一种状态所需的信息;任何其他信息都不应是必需的。
当前没有如何为 HATEOAS 原则建模的任何通用标准。 此部分的示例展示了一个可能的专有解决方案。
例如,若要处理订单与客户之间的关系,可以在订单的表示形式中包含链接,用于指定下单客户可以执行的操作。 下面是可能的表示形式:
"orderID":3,
"productID":2,
"quantity":4,
"orderValue":16.60,
"links":[
"rel":"customer",
"href":"https://adventure-works.com/customers/3",
"action":"GET",
"types":["text/xml","application/json"]
"rel":"customer",
"href":"https://adventure-works.com/customers/3",
"action":"PUT",
"types":["application/x-www-form-urlencoded"]
"rel":"customer",
"href":"https://adventure-works.com/customers/3",
"action":"DELETE",
"types":[]
"rel":"self",
"href":"https://adventure-works.com/orders/3",
"action":"GET",
"types":["text/xml","application/json"]
"rel":"self",
"href":"https://adventure-works.com/orders/3",
"action":"PUT",
"types":["application/x-www-form-urlencoded"]
"rel":"self",
"href":"https://adventure-works.com/orders/3",
"action":"DELETE",
"types":[]
在此示例中,links
数组包含一组链接。 每个链接表示可对相关实体执行的操作。 每个链接的数据包含关系 ("customer")、URI (https://adventure-works.com/customers/3
)、HTTP 方法和支持的 MIME 类型。 这是客户端应用程序在调用操作时所需的全部信息。
links
数组还包含有关已检索的资源本身的自引用信息。 这些链接包含关系 self。
返回的链接集可能会根据资源的状态发生更改。 这就是为何将超文本称作“应用程序状态引擎”的原因。
对 RESTful Web API 进行版本控制
Web API 一直保持静态的可能性很小。 随着业务需求变化,可能会添加新的资源集合,资源之间的关系可能会更改,并且可能会修改资源中的数据结构。 虽然更新 Web API 以处理新的或不同的需求是一个相对简单的过程,但你必须考虑此类更改将对使用该 Web API 的客户端应用程序造成的影响。 问题在于,尽管设计和实现 Web API 的开发人员可以完全控制该 API,但开发人员对客户端应用程序不具有相同程度的控制,因为这些客户端应用程序可能是由远程运营的第三方组织生成的。 主要规则是要让现有客户端应用程序能够继续不变地正常运行,同时允许新客户端应用程序利用新功能和新资源。
版本控制使 Web API 可以指定它所公开的功能和资源,并且客户端应用程序可以提交定向到特定版本的功能或资源的请求。 以下各节介绍几种不同的方法,其中每一种方法都有其自己的优势和不足。
无版本控制
这是最简单的方法,它对于一些内部 API 来说可能是可以接受的。 重大更改可以表示为新资源或新链接。 向现有资源添加内容可能未呈现重大更改,因为不应查看此内容的客户端应用程序将会忽略它。
例如,向 URI https://adventure-works.com/customers/3
发出请求应返回包含客户端应用程序所需的 id
、name
和 address
字段的单个客户的详细信息:
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
{"id":3,"name":"Contoso LLC","address":"1 Microsoft Way Redmond WA 98053"}
为简单起见,本部分中所示的示例响应不包含 HATEOAS 链接。
如果 DateCreated
字段已添加到客户资源的架构中,则响应将如下所示:
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
{"id":3,"name":"Contoso LLC","dateCreated":"2014-09-04T12:11:38.0376089Z","address":"1 Microsoft Way Redmond WA 98053"}
现有的客户端应用程序可能会继续正常工作(如果能够忽略无法识别的字段),而新的客户端应用程序则可以设计为处理该新字段。 但是,如果对资源的架构进行了更根本的更改(如删除或重命名字段)或资源之间的关系发生更改,则这些更改可能构成重大更改,从而阻止现有客户端应用程序正常工作。 在这些情况下应考虑以下方法之一:
URI 版本管理
每次修改 Web API 或更改资源的架构时,向每个资源的 URI 添加版本号。 以前存在的 URI 应像以前一样继续运行,并返回符合原始架构的资源。
继续解释前面的示例,如果将 address
字段重构为包含地址的每个构成部分的子字段(例如 streetAddress
、city
、state
和 zipCode
),则可通过包含版本号的 URI(如 https://adventure-works.com/v2/customers/3
)公开此版资源:
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
{"id":3,"name":"Contoso LLC","dateCreated":"2014-09-04T12:11:38.0376089Z","address":{"streetAddress":"1 Microsoft Way","city":"Redmond","state":"WA","zipCode":98053}}
此版本控制机制非常简单,但依赖于将请求路由到相应终结点的服务器。 但是,随着 Web API 经过多次迭代而变得成熟,服务器必须支持多个不同版本,它可能变得难以处理。 此外,简单而言,在所有情况下客户端应用程序都要提取相同数据(客户 3),因此 URI 实在不应该因版本而有所不同。 此方案也增加了 HATEOAS 实现的复杂性,因为所有链接都需要在其 URI 中包括版本号。
查询字符串版本控制
不是提供多个 URI,而是可以通过在追加到 HTTP 请求后面的查询字符串中使用参数来指定资源的版本,例如 https://adventure-works.com/customers/3?version=2
。 如果 version 参数被较旧的客户端应用程序省略,则应默认为有意义的值(例如 1)。
此方法具有语义优势(即,同一资源始终从同一 URI 进行检索),但它依赖于代码处理请求以分析查询字符串并发送回相应的 HTTP 响应。 此方法也与 URI 版本控制机制一样,增加了实现 HATEOAS 的复杂性。
某些较旧的 Web 浏览器和 Web 代理不会缓存在 URI 中包含查询字符串的请求的响应。 这可能会对使用 Web API 以及从此类 Web 浏览器运行的 Web 应用的性能产生不利影响。
不是追加版本号作为查询字符串参数,而是可以实现指示资源的版本的自定义标头。 此方法需要客户端应用程序将相应标头添加到所有请求,虽然如果省略了版本标头,处理客户端请求的代码可以使用默认值(版本 1)。 下面的示例使用了名为 Custom-Header 的自定义标头。 此标头的值指示 Web API 的版本。
版本 1:
GET https://adventure-works.com/customers/3 HTTP/1.1
Custom-Header: api-version=1
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
{"id":3,"name":"Contoso LLC","address":"1 Microsoft Way Redmond WA 98053"}
版本 2:
GET https://adventure-works.com/customers/3 HTTP/1.1
Custom-Header: api-version=2
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
{"id":3,"name":"Contoso LLC","dateCreated":"2014-09-04T12:11:38.0376089Z","address":{"streetAddress":"1 Microsoft Way","city":"Redmond","state":"WA","zipCode":98053}}
与前面两个方法一样,实现 HATEOAS 需要在任何链接中加入相应的自定义标头。
如本指南前面所述,当客户端应用程序向 Web 服务器发送 HTTP GET 请求时,它应使用 Accept 标头规定它可以处理的内容的格式。 通常,Accept 标头的用途是允许客户端应用程序指定响应的正文应是 XML、JSON 还是客户端可以分析的其他某种常见格式。 但是,可以定义包括以下信息的自定义媒体类型:该信息使客户端应用程序可以指示它所需的资源版本。
下面的示例演示了将 Accept 标头指定为值 application/vnd.adventure-works.v1+json 的请求。 vnd.adventure-works.v1 元素向 Web 服务器指示它应返回资源的版本 1,而 json 元素则指定响应正文的格式应为 JSON:
GET https://adventure-works.com/customers/3 HTTP/1.1
Accept: application/vnd.adventure-works.v1+json
处理请求的代码负责处理 Accept 标头并尽可能采用该值(客户端应用程序可以在 Accept 标头中指定多种格式,在这种情况下,Web 服务器可以在其中选择最适合的格式用于响应正文)。 Web 服务器使用 Content-Type 标头确认响应正文中的数据格式:
HTTP/1.1 200 OK
Content-Type: application/vnd.adventure-works.v1+json; charset=utf-8
{"id":3,"name":"Contoso LLC","address":"1 Microsoft Way Redmond WA 98053"}
如果 Accept 标头未指定任何已知的媒体类型,则 Web 服务器可以生成 HTTP 406(不可接受)响应消息或返回使用默认媒体类型的消息。
此方法可以说是最纯粹的版本控制机制并自然地适用于 HATEOAS,后者可以在资源链接中包含相关数据的 MIME 类型。
选择版本控制策略时,还应考虑对性能的影响,尤其是在 Web 服务器上缓存时。 URI 版本控制和查询字符串版本控制方案都是缓存友好的,因为同一 URI/查询字符串组合每次都指向相同的数据。
标头版本控制和媒体类型版本控制机制通常需要其他逻辑来检查自定义标头或 Accept 标头中的值。 在大型环境中,使用不同版本的 Web API 的多个客户端可能会在服务器端缓存中生成大量重复数据。 如果客户端应用程序通过实现缓存的代理与 Web 服务器进行通信,并且该代理在当前未在其缓存中保留所请求数据的副本时,仅将请求转发到 Web 服务器,则此问题可能会变得很严重。
Open API 计划
Open API 计划由一个行业协会创建,目的是标准化供应商的 REST API 说明。 作为该计划的一部分,Swagger 2.0 规范被重新命名为 OpenAPI 规范 (OAS),并引入 Open API 计划。
建议为 Web API 采用 OpenAPI。 考虑的要点:
OpenAPI 规范随附了一组有关如何设计 REST API 的强制性准则。 这有益于互操作性,但在设计 API 时需多加注意,以符合规范。
OpenAPI 首推协定优先的方法,而不是实现优先的方法。 协定优先意味着首先设计 API 协定(接口),然后写入实现协定的代码。
Swagger 之类的工具可以从 API 协定生成客户端库或文档。 有关示例,请参阅使用 Swagger 的 ASP.NET Web API 帮助页。
Microsoft Azure REST API 准则。 有关在 Azure 上设计 REST API 的详细建议。
Web API 清单。 设计和实现 Web API 时要考虑的有用事项列表。
开放式 API 计划。 有关开放式 API 的文档和实施详细信息。