1. 问题背景
REST API 的演进是个棘手问题——存在多种解决方案。本文将探讨其中几种主流方案。
2. 契约的核心要素
首先需要回答一个关键问题:API 与客户端之间的契约究竟是什么?
2.1 URI 是否属于契约?
先看 REST API 的 URI 结构——它属于契约的一部分吗?客户端是否应该集合、硬编码或依赖这些 URI?
如果答案是肯定的,那么客户端与 REST 服务的交互将不再由服务本身驱动,而是依赖 Roy Fielding 所称 的 out-of-band 信息:
REST API 的入口只需初始 URI(书签)和适合目标受众的标准化媒体类型...失败意味着交互由带外信息驱动而非超文本。
显然 URI 不属于契约!客户端只需知道一个 URI——API 的入口点。其他所有 URI 都应在消费 API 时动态发现。
2.2 媒体类型是否属于契约?
资源表示所用的媒体类型信息——这些属于客户端与服务间的契约吗?
为成功消费 API,客户端必须预先了解这些媒体类型。实际上,这些媒体类型的定义构成了整个契约。
因此,REST 服务应重点关注:
REST API 应将绝大部分描述工作用于定义资源表示和驱动应用状态的媒体类型,或定义现有标准媒体类型的扩展关系名/超文本标记。
所以 媒体类型定义属于契约,且应是消费 API 的客户端的先验知识。标准化正是为此而生。
现在我们明确了契约的核心,接下来探讨如何解决版本控制问题。
3. 高层方案
以下是 REST API 版本控制的两种主流方案:
- URI 版本控制 – 在 URI 中嵌入版本标识符
- 媒体类型版本控制 – 对资源表示进行版本控制
当在 URI 中引入版本时,资源表示被视为不可变。因此当 API 需要变更时,必须创建新的 URI 空间。
例如,某 API 发布了用户和权限资源:
http://host/v1/users
http://host/v1/privileges
当用户 API 需要破坏性变更时,引入第二个版本:
http://host/v2/users
http://host/v2/privileges
当对媒体类型进行版本控制并扩展语言时,我们通过内容协商机制处理。REST API 会使用自定义 供应商 MIME 媒体类型 替代通用类型(如 application/json
)。我们版本控制的是媒体类型而非 URI。
例如:
===>
GET /users/3 HTTP/1.1
Accept: application/vnd.myname.v1+json
<===
HTTP/1.1 200 OK
Content-Type: application/vnd.myname.v1+json
{
"user": {
"name": "John Smith"
}
}
关于此主题的更多信息和示例,可参考这篇《REST API 自定义媒体类型》文章。
关键点在于:客户端不能对响应结构做任何超出媒体类型定义的假设。
这就是通用媒体类型不理想的原因——它们缺乏足够的语义信息,迫使客户端依赖额外提示来处理资源表示。
例外情况是使用其他方式唯一标识内容语义(如 XML schema)。
4. 方案优劣分析
现在我们明确了契约的核心要素,也了解了版本控制的高层方案,下面分析各方案的优缺点。
首先,在 URI 中引入版本标识符会导致 URI 空间急剧膨胀。因为任何 API 的破坏性变更都会为整个 API 创建全新的资源表示树。随着时间推移,这会成为维护负担,也让客户端面临更多选择困境。
URI 版本标识符还严重缺乏灵活性。无法单独演进某个资源或 API 子集。
如前所述,这是"全有或全无"的方案。若部分 API 升级到新版本,整个 API 必须同步升级。这导致客户端从 v1 升级到 v2 成为重大工程——升级速度变慢,旧版本生命周期被迫延长。
HTTP 缓存也是版本控制的关键考量。
从中间代理缓存的角度看,两种方案各有优劣:
- ✅ URI 版本化:缓存需为每个资源维护多份副本(每个 API 版本一份),增加缓存负担,降低命中率(不同客户端使用不同版本)
- ❌ 部分缓存失效机制失效
- ⚠️ 媒体类型版本化:客户端和服务端需支持 Vary HTTP 头 以标识存在多个缓存版本
从客户端缓存角度看,媒体类型版本化方案比 URI 版本化稍复杂——因为用 URL 作为缓存键比用媒体类型更直接。
最后引用 API Evolution 的目标作为总结:
- 兼容性变更不体现在名称中
- 避免引入主版本号
- 保持向后兼容
- 考虑向前兼容
5. API 变更类型
接下来分析 REST API 的变更类型:
- 表示格式变更
- 资源变更
5.1 资源表示的扩展
媒体类型的格式设计应考虑向前兼容性。具体来说,客户端应忽略无法理解的信息(JSON 在这方面比 XML 更好)。
若客户端实现正确,在资源表示中新增信息不会破坏现有客户端。
延续之前的例子,在用户表示中添加 amount
字段不是破坏性变更:
{
"user": {
"name": "John Smith",
"amount": "300"
}
}
5.2 删除或修改现有表示
删除、重命名或重构现有表示中的信息是破坏性变更——因为客户端已依赖旧格式。
此时需要内容协商机制:可引入新的供应商 MIME 媒体类型。
延续前例,假设需将用户 name
拆分为 firstname
和 lastname
:
===>
GET /users/3 HTTP/1.1
Accept: application/vnd.myname.v2+json
<===
HTTP/1.1 200 OK
Content-Type: application/vnd.myname.v2+json
{
"user": {
"firstname": "John",
"lastname": "Smith",
"amount": "300"
}
}
这对客户端是不兼容变更——需请求新表示并理解新语义。但 URI 空间保持稳定不受影响。
5.3 重大语义变更
这类变更涉及资源含义、资源间关系或后端映射关系的改变。可能需要:
- 新媒体类型
- 在旧资源旁发布新资源,并通过链接指向
这听起来像又回到了 URI 版本控制,但关键区别在于:新资源独立于 API 中其他资源发布,不会在根级别分叉整个 API。
REST API 应遵循 HATEOAS 约束:大多数 URI 应由客户端动态发现而非硬编码。修改此类 URI 不应视为不兼容变更——新 URI 可替换旧 URI,客户端能重新发现并正常工作。
需注意:虽然 URI 版本控制存在诸多问题,但它并不违反 REST 原则。
6. 结论
本文概述了 REST 服务演进这个复杂问题,讨论了两种主流方案及其优缺点,并在 REST 语境下分析了这些方案。
最终结论是:推荐媒体类型版本控制方案,同时结合 RESTful API 的可能变更类型进行考量。
7. 延伸阅读
以下资源贯穿全文,值得深入研究: