Appearance
版本设计
本章导读:先说一个反直觉结论——理想的 REST 架构不需要 URI 版本号(Fielding 的立场:HATEOAS 可以让客户端"发现"变化)。但现实是绝大多数团队没有超媒体驱动,URI 一旦外发就改不动。所以本章讲工程上的版本策略:版本放哪、major/minor 怎么分、如何弃用而不炸客户端。
1. 什么时候需要版本
只有**破坏性变更(breaking change)**才需要升版本。先分清变更类型:
| 变更 | 兼容性 | 例子 | 处理 |
|---|---|---|---|
| 向后兼容 | ✅ | 响应新增字段;新增可选查询参数;新增端点 | 直接上线(客户端按"未知字段忽略"原则设计) |
| 语义变更 | ⚠️ | status=published 含义变化;默认值改变 | 文档 + 新查询参数灰度,或升版本 |
| 破坏性变更 | ❌ | 删除/重命名字段;字段类型变化;URI 结构变化;校验变严 | 升版本 |
治理要求: 团队规范必须写死"响应新增字段不算破坏性变更",且客户端禁止做"严格模式解析"(未知字段报错)——这是版本能少活一半的前提。
2. 版本号放哪里
| 方案 | 例 | 优点 | 缺点 |
|---|---|---|---|
| URI 路径(主流推荐) | /v1/articles 或 /api/v1/articles | 可见、可路由、网关友好、日志可查、缓存天然分键 | URI 成为契约的一部分,迁移需重定向 |
| 查询参数 | /articles?api-version=1.0 | 实现与测试简单 | CDN/网关忽略查询串时会串版本;不可路由 |
| 请求头 | Accept: application/vnd.example.api-v1+json | URI 干净、贴近"内容协商"本质 | 不可浏览、测试麻烦、代理无法路由 |
| 响应头声明 | 服务端回 API-Version: 1.2 | 仅作信息展示 | 不能用于选择版本 |
实践建议: 对外 API 用 URI 路径版本(GitHub、Stripe、AWS 皆如此),理由不是"优雅"而是可运维性——网关按路径分流、监控按路径聚合、文档按路径生成。追求极致兼容的团队可"URI 主版本 + 头部次版本"组合。
3. 版本粒度:major 进 URI,minor 全局
/v1/articles ← URI 只承载 major 版本
响应头: API-Version: 1.7.2 ← 可选:细粒度版本以头部/文档披露- major:不兼容变更的聚合包装窗(v1 → v2)。
- minor/patch:向后兼容的功能迭代,不体现在 URI。
- 语义化版本(semver)在 API 层的映射:MAJOR=破坏性、MINOR=加功能、PATCH=修 bug。
4. 多版本共存策略
┌─────────────── gateway ───────────────┐
客户端 /v1/articles ─► 路由到 blog-svc:v1 部署单元 │
客户端 /v2/articles ─► 路由到 blog-svc:v2 部署单元 │
└───────────────────────────────────────┘- 代码层多版本:同一服务内并存 v1/v2 控制器——适合小改动,慎用(逻辑分叉难维护)。
- 部署层多版本(推荐):网关按路径前缀路由到不同版本服务实例,服务内部只有"当前版本"。
- 反腐层:新版本服务内部适配旧模型,
v1 → adapter → v2 core,只保留一份业务逻辑。
共存窗口 = 旧版本用户迁移期。目标:任何时刻并行版本 ≤ 2。
5. 弃用与日落(Deprecation & Sunset)
废弃端点的标准动作(RFC 9745 Deprecation 头 + RFC 8594 Sunset 头):
http
GET /v1/articles/42
HTTP/1.1 200 OK
Deprecation: Thu, 31 Dec 2026 23:59:59 GMT
Sunset: Fri, 01 Jan 2027 00:00:00 GMT
Link: <https://docs.example.com/migrate-v2>; rel="deprecation"; type="text/html"
Link: </v2/articles/42>; rel="successor-version"
{ "id": 42, ... }时间线模板:
t0 发布 v2,v1 进入维护(仅 bugfix)
t0+公告 文档/CHANGELOG/邮件通知;Deprecation/Sunset 头上线
t0+6mo v1 请求开始记录调用方;对仍在使用的大客户定向支持
t0+12mo Sunset 到期:v1 返回 410 Gone(或删除路由)——保留重定向更久禁止:悄悄删除已发布端点、未通知收紧校验(如原来允许 200 字符现在 100)。
6. 无版本化(URI Versionless)路线
成熟团队终局常是"少版本":
- 只做加法(新增字段/端点/枚举值),永不原地改语义;
- 确需破坏时,新旧端点并存(
/articles/{id}/summaries与旧的整页表示),靠链接/文档引导迁移,而不是全 API 升 major; - 用 HATEOAS/
Link头让"可用操作"随状态变化(412/409 的Link: rel="fixing-hypermedia"——IETF draft-dnotakis-restful-problem 思路)。
TIP
判断自己该不该上 URI 版本:调用方是否可控?(公司内部 API:可控 → 少版本、强治理;对外开放:不可控 → 版本化 + 弃用流程是生命线。)
7. 本章小结
- 版本只为破坏性变更服务;治理"兼容变更自由通行"能大幅减少版本数量。
- 对外 API:URI 放 major(
/v1),细粒度版本进头部与文档。 - 弃用要"有头有流程":
Deprecation(RFC 9745)、Sunset(RFC 8594)、Link指路迁移文档。 - 并行版本 ≤ 2;到期返回 410,而不是静默失败。
8. 下一步
列表接口的"三件套":分页、过滤、排序 → 分页、过滤与排序