Spring Boot 4 里给 API 版本控制设默认版本,配置键是 spring.mvc.apiversion.default,写成 spring.mvc.apiversion.default-version 不报错也不生效。这是我在 Boot 4.1.1(Framework 7.0.9)上实测的结果,6 个测试全部通过,curl 的对比输出放在后文。
Spring Framework 7 把 API 版本控制做进了 Spring MVC。在 @GetMapping 上写 version,再用 spring.mvc.apiversion.* 属性选好从请求头、查询参数还是路径段取版本,同一个接口的两个版本就能并存。下面把三种取法、缺版本号和版本号不支持的响应、RestClient 和 RestTestClient 怎么带版本都过一遍。
之前那篇介绍 Spring Boot 4.0 API 版本功能的文章写在快照阶段,讲的是 ApiVersionConfigurer 的几种写法。这篇换成 GA 版,看实际请求和响应。
接口写两个版本
@RequestMapping 及各个变体都多了一个 version 属性。同一路径写两个方法,各管一个版本:
@RestController
@Profile("!path")
public class BookController {
@GetMapping(path = "/books/{id}", version = "1.0")
BookV1 bookV1(@PathVariable String id) {
return new BookV1(id, "Spring in Practice");
}
@GetMapping(path = "/books/{id}", version = "2.0")
BookV2 bookV2(@PathVariable String id) {
return new BookV2(id, "Spring in Practice", List.of("Alice", "Bob"));
}
}
v1 只返回 id 和书名,v2 多一个作者列表。@Profile 是为了在一个项目里切换几种取版本的方式,路径段那种要换一套映射,后面会说。
用属性选取版本的方式
Boot 4.1.1 的 WebMvcProperties 里有个 Apiversion 内部类,从 jar 里的配置元数据能列出全部键:
spring.mvc.apiversion.default
spring.mvc.apiversion.detect-supported
spring.mvc.apiversion.required
spring.mvc.apiversion.supported
spring.mvc.apiversion.use.header
spring.mvc.apiversion.use.media-type-parameter
spring.mvc.apiversion.use.path-segment
spring.mvc.apiversion.use.query-parameter
三种方式各用一个 profile 配置文件:
# application-header.yaml
spring:
mvc:
apiversion:
use:
header: X-API-Version
# application-query.yaml
spring:
mvc:
apiversion:
use:
query-parameter: version
default: "1.0"
# application-path.yaml
spring:
mvc:
apiversion:
use:
path-segment: 0
查询参数那套顺便配了默认版本。另外 application.yaml 里开了 spring.web.error.include-message: always,下面的错误响应里才看得到 message。Boot 4 默认不写这一项,旧的 server.error.include-message 在 Boot 4 里已经不生效,RestTestClient 那篇里实测过。
路径段方式要求版本是路径的一部分,所以映射得把这一段留出来。索引从 0 开始,/v1/books/42 的第 0 段就是 v1:
@RestController
@Profile("path")
public class PathBookController {
@GetMapping(path = "/{version}/books/{id}", version = "1.0")
BookV1 bookV1(@PathVariable String id) {
return new BookV1(id, "Spring in Practice");
}
@GetMapping(path = "/{version}/books/{id}", version = "2.0")
BookV2 bookV2(@PathVariable String id) {
return new BookV2(id, "Spring in Practice", List.of("Alice", "Bob"));
}
}
不想写属性的话,Java 配置也行,效果和 header 那套加默认版本一样:
@Configuration
@Profile("configurer")
public class ConfigurerVersioning implements WebMvcConfigurer {
@Override
public void configureApiVersioning(ApiVersionConfigurer configurer) {
configurer.useRequestHeader("X-API-Version")
.setDefaultVersion("1.0");
}
}
curl 实测
打成 jar,用不同 profile 和端口同时起了五个实例。8081 是请求头,8082 是查询参数加默认版本,8083 是路径段,8084 和 8085 用来对比两个默认版本的写法:
J=target/api-versioning-demo-0.0.1-SNAPSHOT.jar
java -jar $J --spring.profiles.active=header --server.port=8081 > logs/app-header.log 2>&1 &
java -jar $J --spring.profiles.active=query --server.port=8082 > logs/app-query.log 2>&1 &
java -jar $J --spring.profiles.active=path --server.port=8083 > logs/app-path.log 2>&1 &
java -jar $J --spring.profiles.active=header --server.port=8084 --spring.mvc.apiversion.default-version=1.0 > logs/app-header-default-version.log 2>&1 &
java -jar $J --spring.profiles.active=header --server.port=8085 --spring.mvc.apiversion.default=1.0 > logs/app-header-default.log 2>&1 &
先看请求头方式:
$ curl -i -H 'X-API-Version: 1.0' http://localhost:8081/books/42
HTTP/1.1 200
Content-Type: application/json
{"id":"42","title":"Spring in Practice"}
$ curl -i -H 'X-API-Version: 2' http://localhost:8081/books/42
HTTP/1.1 200
Content-Type: application/json
{"id":"42","title":"Spring in Practice","authors":["Alice","Bob"]}
请求头里只写 2 也能命中 2.0。框架默认用语义化版本解析,次版本号和修订号没写就当 0。
不带版本号,和带一个没声明过的版本号,都是 400:
$ curl -i http://localhost:8081/books/42
HTTP/1.1 400
Content-Type: application/json
{"timestamp":"2026-10-13T02:01:06.407Z","status":400,"error":"Bad Request","message":"API version is required.","path":"/books/42"}
$ curl -i -H 'X-API-Version: 3.0' http://localhost:8081/books/42
HTTP/1.1 400
Content-Type: application/json
{"timestamp":"2026-10-13T02:01:06.417Z","status":400,"error":"Bad Request","message":"Invalid API version: '3.0.0'.","path":"/books/42"}
报错里的版本号是解析后的 3.0.0。支持哪些版本,默认从控制器上声明的 version 收集,这里只有 1.0 和 2.0。
查询参数这套配了默认版本 1.0,不带参数就落到 v1。1.5 没有声明过,照样 400:
$ curl -i 'http://localhost:8082/books/42?version=2.0'
HTTP/1.1 200
Content-Type: application/json
{"id":"42","title":"Spring in Practice","authors":["Alice","Bob"]}
$ curl -i http://localhost:8082/books/42
HTTP/1.1 200
Content-Type: application/json
{"id":"42","title":"Spring in Practice"}
$ curl -i 'http://localhost:8082/books/42?version=1.5'
HTTP/1.1 400
Content-Type: application/json
{"timestamp":"2026-10-13T02:01:06.483Z","status":400,"error":"Bad Request","message":"Invalid API version: '1.5.0'.","path":"/books/42"}
路径段方式里,v1、v2 这种带 v 前缀的写法能直接解析。少了版本段的路径匹配不上任何映射,最后落到静态资源处理,返回 404:
$ curl -i http://localhost:8083/v2/books/42
HTTP/1.1 200
Content-Type: application/json
{"id":"42","title":"Spring in Practice","authors":["Alice","Bob"]}
$ curl -i http://localhost:8083/v3/books/42
HTTP/1.1 400
Content-Type: application/json
{"timestamp":"2026-10-13T02:01:06.540Z","status":400,"error":"Bad Request","message":"Invalid API version: '3.0.0'.","path":"/v3/books/42"}
$ curl -i http://localhost:8083/books/42
HTTP/1.1 404
Content-Type: application/json
{"timestamp":"2026-10-13T02:01:06.550Z","status":404,"error":"Not Found","message":"No static resource books/42.","path":"/books/42"}
默认版本的键叫 default
Java 字段叫 defaultVersion,配置方法叫 setDefaultVersion,很自然会写成 spring.mvc.apiversion.default-version。但源码里这个字段上标了 @Name("default"),真正的键是 spring.mvc.apiversion.default。写错的键不会报错,启动日志里也没有提示,就是不生效:
### header + spring.mvc.apiversion.default-version=1.0 (8084)
$ curl -i http://localhost:8084/books/42
HTTP/1.1 400
Content-Type: application/json
{"timestamp":"2026-10-13T02:01:06.560Z","status":400,"error":"Bad Request","message":"API version is required.","path":"/books/42"}
### header + spring.mvc.apiversion.default=1.0 (8085)
$ curl -i http://localhost:8085/books/42
HTTP/1.1 200
Content-Type: application/json
{"id":"42","title":"Spring in Practice"}
客户端怎么带版本
Framework 7 的 RestClient 有个 ApiVersionInserter,决定版本放进请求的哪里,和服务端的三种取法一一对应。defaultApiVersion 设默认值,单个请求用 apiVersion(...) 覆盖:
RestClient client = RestClient.builder()
.baseUrl("http://localhost:" + port)
.apiVersionInserter(ApiVersionInserter.useHeader("X-API-Version"))
.defaultApiVersion("1.0")
.build();
BookV1 v1 = client.get().uri("/books/42").retrieve().body(BookV1.class);
BookV2 v2 = client.get().uri("/books/42").apiVersion("2.0").retrieve().body(BookV2.class);
路径段用 usePathSegment(0),请求写 /books/42,客户端会把版本插到第 0 段:
RestClient client = RestClient.builder()
.baseUrl("http://localhost:" + port)
.apiVersionInserter(ApiVersionInserter.usePathSegment(0))
.build();
var response = client.get().uri("/books/42").apiVersion("v2").retrieve().toEntity(BookV2.class);
测试里的 RestTestClient 也是同一套 API。Boot 注入的那个实例没配 inserter,用 mutate() 加上就行:
RestTestClient client = restTestClient.mutate()
.apiVersionInserter(ApiVersionInserter.useHeader("X-API-Version"))
.build();
client.get().uri("/books/42").apiVersion("2.0")
.exchange()
.expectStatus().isOk()
.expectBody().jsonPath("$.authors[0]").isEqualTo("Alice");
缺版本和版本不支持的两种 400 也写进了测试,断言的就是上面 curl 看到的 message:
@Test
void missingAndUnsupportedVersionsAreBadRequests() {
restTestClient.get().uri("/books/42")
.exchange()
.expectStatus().isBadRequest()
.expectBody().jsonPath("$.message").isEqualTo("API version is required.");
RestTestClient client = restTestClient.mutate()
.apiVersionInserter(ApiVersionInserter.useHeader("X-API-Version"))
.build();
client.get().uri("/books/42").apiVersion("3.0")
.exchange()
.expectStatus().isBadRequest()
.expectBody().jsonPath("$.message").isEqualTo("Invalid API version: '3.0.0'.");
}
mvn test 的输出,header、path、query 三个测试类连真端口,configurer 那个跑在 MockMvc 上:
RestClient header v1=BookV1[id=42, title=Spring in Practice] v2=BookV2[id=42, title=Spring in Practice, authors=[Alice, Bob]]
[INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 2.088 s -- in com.example.apiver.HeaderVersionTest
RestClient path v2=BookV2[id=42, title=Spring in Practice, authors=[Alice, Bob]]
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.161 s -- in com.example.apiver.PathVersionTest
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.187 s -- in com.example.apiver.ConfigurerVersionTest
RestClient query v2=BookV2[id=42, title=Spring in Practice, authors=[Alice, Bob]] no-version=BookV1[id=42, title=Spring in Practice]
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.133 s -- in com.example.apiver.QueryVersionTest
[INFO] Tests run: 6, Failures: 0, Errors: 0, Skipped: 0
[INFO] BUILD SUCCESS
环境是 Debian 13、OpenJDK 21.0.12.1、Maven 3.9.9、curl 8.14.1,Spring Boot 4.1.1,Spring Framework 7.0.9。
官方文档:Spring Framework API Versioning、Spring Boot API Versioning。
一起交流
分享你的思考,让讨论更进一步。