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

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐