API 版本控制:Spring Boot 多版本路由的六种实现方案对比
·
API 版本控制:Spring Boot 多版本路由的六种实现方案对比
标签:Spring Boot | API 版本控制 | RESTful | 架构设计
一、问题引入
当你的 API 被 100+ 客户端调用时,一次不兼容的变更可能导致大面积故障。如何优雅地管理 API 版本?
场景:
- V1: GET /api/users/{id} → 返回 {id, name}
- V2: GET /api/users/{id} → 返回 {id, name, email, avatar}
问题:
- 老客户端期望 V1 的精简格式
- 新客户端需要 V2 的完整数据
- 如何同时支持两个版本?
二、六种版本控制方案对比
| 方案 | 实现复杂度 | 可读性 | 缓存友好 | RESTful 纯度 | 推荐度 |
|---|---|---|---|---|---|
| URL Path | 低 | 高 | 高 | 中 | ★★★★★ |
| Header | 中 | 中 | 低 | 高 | ★★★★ |
| Content-Type | 中 | 中 | 中 | 高 | ★★★ |
| Query Parameter | 低 | 中 | 低 | 低 | ★★ |
| API Gateway 路由 | 高 | 高 | 高 | 高 | ★★★★★ |
| 数据库版本配置 | 高 | 低 | 低 | 低 | ★ |
三、方案 1:URL Path(最常用)
@RestController
@RequestMapping("/api/v1/users")
public class UserControllerV1 {
@GetMapping("/{id}")
public UserV1 getUser(@PathVariable Long id) {
return userService.getUserV1(id);
}
}
@RestController
@RequestMapping("/api/v2/users")
public class UserControllerV2 {
@GetMapping("/{id}")
public UserV2 getUser(@PathVariable Long id) {
return userService.getUserV2(id);
}
@GetMapping("/{id}/detail")
public UserDetail getUserDetail(@PathVariable Long id) {
return userService.getUserDetail(id);
}
}
优点:直观、缓存友好、文档自动生成方便
缺点:URL 变更,版本号扩散
四、方案 2:Header 版本控制
@RestController
@RequestMapping("/api/users")
public class UserController {
@GetMapping(value = "/{id}", headers = "X-API-VERSION=1")
public UserV1 getUserV1(@PathVariable Long id) {
return userService.getUserV1(id);
}
@GetMapping(value = "/{id}", headers = "X-API-VERSION=2")
public UserV2 getUserV2(@PathVariable Long id) {
return userService.getUserV2(id);
}
}
自定义注解优化:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@RequestMapping
public @interface ApiVersion {
int[] value();
}
// 自定义 RequestMappingHandlerMapping
public class ApiVersionHandlerMapping extends RequestMappingHandlerMapping {
@Override
protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {
ApiVersion apiVersion = AnnotationUtils.findAnnotation(method, ApiVersion.class);
if (apiVersion == null) return super.getMappingForMethod(method, handlerType);
RequestMappingInfo mappingInfo = super.getMappingForMethod(method, handlerType);
if (mappingInfo == null) return null;
// 添加版本条件
String[] versions = Arrays.stream(apiVersion.value())
.mapToObj(v -> "X-API-VERSION=" + v)
.toArray(String[]::new);
RequestMappingInfo versionMapping = RequestMappingInfo
.paths()
.headers(versions)
.build();
return mappingInfo.combine(versionMapping);
}
}
五、方案 3:Content-Type 协商
@RestController
@RequestMapping("/api/users")
public class UserController {
@GetMapping(value = "/{id}",
produces = "application/vnd.api.v1+json")
public UserV1 getUserV1(@PathVariable Long id) {
return userService.getUserV1(id);
}
@GetMapping(value = "/{id}",
produces = "application/vnd.api.v2+json")
public UserV2 getUserV2(@PathVariable Long id) {
return userService.getUserV2(id);
}
}
客户端请求:
curl -H "Accept: application/vnd.api.v2+json" \
http://api.example.com/api/users/123
六、方案 4:API Gateway 路由(生产推荐)
# Spring Cloud Gateway 配置
spring:
cloud:
gateway:
routes:
# V1 路由 → v1-service
- id: user-service-v1
uri: lb://user-service-v1
predicates:
- Path=/api/users/**
- Header=X-API-VERSION, 1
# V2 路由 → v2-service
- id: user-service-v2
uri: lb://user-service-v2
predicates:
- Path=/api/users/**
- Header=X-API-VERSION, 2
架构图:
Client → API Gateway → V1 Service(独立部署)
↓
V2 Service(独立部署)
七、版本兼容策略
/**
* DTO 演进策略:使用 @JsonIgnore 和默认值保证向后兼容
*/
public class UserDTO {
private Long id;
private String name;
// V2 新增字段,V1 序列化时自动忽略 null
@JsonInclude(JsonInclude.Include.NON_NULL)
private String email; // V1 中为 null
@JsonInclude(JsonInclude.Include.NON_NULL)
private String avatar; // V1 中为 null
// 弃用字段标记
@Deprecated
@JsonIgnore // V2 中不再返回
private String oldField;
}
八、总结
| 场景 | 推荐方案 |
|---|---|
| 小型项目,快速迭代 | URL Path /api/v1/ |
| 大型微服务,独立部署 | API Gateway 路由 |
| RESTful 纯化要求 | Content-Type 协商 |
| 内部 API,版本少 | Header X-API-VERSION |
更多推荐



所有评论(0)