一个接口从 v1 升级到 v2,最麻烦的通常不是多写一个 Controller,而是旧客户端如何继续使用、服务端如何正确路由、何时提示废弃,以及测试如何避免两个版本互相污染。
Spring Framework 7 在 Spring MVC 和 WebFlux 中提供了原生 API Versioning 支持。本文以商品查询接口为例,介绍请求头版本解析、映射、默认版本、客户端兼容性和迁移策略。它是 Spring Framework 7 系列已有能力,并非 2026 年 11 月 9 日当天新发布的功能。
技术栈:Java 17+、Spring Framework 7.0、Spring Boot 4.x、Spring MVC;以下代码是参考官方 API 的设计示例,未在独立工程编译执行。
一、为什么传统 URL 版本管理会越来越难维护?
常见设计:
text
GET /api/v1/products/1001
GET /api/v2/products/1001
这并没有错,尤其当 API 面向外部客户且版本长期并存时,URL 前缀清晰易懂。
但随着接口数量增长,可能出现:
- 每个 Controller 都要复制
/v1、/v2路径。 - 路由版本、接口契约版本和文档版本不同步。
- 无法集中处理不支持的版本值。
- 客户端没有统一的版本传递规则。
- 废弃接口只能靠文档通知,缺少标准化响应提示。
Spring 7 的原生版本管理解决的是请求版本解析与路由的基础设施问题,不是自动生成版本迁移代码。
二、Spring Framework 7 提供了什么?
根据 Spring Framework 7 官方参考文档,API 版本体系围绕几个概念:
| 组件 | 主要职责 |
|---|---|
ApiVersionStrategy |
组织解析、比较、验证与废弃提示 |
ApiVersionResolver |
从请求头、参数、路径或媒体类型获取版本 |
ApiVersionParser |
将原始版本值转换为可比较对象 |
ApiVersionConfigurer |
在 MVC 配置中声明版本解析规则 |
@RequestMapping(version=...) |
将版本与具体请求映射关联 |
框架支持请求头、查询参数、路径片段和媒体类型参数等方式。不是所有方式都要一起开启;对于一个 API,最好有清晰且唯一的客户端契约。
本文选择请求头:
http
GET /api/products/1001 HTTP/1.1
Host: api.example.test
API-Version: 2.0
相比 URL 前缀,它保持路径稳定;缺点是浏览器直接输入 URL 不方便观察版本,需要在客户端和网关层传递请求头。
三、最小配置:从请求头读取 API-Version
java
@Configuration
public class ApiVersionWebConfig implements WebMvcConfigurer {
@Override
public void configureApiVersioning(
ApiVersionConfigurer configurer) {
configurer.useRequestHeader("API-Version");
}
}
这里使用的是 Spring Framework 7 的 WebMvcConfigurer 扩展点。旧的 Spring Framework 6.x 项目不能直接复制后期待这些 API 存在。
实际升级应使用匹配的 Spring Boot 4.x 依赖管理,不要手动把单个 spring-webmvc 依赖强行升级到 7.x 而保持其他 Spring 组件在 6.x。
请求头版本缺失怎么办?
这是产品契约选择,不是一个可以忽略的小问题:
- 新 API 可以要求客户端显式传版本。
- 已有老客户端可能从未发送版本头。
- 若直接启用强制版本校验,旧客户端可能立即失败。
- 如果需要默认版本,应在配置中明确指定,并有退场计划。
上线前必须通过真实请求确认缺失版本、非法版本和不支持版本的 HTTP 响应,不应假定所有场景都自动落到 v1。
四、同一个路径如何提供两个版本?
定义两个 DTO,避免在旧版本上直接新增破坏性字段或改变字段语义。
java
public record ProductV1(
long id,
String name,
BigDecimal price
) {}
public record ProductV2(
long id,
String displayName,
Money price,
boolean available
) {}
public record Money(
BigDecimal amount,
String currency
) {}
Controller 使用版本化映射:
java
@RestController
@RequestMapping("/api/products")
public class ProductController {
@GetMapping(path = "/{id}", version = "1.0")
public ProductV1 getV1(@PathVariable long id) {
return new ProductV1(
id, "机械键盘", new BigDecimal("199.00"));
}
@GetMapping(path = "/{id}", version = "2.0")
public ProductV2 getV2(@PathVariable long id) {
return new ProductV2(
id,
"机械键盘",
new Money(new BigDecimal("199.00"), "CNY"),
true);
}
}
这段示例用固定数据演示响应结构,不是生产商品查询实现。真实项目应将数据读取交给 Service,再由不同版本的 Mapper 转换成对应 DTO。
这种设计的关键不是重复业务逻辑,而是让对外契约独立演进。
五、版本号不等于应用发布版本
接口版本 2.0 不必等于 Spring Boot 版本、Git tag 或应用构建版本。
例如:
text
应用版本:2026.11.09-1
Spring Boot:4.x
商品 API:1.0、2.0
订单 API:1.0
这几个数字描述的是不同维度。
推荐把 API 版本变化与契约破坏性关联,而不是每次服务上线就增加 API 版本。
哪些变化可能需要新版本?
- 删除旧字段或改变字段名称。
- 字段类型由字符串变为对象。
- 修改字段含义或金额单位。
- 将可选字段改为必填。
- 改变错误码、分页语义或排序规则。
- 将原来同步成功的操作改成异步接受且客户端必须适配。
新增可选字段通常可以保持向后兼容,但仍需考虑严格 JSON 解析客户端、生成 SDK 和缓存行为。
六、服务层如何避免 v1/v2 两套业务逻辑?
一个可维护的结构是:
text
ProductController
├── GET v1 -> ProductV1Mapper
└── GET v2 -> ProductV2Mapper
|
ProductService
|
ProductRepository
两套版本的响应映射可以不同,但权限、价格规则、库存查询应共享业务服务。
java
@Service
public class ProductService {
public ProductView findReadableProduct(
long productId, long tenantId) {
// 从可信身份获取 tenantId
// 查询并校验当前租户可访问的商品
throw new UnsupportedOperationException(
"请接入真实商品查询实现");
}
}
public record ProductView(
long id,
String name,
BigDecimal price,
String currency,
boolean available
) {}
这里使用 UnsupportedOperationException 明确标识待实现依赖,避免把伪代码当作已经运行的生产逻辑。
不要让 v1 Controller 绕过鉴权、v2 Controller 才校验权限。版本差异不应造成安全策略差异。
七、默认版本与旧客户端迁移
假设旧客户端没有 API-Version 请求头。直接切换到版本化路由之前,应先盘点调用方:
- 浏览器前端和移动端版本分布。
- 第三方合作伙伴及是否可以同步升级。
- 网关是否保留自定义请求头。
- 缓存是否根据版本区分响应。
- SDK 是否自动注入版本信息。
可以选择"兼容期默认 v1 → 客户端逐步显式传版本 → 监控旧流量 → 取消默认"的路线。
但是否以及如何配置默认版本,应以所用 Spring Framework 7.0.x 的具体配置 API为准;不要从 7.1 SNAPSHOT 文档复制未经验证的选项。
网关和 CDN 缓存尤其容易出错
如果 GET /api/products/1001 在不同 API-Version 下返回不同 JSON,而共享缓存只按 URL 作为键,就可能把 v2 内容返回给 v1 客户端。
应确保缓存键包含版本信息,或按实际缓存架构正确使用 Vary 等机制。
http
Vary: API-Version
注意 Vary 主要约束遵守 HTTP 缓存语义的缓存;自研 Redis 缓存仍需要自己把版本加入 key。
八、接口废弃:先通知,再停止支持
版本管理不仅是"能路由",还包括生命周期治理。
Spring Framework 7 的版本策略支持响应中的废弃提示。相关行为应对照目标版本文档与 RFC 9745 校验。
实际对外沟通建议包含:
- 哪个 API 版本进入废弃期。
- 新版本的迁移文档地址。
- 最后支持日期与时区。
- 字段级差异与错误码差异。
- 客户端如何验证迁移成功。
- 遇到问题如何回滚。
不要将 Deprecation 提示等同于立即下线。 很多调用方需要一个完整的升级周期。
九、如何测试版本路由?
可以使用 MockMvc 构造不同版本请求:
java
@WebMvcTest(ProductController.class)
class ProductVersionControllerTests {
@Autowired
MockMvc mockMvc;
@Test
void v1ShouldUseLegacyShape() throws Exception {
mockMvc.perform(get("/api/products/1001")
.header("API-Version", "1.0"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name").value("机械键盘"))
.andExpect(jsonPath("$.displayName").doesNotExist());
}
@Test
void v2ShouldUseNewShape() throws Exception {
mockMvc.perform(get("/api/products/1001")
.header("API-Version", "2.0"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.price.currency").value("CNY"));
}
}
这只是测试思路:@WebMvcTest 切片是否加载自定义 WebMvcConfigurer 需要确认。必要时显式 @Import(ApiVersionWebConfig.class),或改用完整 @SpringBootTest + @AutoConfigureMockMvc。
测试还必须覆盖:
- 不传版本头的行为。
- 版本值格式错误。
- 请求不支持的版本。
- 已废弃版本的响应头。
- 同一路径不同版本的缓存隔离。
- 认证失败和跨租户访问。
- 老客户端序列化/反序列化兼容性。
十、与 HTTP Interface、RestClient 如何协同?
Spring Framework 7 的 API 版本支持不仅用于服务端。官方文档还说明 RestClient、WebClient 和 HTTP Service Client 具备相应客户端版本支持。
但"客户端支持 API 版本"不意味着自动知道服务端当前的契约版本。调用方仍需要明确配置所需版本,避免请求随服务端升级而悄悄改变语义。
如果已有 HTTP Interface 客户端,可以把"版本策略"集中在客户端配置中,而不是在每个业务方法里随意拼接请求头。
本文不重复讲解 HTTP Interface 代理创建,相关内容与此前专题互补:这篇重点是服务端版本契约与迁移治理。
十一、上线前的兼容性检查表
| 检查项 | 目的 |
|---|---|
| 所有请求都有确定版本语义 | 避免落入错误 Controller |
| 网关透传 API-Version | 避免版本头丢失 |
| 缓存按版本隔离 | 避免响应串版本 |
| DTO 不复用破坏性字段 | 避免旧客户端反序列化失败 |
| 鉴权规则跨版本一致 | 避免新旧版本权限漏洞 |
| 文档和 SDK 与版本一致 | 避免调用方误用 |
| 旧版本流量可观测 | 为下线决策提供证据 |
| 回滚后仍能处理旧版本 | 避免部署回滚引发兼容事故 |
建议先在测试环境对真实旧客户端回放契约测试,再逐步放量。本文没有进行压测或真实流量回放,因此不提供未经验证的性能结论。
十二、常见问题
所有接口都要从 v1 开始吗?
不一定。新系统可以先定义清晰的兼容策略;如果接口只有一个稳定契约,过早拆分版本也会增加维护成本。
用 URL 路径版本化是否已经过时?
没有。路径版本化仍然直观,尤其适合公开 API。Spring 7 只是提供统一的版本解析与映射能力,不强制使用请求头。
Spring Boot 3.x 能直接使用这些注解吗?
不能假设。Spring Framework 7 是 Spring Boot 4 的基础,Spring Boot 3.x 通常基于 Spring Framework 6.x;迁移需要成套评估。
v2 发布后能立即删除 v1 吗?
不建议。先监控调用量,明确合作方升级计划和下线窗口,再进行受控删除。
总结
Spring Framework 7 的 API Versioning 让版本解析和路由成为框架级能力,但真正决定系统是否容易升级的,仍是契约边界、客户端兼容、缓存隔离、废弃通知和自动化测试。
对于长期维护的 SaaS 或开放 API,建议先明确版本策略,再选择 URL、Header 或其他版本来源;不要把版本号当作一次普通的 Controller 重构。