
本规范旨在指导开发者使用自定义媒体类型(Content Negotiation)进行 API 版本控制,以实现更优雅、更可持续的 API 设计。
1. 概述
传统的 URL 版本化(例如 /v1/orders)存在诸多问题,如 URI 不稳定、客户端耦合度高、代码冗余等。本规范推荐使用自定义媒体类型,将版本信息嵌入 HTTP 的媒体类型(MIME type)中,通过内容协商来决定后端该使用哪一版逻辑。
2. 媒体类型定义
根据 RFC 6838 规范,自定义媒体类型的格式如下:
<type>/vnd.<vendor>.<resource>-v<version>+<suffix>
<type>: 顶级类型,通常为application。vnd: 表示厂商特定的媒体类型。<vendor>: 厂商或组织的名称。<resource>: 资源名称。-v<version>: API 的版本号,可选。<suffix>: 结构化后缀,指示底层编码格式,如json、xml。
示例:
application/vnd.example.order-v1+json:Example 厂商的 Order 资源的第一个版本,使用 JSON 格式。application/vnd.example.order-v2+xml:Example 厂商的 Order 资源的第二个版本,使用 XML 格式。application/vnd.example.order+json:Example 厂商的 Order 资源的最新版本(未指定版本号)。
3. 客户端请求
客户端通过 Accept 请求头指定期望的 API 版本。
示例:
GET /orders/123 HTTP/1.1
Accept: application/vnd.example.order-v2+json
在 POST/PUT 请求中,客户端可以通过 Content-Type 请求头指定请求体的版本。
示例:
POST /orders HTTP/1.1
Content-Type: application/vnd.example.order-v1+json
4. 服务端实现 (Java)
4.1 Spring MVC 配置
在 Spring MVC 中,可以使用 @RequestMapping 注解的 produces 和 consumes 属性来指定支持的媒体类型。
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/orders")
public class OrderController {
@GetMapping(value = "/{orderId}", produces = "application/vnd.example.order-v1+json")
public ResponseEntity<OrderV1> getOrderV1(@PathVariable String orderId) {
// 处理 V1 版本的逻辑
OrderV1 order = new OrderV1(orderId, "Order V1 Details");
return ResponseEntity.ok(order);
}
@GetMapping(value = "/{orderId}", produces = "application/vnd.example.order-v2+json")
public ResponseEntity<OrderV2> getOrderV2(@PathVariable String orderId) {
// 处理 V2 版本的逻辑
OrderV2 order = new OrderV2(orderId, "Order V2 Details with extra fields");
return ResponseEntity.ok(order);
}
@PostMapping(consumes = "application/vnd.example.order-v1+json", produces = "application/vnd.example.order-v1+json")
public ResponseEntity<OrderV1> createOrderV1(@RequestBody OrderV1 order) {
// 处理 V1 版本的创建订单逻辑
return ResponseEntity.ok(order);
}
@PostMapping(consumes = "application/vnd.example.order-v2+json", produces = "application/vnd.example.order-v2+json")
public ResponseEntity<OrderV2> createOrderV2(@RequestBody OrderV2 order) {
// 处理 V2 版本的创建订单逻辑
return ResponseEntity.ok(order);
}
}
// V1 版本的订单类
class OrderV1 {
private String orderId;
private String details;
public OrderV1(String orderId, String details) {
this.orderId = orderId;
this.details = details;
}
public String getOrderId() { return orderId; }
public String getDetails() { return details; }
}
// V2 版本的订单类
class OrderV2 {
private String orderId;
private String details;
public OrderV2(String orderId, String details) {
this.orderId = orderId;
this.details = details;
}
public String getOrderId() { return orderId; }
public String getDetails() { return details; }
}
4.2 版本解析器 (Version Resolver)
如果需要更灵活的版本控制,可以自定义版本解析器,根据 Accept 头动态选择处理逻辑。
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.condition.AbstractRequestCondition;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class ApiVersionCondition extends AbstractRequestCondition<ApiVersionCondition> {
private static final Pattern VERSION_PATTERN = Pattern.compile(".*-v(\\d+).*");
private final int version;
public ApiVersionCondition(int version) {
this.version = version;
}
public int getVersion() {
return version;
}
@Override
protected Collection<?> getContent() {
return List.of(version);
}
@Override
protected String getToStringInfix() {
return "apiVersion=";
}
@Override
public ApiVersionCondition combine(ApiVersionCondition other) {
// 总是使用最新的版本
return other;
}
@Override
public ApiVersionCondition getMatchingCondition(ServerWebExchange exchange) {
String accept = exchange.getRequest().getHeaders().getFirst("Accept");
if (accept != null) {
Matcher matcher = VERSION_PATTERN.matcher(accept);
if (matcher.find()) {
int version = Integer.parseInt(matcher.group(1));
if (version == this.version) {
return this;
}
}
}
return null;
}
@Override
public int compareTo(ApiVersionCondition other, ServerWebExchange exchange) {
// 优先匹配版本号大的
return other.version - this.version;
}
}
4.3 使用自定义版本解析器
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/orders")
public class OrderController {
@GetMapping("/{orderId}")
@ApiVersion(1)
public ResponseEntity<OrderV1> getOrderV1(@PathVariable String orderId) {
// 处理 V1 版本的逻辑
OrderV1 order = new OrderV1(orderId, "Order V1 Details");
return ResponseEntity.ok(order);
}
@GetMapping("/{orderId}")
@ApiVersion(2)
public ResponseEntity<OrderV2> getOrderV2(@PathVariable String orderId) {
// 处理 V2 版本的逻辑
OrderV2 order = new OrderV2(orderId, "Order V2 Details with extra fields");
return ResponseEntity.ok(order);
}
}
5. 版本废弃策略
- 提前通知: 在 API 响应头中添加
Deprecation字段,告知客户端该版本即将废弃。 - 提供迁移指南: 提供详细的迁移指南,帮助客户端升级到新版本。
- 逐步下线: 逐步降低旧版本的优先级,最终停止支持。
示例:
HTTP/1.1 200 OK
Content-Type: application/vnd.example.order-v1+json
Deprecation: version=v1, sunset=2024-12-31, link="https://example.com/api/migration"
6. 优点
- URI 稳定: 避免了因版本更新而频繁更改 URL。
- 客户端和服务器解耦: 客户端无需硬编码版本号。
- 避免路径臃肿和重复代码: 减少了相似 Controller、路由、文档和测试的维护工作。
- 利于缓存和中间件优化: 版本信息在请求头中,使得缓存策略可以更好地复用。
- 平滑升级和废弃: 可以逐步提示客户端升级,而不是直接返回 404。
- 标准明确,版本协商清晰: 通过自定义媒体类型,可以清晰地表达版本信息。
7. 缺点
- 客户端需要支持并设置请求头: 客户端必须支持并正确设置
Accept请求头。 - 需要客户端文档指导: 需要清晰的文档指导客户端如何使用
Accept头指定版本。
8. 总结
使用自定义媒体类型进行 API 版本控制是一种优雅且可持续的方案,能够有效地解决 URL 版本化带来的问题,并提升 API 的可维护性和可扩展性。虽然有一定的学习成本和客户端改造工作,但长期来看,收益远大于成本。
说明:
- 以上代码示例仅为演示,实际应用中需要根据具体情况进行调整。
- 请确保客户端正确设置
Accept请求头,并提供清晰的文档指导。 - 制定清晰的版本废弃策略,并提前通知客户端。
- 监控 API 的版本使用情况,并记录版本相关的日志。
希望这份指导规范能够帮助你更好地进行 API 版本控制。
原创不易,如果觉得此文对你有帮助,不妨点赞+收藏+关注,你的鼓励是我持续创作的动力!

原创文章,作者:诺多,如若转载,请注明出处:https://www.huluohu.com/posts/1548/
微信扫一扫
支付宝扫一扫
评论列表(2条)
这种方式还挺新颖的,现在大多数都是直接在接口上面加/v1、/v2 ~
@青萍叙事:是的,感觉挺不错。