同时支撑 v1、v2、v3 三个接口版本,最优解是把版本决策收口到网关层:用统一的路由规则把请求分发到不同后端,旧版本通过适配器做契约转换,新版本作为唯一真源。这样后端不必为历史版本各维护一套代码,客户端也能按自己的节奏迁移。
一、版本放在哪里:四种路线对比
版本标识的位置决定了路由复杂度、缓存行为和客户端改造成本。主流做法有四类,网关都能收敛到同一个入口做分发。
| 策略 | 版本位置 | 可见性 | 缓存难度 | 客户端成本 | 适用场景 |
|---|---|---|---|---|---|
| URI 路径版本 | /v1/orders、/v2/orders |
高(URL 可见) | 低(天然按 URL 缓存) | 低 | 公开 API、简单版本控制 |
| 请求头版本 | X-API-Version: 2 |
低 | 中(需 Vary 头) |
中 | 企业内部、内部 API |
| 查询参数版本 | ?version=2 |
中 | 中 | 低 | 快速修复、向后兼容 |
| 内容协商 | Accept: application/vnd.x.v2+json |
低 | 高(需 Vary: Accept) |
高 | RESTful、多表示形式 |
URI 版本因显式、可见、网关路由零成本,成为公开 API 的默认选择;内容协商最贴近 REST 语义,但要求每个客户端正确设置 Accept,调试也更费劲。
Stripe 把版本和日期绑定(Stripe-Version: 2024-06-20),每个 API Key 钉死创建时的版本,服务端改默认值也不影响存量调用;GitHub 的 REST API 走 Accept: application/vnd.github.v3+json。两者都证明:版本契约可以由网关统一解耦,不必耦合到资源路径里。
二、网关层多版本共存的落地步骤
把多版本落到生产,按下面四步推进,后端代码保持单真源,历史版本由网关侧适配器兜住。
- 选定版本策略(公开 API 建议 URI,内部 API 可用 Header),并规定破坏性变更的定义;
- 在网关配置按版本前缀或 Header 匹配路由,分别指向 v1、v2、v3 上游;
- 为旧版本部署适配器服务,把老契约翻译成新版本的内部模型,避免双代码库;
- 给每个版本打上监控标签,按版本统计流量,设定并公告下线时间线。
2.1 路径路由配置(Spring Cloud Gateway)
URI 版本下,网关按路径前缀分流,并剥离前缀让上游无感知:
spring:
cloud:
gateway:
routes:
- id: orders-v1
uri: lb://orders-v1
predicates:
- Path=/v1/orders/**
filters:
- StripPrefix=1
- id: orders-v2
uri: lb://orders-v2
predicates:
- Path=/v2/orders/**
filters:
- StripPrefix=1
- id: orders-v3
uri: lb://orders-v3
predicates:
- Path=/v3/orders/**
filters:
- StripPrefix=1
2.2 基于 Header 的路由(Kong)
内部 API 常用 Header 路由,URL 保持稳定,版本藏在请求头里:
services:
- name: orders-v1
url: http://orders-v1:8080
routes:
- name: orders-v1-route
paths: ["/orders"]
headers:
X-API-Version: ["1"]
- name: orders-v2
url: http://orders-v2:8080
routes:
- name: orders-v2-route
paths: ["/orders"]
headers:
X-API-Version: ["2"]
Kong 按 X-API-Version 命中对应服务;缺失时建议显式设默认版本,避免路由歧义。
三、v1→v2→v3 的演进与退役
版本管理的难点不是上线,而是收尾。长期并存的版本越多,维护、监控、安全加固的成本就线性叠加,必须靠纪律性下线来止血。
3.1 破坏性变更判定
不是所有改动都值得升主版本。以下改动属于破坏性变更,必须走新版本:删除或重命名字段、改变字段类型、收紧校验导致旧请求被拒、改变错误格式与语义、下线端点。而增加可选字段、新增端点、扩充枚举值(客户端容忍未知值的前提下)属于兼容变更,不升主版本。
3.2 用适配器隔离旧版本
运维上把 v1、v2 流量路由到同一套新版本服务,由适配器做契约翻译,新版本始终是唯一真源。适配器用 Python 表达核心思路:
def adapt_v1_to_v3(payload: dict) -> dict:
# v1 用 name,v3 拆为 first_name / last_name
name = payload.pop("name", "")
if name:
first, _, last = name.partition(" ")
payload["first_name"] = first
payload["last_name"] = last
# v1 无 status 字段,补默认值
payload.setdefault("status", "active")
return payload
网关在转发 v1 请求时先过这道转换,后端只认 v3 模型。退役 v1 时删掉适配器与路由规则即可,业务代码零改动。
四、两个易错点
版本窗口放太宽是常见隐患:给旧版本无限期兼容,客户端永不迁移,版本数量膨胀成永久负担。发布公开下线时间线、在旧版本响应头加 Deprecation 和 Sunset 字段、按版本监控用量,到期限就强制回收。
另一点是缓存串版本。Header 或内容协商路由下,CDN 和代理默认按 URL 缓存,会混流不同版本。必须返回 Vary: X-API-Version 或 Vary: Accept,让缓存按版本维度区分,否则用户可能取到错误版本的数据。
常见问题(FAQ)
Q1:内部 API 该用 URI 还是 Header 版本?
内部 API 推荐 Header 或内容协商,保持 URL 稳定,降低调用方改造量。
Q2:旧版本要维护到什么时候?
公开下线时间线,加 Sunset 头,监控用量到阈值即回收,避免版本堆积。
Q3:新版本上线要双写代码吗?
不必。旧版本走适配器翻译到新版本,新版本作唯一真源,退役时删适配器即可。