别再把你的 API 版本管理搞砸了!
如果你正在使用 /v1/products 和 /v2/products,那么这篇文章就是为你准备的
最近,我接到一个需求,要将我的 API 更新到一个新版本,同时还要运行旧版本以支持现有的客户端。
我原来的端点是 /products
。
于是,我想:
让我们把当前版本切换到 /v1/products
,新版本切换到 /v2/products
。很简单,对吧?
GET /v1/products GET /v2/products
这种方法感觉“没错,就该这么做”,
但是
它违反了 REST 原则,还会让维护工作变得异常棘手。
简单性陷阱
@RestController
@RequestMapping("/v1/products")
public class ProductControllerV1 {... }
@RestController
@RequestMapping("/v2/products")
public class ProductControllerV2 {... }
没错,实现起来很快,对吧?嗯……但是,我们要维护两个控制器(还有测试、文档……)。
教程和遗留系统让 URL 版本控制变得很常见。如果我们在谷歌上搜索版本控制,会发现很多资料都提到了 URL API 版本控制。
为什么在 URL 中进行版本控制会破坏 REST?
URI 应该是永恒不变的
REST 提到,URI 是资源的稳定标识符。当我们对 URL 进行版本控制时:
- 客户端会硬编码像
/v1/products
这样的路径,从而造成紧密耦合。 - 更改 URI 就像更改一本书的 ISBN 一样——会破坏所有引用它的图书馆。
维护并不容易
几年后,
/v1/products /v2/products /v3/products /legacy/products
现在我们面临着以下问题:
- 代码重复。
- 不能停用已弃用的版本(客户端仍在使用它们)。
- 文档臃肿。
客户端静默失败
当 /v1/products
被弃用时:
- 使用它的移动应用开始返回
404
错误。 - 用户没有收到任何警告——只是功能无法使用。
RESTful 的 API 版本控制替代方案
通过请求头进行版本控制
使用 Accept
请求头来协商版本:
GET /products Accept: application/vnd.myapi.v2+json
@GetMapping(value = "/users", produces = {
"application/vnd.myapi.v1+json",
"application/vnd.myapi.v2+json"
})
public ResponseEntity<?> getUsers(HttpServletRequest request) {
String acceptHeader = request.getHeader("Accept");
if (acceptHeader.contains("v2")) {
return ResponseEntity.ok(userService.getUsersV2());
} else {
return ResponseEntity.ok(userService.getUsersV1());
}
}
优点
- URI 保持稳定(
/products
永远不变) - 向后兼容性更容易实现(从一个端点提供多个版本)。
- 缓存自然生效(版本是请求头的一部分)。
缺点
- 客户端必须配置请求头(一些工具/库使得这并不容易实现)
Stripe:通过使用请求头,在不使 URL 变得杂乱的情况下将 API 版本扩展到了 10 多个。
通过自定义媒体类型进行版本控制
为什么这种方法可行
媒体类型(如 application/vnd.github.v3+json
)明确声明了格式和版本,从而将版本控制与 URI 解耦。
如何实现
为每个版本定义媒体类型:
GET /users HTTP/1.1
Accept: application/vnd.company.products.v2+json
服务器端逻辑:
- 解析
Accept
请求头以确定版本。 - 以请求的格式返回数据。
实际示例:
- GitHub:
Accept: application/vnd.github.v3+json
- Stripe:
Accept: application/vnd.stripe.v2+json
优点:
- 一种清晰、标准化的版本协商方式。
- 支持在媒体类型中使用语义版本控制(例如,
v1.2.3
)
缺点:
- 需要文档来指导客户端
GitHub:通过媒体类型多年来一直保持着向后兼容性。
为什么这些替代方案比 URL 版本控制更好
- 稳定的 URI:客户端收藏的是
/products
,而不是/v1/products
。 - 向后兼容性:从同一个端点提供旧版本和新版本。
- 代码更简洁:没有重复的控制器或路由。
- 符合 REST 规范:符合 Fielding 的约束条件。
如果你无法摆脱 URL 版本控制,那么应该只在以下两种情况下使用:
- 内部 API(你所控制的微服务)。
- 改变资源标识的重大变更(如 GDPR 规定的结构变更)。
即便如此,也要问一问:“我们能不能通过请求头来进行版本控制呢?”
关键要点
- URI 是永恒的 —— 不要将它们与版本紧密耦合。
- 请求头优于 URL,有利于可扩展性和符合 REST 规范。
- 指导客户端使用
Accept
请求头——这是值得的。
同意吗?不同意? → 点赞 👏