别再乱搞 API 版本管理了!这些坑你踩过几个?

开发 项目管理
在日常开发中,API版本的迭代几乎是无法避免的事情。最近在项目中,我们遇到了需要在不影响旧客户端的前提下,发布新版接口的需求。

在日常开发中,API版本的迭代几乎是无法避免的事情。最近在项目中,我们遇到了需要在不影响旧客户端的前提下,发布新版接口的需求。

一开始,我也用了最常见的做法:

GET /v1/products
GET /v2/products
  • 1.
  • 2.

乍一看很直观,路径一目了然,新老版本分开,大家都这么干,好像也没毛病。

可惜,这种方式看似简单,实则问题重重。

路径版本化的“表面光鲜”

我们可能会这么设计控制器:

@RestController
@RequestMapping("/v1/products")
public class ProductControllerV1 {
    // V1逻辑
}


@RestController
@RequestMapping("/v2/products")
public class ProductControllerV2 {
    // V2逻辑
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.

这在项目初期确实很“快”。但是随着版本增多,问题接踵而至:

  • 控制器、测试、文档全要复制一份;
  • 每多一个版本,代码重复度、维护成本、测试开销都指数上升;
  • 文档冗余、客户端混乱、接口路径不稳定。

URL版本控制违背了 REST 的设计初衷

根据 RESTful 的核心理念:

URI 是资源的永久地址,应保持稳定性。

路径中包含版本号,就像是给图书的 ISBN 每年都改一次,不仅违背设计初衷,还容易造成客户端的严重耦合。

举个例子:

如果我们每年都改接口路径:

/v1/products  
/v2/products  
/v3/products  
/legacy/products
  • 1.
  • 2.
  • 3.
  • 4.

老系统不能删除,新系统又要兼容,最终的结果是:

  • 控制器臃肿不堪;
  • 老版本无法完全淘汰;
  • 文档极度冗余;
  • 客户端升级困难;
  • 出现 404 时,用户只能摸黑报 bug。

更优雅的做法:基于请求头的版本控制

相比 URL 版本,通过 HTTP Header 来传递版本信息更贴合 REST 设计原则。

接口路径不变:

GET /products
  • 1.

版本通过 Accept 头协商:

  • 请求 V1:
Accept: application/vnd.icoderoad.v1+json
  • 1.
  • 请求 V2:
Accept: application/vnd.icoderoad.v2+json
  • 1.

Spring Boot 3.4 实现方式

项目结构基于 com.icoderoad 包名:

package com.icoderoad.api.controller;


@RestController
@RequiredArgsConstructor
public class ProductController {


    private final VersionProvider versionProvider;
    private final IProductService productService;


    @GetMapping(value = "/products", produces = {
        "application/vnd.icoderoad.v1+json",
        "application/vnd.icoderoad.v2+json"
    })
    public ResponseEntity<List<IProductResponseDto>> getProducts(
            @RequestHeader(value = "Accept", defaultValue = "application/vnd.icoderoad.v1+json") String acceptHeader) {


        Version version = versionProvider.identifyVersion(acceptHeader);
        List<IProductResponseDto> products = productService.getProducts(version);
        return ResponseEntity.ok(products);
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.

VersionProvider 示例:

package com.icoderoad.api.version;


@Component
public class VersionProvider {


    public Version identifyVersion(String acceptHeader) {
        if (acceptHeader.contains("v2")) {
            return Version.V2;
        }
        return Version.V1;
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.

响应 DTO 接口:

public interface IProductResponseDto {
    String getName();
}
  • 1.
  • 2.
  • 3.

不同版本可以实现不同的 DTO,比如 ProductV1DtoProductV2Dto

优点解析

优势

说明

URI 稳定

/products


永远不会改变


向后兼容

多版本共存,只需判断 Header

更适配缓存

请求路径不变,缓存更好命中

遵循 REST 规范

与 Fielding 的 REST 理论保持一致

扩展建议:再往前走一步

增加 Swagger 兼容支持

使用 springdoc-openapi,通过 API Group 实现版本文档拆分展示:

springdoc:
  group-configs:
    - group: v1
      paths-to-match: /products
      produces-to-match: application/vnd.icoderoad.v1+json
    - group: v2
      paths-to-match: /products
      produces-to-match: application/vnd.icoderoad.v2+json
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.

自定义注解 + AOP 做更细粒度的控制

你也可以用注解标识不同版本的方法,再配合 AOP 在运行时动态路由调用,非常优雅。

那 URL 版本化有没有场景?

有!但 仅限以下两种情况

  1. 内部系统或微服务之间通信,版本变动可控
  2. 资源结构发生破坏性变化,无法向后兼容(例如合规要求)

除此之外,请尽量使用 Header 版本控制。

总结

  • URI 是资源的“身份证”,不应频繁变化;
  • 版本控制建议通过 HTTP Header 实现;
  • 教育前端和客户端团队理解 Accept Header 的重要性;
  • 采用统一控制器,降低代码重复,维护成本更低。

如果你还在用 /v1/xxx 的方式管理版本,或许可以思考下换个方式,拥抱更优雅的 REST 实践。

责任编辑:武晓燕 来源: 路条编程
相关推荐

2018-09-11 09:14:52

面试公司缺点

2024-04-01 08:05:27

Go开发Java

2023-03-13 13:36:00

Go扩容切片

2018-08-01 14:45:16

PHP编程语言

2018-01-10 06:17:24

2020-06-18 10:48:44

Linux 系统 数据

2018-07-30 16:18:51

容灾备份

2022-04-26 21:49:55

Spring事务数据库

2017-07-17 15:46:20

Oracle并行机制

2019-12-12 14:32:26

SQL语句数据库

2025-02-10 00:27:54

2018-01-10 13:40:03

数据库MySQL表设计

2021-04-14 17:34:18

线程安全

2015-03-24 16:29:55

默认线程池java

2022-03-16 15:28:17

黑产安全网络

2019-09-25 15:30:15

2024-06-26 10:37:05

2024-05-06 00:00:00

缓存高并发数据

2020-11-03 13:50:31

Redis缓存数据库

2021-05-27 22:46:00

Nacos Clien版本Nacos
点赞
收藏

51CTO技术栈公众号