⏰ 接口版本管理:API的时光机

55 阅读8分钟

知识点编号:271
难度系数:⭐⭐⭐⭐
实用指数:💯💯💯💯💯


📖 开篇:一次API升级引发的血案

某天,你发布了新版本API:

// v1版本(旧)
@GetMapping("/api/user/info")
public User getUserInfo() {
    return User.builder()
        .name("张三")
        .age(25)
        .build();
}

// v2版本(新)- 字段名改了
@GetMapping("/api/user/info")
public User getUserInfo() {
    return User.builder()
        .username("张三")  // name → username
        .age(25)
        .gender("male")   // 新增字段
        .build();
}

上线后,客户服务电话被打爆 📞:

iOS用户:"App闪退了!😭"
Android用户:"个人信息显示不出来!😡"
Web用户:"页面报错了!🤬"

你一查日志:

// 客户端代码
const name = response.name;  // undefined(因为改成username了)
console.log(name.toUpperCase());  // 💥 Cannot read property 'toUpperCase' of undefined

产品经理冲过来:

"小王!你没做兼容吗?老版本APP怎么办?😱"

你:

"啊?我以为大家都会更新APP... 😭"

这就是为什么需要API版本管理! 🎯


🎯 为什么需要接口版本管理?

1. 向后兼容 🔄

旧版APP(v1.0)  →  调用 /api/v1/user
新版APP(v2.0)  →  调用 /api/v2/user

两个版本同时存在,互不影响!

2. 平滑升级 📱

用户不需要立即更新APP
给客户端充足的升级时间
避免强制更新引起的用户流失

3. A/B测试 🧪

部分用户使用新接口(v2)
其他用户使用旧接口(v1)
对比两个版本的数据效果

4. 多端适配 📲

iOS:v2.1
Android:v2.0
Web:v1.8
小程序:v1.5

每个平台可能版本不同!

🎨 API版本管理的四种方式

┌──────────────────────────────────────────────────────────┐
│              API版本管理方案                               │
└──────────────────────────────────────────────────────────┘

方案1: URL路径版本
  /api/v1/users
  /api/v2/users
  ✅ 最常用 ⭐⭐⭐⭐⭐

方案2: 请求头版本
  GET /api/users
  Headers: API-Version: 2
  ✅ RESTful风格

方案3: 参数版本
  /api/users?version=2
  ⚠️ 不推荐

方案4: 域名版本
  v1.api.example.com
  v2.api.example.com
  ✅ 适合大型项目

💻 方案一:URL路径版本(推荐⭐⭐⭐⭐⭐)

1. 基础实现

// v1版本
@RestController
@RequestMapping("/api/v1/user")
public class UserV1Controller {
    
    @GetMapping("/info")
    public UserV1DTO getUserInfo() {
        return UserV1DTO.builder()
            .name("张三")
            .age(25)
            .build();
    }
}

// v2版本
@RestController
@RequestMapping("/api/v2/user")
public class UserV2Controller {
    
    @GetMapping("/info")
    public UserV2DTO getUserInfo() {
        return UserV2DTO.builder()
            .username("张三")  // 字段名改变
            .age(25)
            .gender("male")   // 新增字段
            .build();
    }
}

2. DTO版本管理

// v1版本DTO
@Data
@Builder
public class UserV1DTO {
    private String name;
    private Integer age;
}

// v2版本DTO
@Data
@Builder
public class UserV2DTO {
    private String username;  // name → username
    private Integer age;
    private String gender;    // 新增字段
    private String avatar;    // 新增字段
}

// 转换器
@Component
public class UserDTOConverter {
    
    public UserV1DTO toV1(User user) {
        return UserV1DTO.builder()
            .name(user.getUsername())
            .age(user.getAge())
            .build();
    }
    
    public UserV2DTO toV2(User user) {
        return UserV2DTO.builder()
            .username(user.getUsername())
            .age(user.getAge())
            .gender(user.getGender())
            .avatar(user.getAvatar())
            .build();
    }
}

3. 统一版本前缀

@Configuration
public class ApiVersionConfig {
    
    @Value("${api.version.default:v1}")
    private String defaultVersion;
    
    @Bean
    public WebMvcConfigurer versionConfigurer() {
        return new WebMvcConfigurer() {
            @Override
            public void configurePathMatch(PathMatchConfigurer configurer) {
                // 统一添加版本前缀
                configurer.addPathPrefix("/api/{version}", 
                    c -> c.isAnnotationPresent(RestController.class));
            }
        };
    }
}

// 使用
@RestController
@RequestMapping("/user")
public class UserController {
    
    @GetMapping("/info")
    public Object getUserInfo(@PathVariable String version) {
        if ("v1".equals(version)) {
            return getUserV1();
        } else if ("v2".equals(version)) {
            return getUserV2();
        }
        
        throw new ApiVersionException("不支持的版本");
    }
}

🎯 方案二:请求头版本(RESTful风格)

1. 自定义注解

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiVersion {
    int value();  // 版本号
}

// 使用
@RestController
@RequestMapping("/api/user")
public class UserController {
    
    @GetMapping("/info")
    @ApiVersion(1)
    public UserV1DTO getUserInfoV1() {
        return new UserV1DTO();
    }
    
    @GetMapping("/info")
    @ApiVersion(2)
    public UserV2DTO getUserInfoV2() {
        return new UserV2DTO();
    }
}

2. 版本拦截器

@Component
public class ApiVersionInterceptor implements HandlerInterceptor {
    
    @Override
    public boolean preHandle(HttpServletRequest request, 
                            HttpServletResponse response, 
                            Object handler) {
        
        if (!(handler instanceof HandlerMethod)) {
            return true;
        }
        
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        
        // 获取方法上的版本注解
        ApiVersion apiVersion = handlerMethod.getMethodAnnotation(ApiVersion.class);
        if (apiVersion == null) {
            // 获取类上的版本注解
            apiVersion = handlerMethod.getBeanType().getAnnotation(ApiVersion.class);
        }
        
        if (apiVersion == null) {
            return true;  // 没有版本要求
        }
        
        // 从请求头获取版本
        String requestVersion = request.getHeader("API-Version");
        if (requestVersion == null) {
            requestVersion = "1";  // 默认v1
        }
        
        int version = Integer.parseInt(requestVersion);
        
        // 校验版本
        if (version != apiVersion.value()) {
            response.setStatus(HttpStatus.NOT_ACCEPTABLE.value());
            return false;
        }
        
        return true;
    }
}

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
    
    @Autowired
    private ApiVersionInterceptor apiVersionInterceptor;
    
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(apiVersionInterceptor);
    }
}

3. 前端调用

// JavaScript
fetch('/api/user/info', {
    headers: {
        'API-Version': '2'  // 指定版本
    }
})

// Axios
axios.get('/api/user/info', {
    headers: {
        'API-Version': '2'
    }
})

🚀 高级特性

1. 版本废弃提示

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Deprecated {
    String since();      // 从哪个版本开始废弃
    String forRemoval(); // 哪个版本会删除
    String message();    // 提示信息
}

@RestController
@RequestMapping("/api/v1/user")
public class UserV1Controller {
    
    @GetMapping("/info")
    @Deprecated(
        since = "v2.0",
        forRemoval = "v3.0",
        message = "请使用 /api/v2/user/info"
    )
    public UserV1DTO getUserInfo() {
        return new UserV1DTO();
    }
}

// 拦截器
@Component
public class DeprecatedInterceptor implements HandlerInterceptor {
    
    @Override
    public boolean preHandle(HttpServletRequest request, 
                            HttpServletResponse response, 
                            Object handler) {
        
        if (handler instanceof HandlerMethod) {
            HandlerMethod hm = (HandlerMethod) handler;
            Deprecated deprecated = hm.getMethodAnnotation(Deprecated.class);
            
            if (deprecated != null) {
                // 添加响应头提示
                response.setHeader("X-API-Deprecated", "true");
                response.setHeader("X-API-Deprecated-Since", deprecated.since());
                response.setHeader("X-API-Deprecated-Message", deprecated.message());
                
                log.warn("调用了废弃接口:{} - {}", 
                    request.getRequestURI(), 
                    deprecated.message()
                );
            }
        }
        
        return true;
    }
}

2. 版本自动路由

@Component
public class ApiVersionRouter {
    
    private Map<String, Method> versionMethodMap = new HashMap<>();
    
    /**
     * 根据版本号自动路由到对应方法
     */
    public Object route(String apiPath, int version, Object... args) {
        String key = apiPath + "_v" + version;
        Method method = versionMethodMap.get(key);
        
        if (method == null) {
            // 找不到对应版本,尝试找最近的旧版本
            method = findNearestVersion(apiPath, version);
        }
        
        if (method == null) {
            throw new ApiVersionException("不支持的版本:v" + version);
        }
        
        try {
            return method.invoke(null, args);
        } catch (Exception e) {
            throw new RuntimeException("调用失败", e);
        }
    }
    
    /**
     * 找到最近的旧版本
     */
    private Method findNearestVersion(String apiPath, int targetVersion) {
        for (int v = targetVersion - 1; v >= 1; v--) {
            String key = apiPath + "_v" + v;
            Method method = versionMethodMap.get(key);
            if (method != null) {
                log.info("版本{}不存在,降级到版本{}", targetVersion, v);
                return method;
            }
        }
        return null;
    }
}

3. 版本兼容层

@Service
public class UserServiceAdapter {
    
    @Autowired
    private UserService userService;
    
    /**
     * v1接口适配(兼容层)
     */
    public UserV1DTO getUserV1(Long userId) {
        User user = userService.getById(userId);
        
        // 转换为v1格式
        return UserV1DTO.builder()
            .name(user.getUsername())  // username → name
            .age(user.getAge())
            .build();
    }
    
    /**
     * v2接口(原生)
     */
    public UserV2DTO getUserV2(Long userId) {
        User user = userService.getById(userId);
        
        return UserV2DTO.builder()
            .username(user.getUsername())
            .age(user.getAge())
            .gender(user.getGender())
            .avatar(user.getAvatar())
            .build();
    }
}

📱 客户端版本管理

1. 客户端版本检测

@RestController
@RequestMapping("/api/version")
public class VersionController {
    
    /**
     * 检查客户端版本
     */
    @GetMapping("/check")
    public VersionCheckResult check(@RequestParam String platform,
                                   @RequestParam String version) {
        
        // 获取最新版本
        AppVersion latest = versionService.getLatestVersion(platform);
        
        // 比较版本
        int compare = VersionUtil.compare(version, latest.getVersion());
        
        if (compare < 0) {
            // 当前版本低于最新版本
            
            // 检查是否强制更新
            if (versionService.isForceUpdate(platform, version)) {
                return VersionCheckResult.builder()
                    .needUpdate(true)
                    .forceUpdate(true)
                    .message("发现新版本,请立即更新")
                    .downloadUrl(latest.getDownloadUrl())
                    .build();
            } else {
                return VersionCheckResult.builder()
                    .needUpdate(true)
                    .forceUpdate(false)
                    .message("发现新版本,建议更新")
                    .downloadUrl(latest.getDownloadUrl())
                    .build();
            }
        }
        
        return VersionCheckResult.builder()
            .needUpdate(false)
            .message("已是最新版本")
            .build();
    }
}

@Data
@Builder
public class VersionCheckResult {
    private Boolean needUpdate;    // 是否需要更新
    private Boolean forceUpdate;   // 是否强制更新
    private String message;        // 提示信息
    private String downloadUrl;    // 下载地址
}

2. API版本匹配

@Component
public class ApiVersionMatcher {
    
    // 客户端版本 → API版本映射
    private static final Map<String, String> VERSION_MAP = new HashMap<>();
    
    static {
        VERSION_MAP.put("1.0.0", "v1");
        VERSION_MAP.put("1.1.0", "v1");
        VERSION_MAP.put("2.0.0", "v2");
        VERSION_MAP.put("2.1.0", "v2");
        VERSION_MAP.put("3.0.0", "v3");
    }
    
    /**
     * 根据客户端版本获取API版本
     */
    public String getApiVersion(String clientVersion) {
        String apiVersion = VERSION_MAP.get(clientVersion);
        
        if (apiVersion == null) {
            // 找最近的版本
            apiVersion = findNearestApiVersion(clientVersion);
        }
        
        return apiVersion != null ? apiVersion : "v1";  // 默认v1
    }
    
    private String findNearestApiVersion(String clientVersion) {
        // 简化实现:找小于等于当前版本的最大版本
        return VERSION_MAP.entrySet().stream()
            .filter(e -> VersionUtil.compare(e.getKey(), clientVersion) <= 0)
            .max(Map.Entry.comparingByKey())
            .map(Map.Entry::getValue)
            .orElse(null);
    }
}

📊 版本监控与统计

1. 版本使用统计

@Aspect
@Component
public class ApiVersionMonitor {
    
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    @Around("@annotation(apiVersion)")
    public Object monitor(ProceedingJoinPoint pjp, ApiVersion apiVersion) throws Throwable {
        String version = "v" + apiVersion.value();
        String api = pjp.getSignature().toShortString();
        
        // 统计调用次数
        String key = "api:version:count:" + version + ":" + api;
        redisTemplate.opsForValue().increment(key);
        
        // 记录最后调用时间
        String timeKey = "api:version:lastcall:" + version + ":" + api;
        redisTemplate.opsForValue().set(timeKey, 
            LocalDateTime.now().toString()
        );
        
        return pjp.proceed();
    }
}

@Service
public class VersionStatService {
    
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    /**
     * 获取版本使用报告
     */
    public Map<String, Long> getVersionReport() {
        Map<String, Long> report = new HashMap<>();
        
        Set<String> keys = redisTemplate.keys("api:version:count:*");
        if (keys != null) {
            for (String key : keys) {
                String count = redisTemplate.opsForValue().get(key);
                report.put(key, Long.parseLong(count));
            }
        }
        
        return report;
    }
    
    /**
     * 检查是否可以下线某个版本
     */
    public boolean canRetireVersion(String version) {
        // 检查过去30天的调用量
        Set<String> keys = redisTemplate.keys("api:version:count:" + version + ":*");
        
        if (keys == null || keys.isEmpty()) {
            return true;  // 没有调用记录,可以下线
        }
        
        long totalCalls = 0;
        for (String key : keys) {
            String count = redisTemplate.opsForValue().get(key);
            totalCalls += Long.parseLong(count);
        }
        
        // 调用量 < 100次,可以考虑下线
        return totalCalls < 100;
    }
}

2. 版本报表

@RestController
@RequestMapping("/admin/version")
public class VersionReportController {
    
    @Autowired
    private VersionStatService statService;
    
    /**
     * 版本使用报表
     */
    @GetMapping("/report")
    public Result<Map<String, Object>> getReport() {
        Map<String, Object> report = new HashMap<>();
        
        // 1. 各版本调用次数
        Map<String, Long> versionCount = statService.getVersionReport();
        report.put("versionCount", versionCount);
        
        // 2. 可下线的版本
        List<String> retirableVersions = new ArrayList<>();
        for (String version : Arrays.asList("v1", "v2", "v3")) {
            if (statService.canRetireVersion(version)) {
                retirableVersions.add(version);
            }
        }
        report.put("retirableVersions", retirableVersions);
        
        // 3. 最后调用时间
        // ...
        
        return Result.success(report);
    }
}

🎓 最佳实践

1. 版本命名规范

✅ 推荐:
/api/v1/users
/api/v2/users
/api/v3/users

❌ 不推荐:
/api/1.0/users       // 小版本号通常不放URL
/api/2023/users      // 用年份不清晰
/api/v1.2.3/users    // 太细粒度

2. 版本生命周期

┌──────────────────────────────────────────────────────────┐
│              API版本生命周期                               │
└──────────────────────────────────────────────────────────┘

阶段1: 开发中(Development)
  - 频繁变动
  - 不对外开放

阶段2: 测试中(Beta)
  - 小范围测试
  - 可能有Breaking Changes

阶段3: 稳定版(Stable)
  - 正式对外
  - 保证向后兼容

阶段4: 维护期(Maintenance)
  - 只修Bug
  - 不增加新功能

阶段5: 废弃期(Deprecated)
  - 标记为废弃
  - 给出替代方案
  - 保持可用(6-12个月)

阶段6: 下线(Retired)
  - 彻底移除
  - 返回410 Gone

3. 版本策略

@Configuration
public class VersionStrategy {
    
    /**
     * 版本配置
     */
    @Bean
    public VersionConfig versionConfig() {
        VersionConfig config = new VersionConfig();
        
        // 当前稳定版本
        config.setStableVersion("v2");
        
        // 最低支持版本
        config.setMinSupportedVersion("v1");
        
        // 即将废弃的版本
        config.setDeprecatedVersions(Arrays.asList("v1"));
        
        // 废弃期(月)
        config.setDeprecationPeriod(12);
        
        return config;
    }
}

⚠️ 常见坑

1. 不要过度版本化

// ❌ 错误:每个小改动都升级版本
/api/v1/user
/api/v1.1/user
/api/v1.1.1/user
/api/v1.1.2/user

// ✅ 正确:只在Breaking Changes时升级主版本
/api/v1/user   // 1.0 - 1.9都用这个
/api/v2/user   // 2.0开始用这个

2. 不要过早下线旧版本

@Service
public class VersionRetireService {
    
    /**
     * 下线前检查
     */
    public boolean canRetire(String version) {
        // 1. 调用量检查
        if (!isLowTraffic(version)) {
            log.warn("版本{}调用量仍然很高,不建议下线", version);
            return false;
        }
        
        // 2. 时间检查(至少废弃6个月)
        if (!isDeprecatedLongEnough(version)) {
            log.warn("版本{}废弃时间不足6个月", version);
            return false;
        }
        
        // 3. 客户端版本检查
        if (hasOldClients(version)) {
            log.warn("仍有大量客户端使用版本{}", version);
            return false;
        }
        
        return true;
    }
}

📝 总结

方案对比

方案优点缺点推荐度
URL路径直观、简单URL变长⭐⭐⭐⭐⭐
请求头RESTful不直观⭐⭐⭐⭐
参数灵活容易忘记⭐⭐
域名彻底隔离部署复杂⭐⭐⭐

关键要点 🎯

  1. 选择URL路径版本 - 最简单直观
  2. 语义化版本号 - v1, v2, v3
  3. 保持向后兼容 - 旧版本保持可用
  4. 废弃提示 - 给客户端升级时间
  5. 监控统计 - 了解版本使用情况

让你的API优雅地进化! 🎉🎉🎉