知识点编号: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 | 不直观 | ⭐⭐⭐⭐ |
| 参数 | 灵活 | 容易忘记 | ⭐⭐ |
| 域名 | 彻底隔离 | 部署复杂 | ⭐⭐⭐ |
关键要点 🎯
- 选择URL路径版本 - 最简单直观
- 语义化版本号 - v1, v2, v3
- 保持向后兼容 - 旧版本保持可用
- 废弃提示 - 给客户端升级时间
- 监控统计 - 了解版本使用情况
让你的API优雅地进化! 🎉🎉🎉