Merge branch '4.0.x'

Closes gh-50127
This commit is contained in:
Stéphane Nicoll
2026-04-20 08:21:39 +02:00
6 changed files with 75 additions and 7 deletions
@@ -284,7 +284,7 @@ The same `@Controller` path can be mapped multiple times to support different ve
For more details see {url-spring-framework-docs}/web/webflux/controller/ann-requestmapping.html#webflux-ann-requestmapping-version[Spring Framework's reference documentation].
Once mappings have been added, you additionally need to configure Spring WebFlux so that it is able to use any version information sent with a request.
Typically, versions are sent as HTTP headers, query parameters or as part of the path.
Typically, versions are sent as HTTP headers, query parameters, media type parameters, or as part of the path.
To configure Spring WebFlux, you can either use a javadoc:org.springframework.web.reactive.config.WebFluxConfigurer[] bean and override the `configureApiVersioning(...)` method, or you can use properties.
@@ -300,7 +300,9 @@ spring:
header: X-Version
----
For more complete control, you can also define javadoc:org.springframework.web.reactive.accept.ApiVersionResolver[], javadoc:org.springframework.web.accept.ApiVersionParser[] and javadoc:org.springframework.web.reactive.accept.ApiVersionDeprecationHandler[] beans which will be injected into the auto-configured Spring MVC configuration.
NOTE: If your setup requires multiple strategies, such as header and query parameter, consider declaring the order programmatically by overriding the `configureApiVersioning` method.
For more complete control, you can also define javadoc:org.springframework.web.reactive.accept.ApiVersionResolver[], javadoc:org.springframework.web.accept.ApiVersionParser[] and javadoc:org.springframework.web.reactive.accept.ApiVersionDeprecationHandler[] beans which will be injected into the auto-configured Spring WebFlux configuration.
TIP: API versioning is also supported on the client-side with both `WebClient` and `RestClient`.
See xref:io/rest-client.adoc#io.rest-client.apiversioning[] for details.
@@ -474,8 +474,8 @@ The same `@Controller` path can be mapped multiple times to support different ve
For more details see {url-spring-framework-docs}/web/webmvc/mvc-controller/ann-requestmapping.html#mvc-ann-requestmapping-version[Spring Framework's reference documentation].
One mappings have been added, you additionally need to configure Spring MVC so that it is able to use any version information sent with a request.
Typically, versions are sent as HTTP headers, query parameters or as part of the path.
Once mappings have been added, you additionally need to configure Spring MVC so that it is able to use any version information sent with a request.
Typically, versions are sent as HTTP headers, query parameters, media type parameters, or as part of the path.
To configure Spring MVC, you can either use a javadoc:org.springframework.web.servlet.config.annotation.WebMvcConfigurer[] bean and override the `configureApiVersioning(...)` method, or you can use properties.
@@ -491,6 +491,8 @@ spring:
header: X-Version
----
NOTE: If your setup requires multiple strategies, such as header and query parameter, consider declaring the order programmatically by overriding the `configureApiVersioning` method.
For more complete control, you can also define javadoc:org.springframework.web.accept.ApiVersionResolver[], javadoc:org.springframework.web.accept.ApiVersionParser[] and javadoc:org.springframework.web.accept.ApiVersionDeprecationHandler[] beans which will be injected into the auto-configured Spring MVC configuration.
TIP: API versioning is also supported with both `WebClient` and `RestClient`.
@@ -301,9 +301,8 @@ public final class WebFluxAutoConfiguration {
PropertyMapper map = PropertyMapper.get();
map.from(use::getHeader).whenHasText().to(configurer::useRequestHeader);
map.from(use::getQueryParameter).whenHasText().to(configurer::useQueryParam);
use.getMediaTypeParameter().forEach(configurer::useMediaTypeParameter);
map.from(use::getPathSegment).to(configurer::usePathSegment);
use.getMediaTypeParameter()
.forEach((mediaType, parameterName) -> configurer.useMediaTypeParameter(mediaType, parameterName));
}
}
@@ -909,6 +909,35 @@ class WebFluxAutoConfigurationTests {
});
}
@Test
void apiVersionUsesPathSegmentLast() {
this.contextRunner
.withPropertyValues("spring.webflux.apiversion.use.path-segment=1",
"spring.webflux.apiversion.use.header=hv", "spring.webflux.apiversion.use.query-parameter=rpv",
"spring.webflux.apiversion.use.media-type-parameter[application/json]=mtpv")
.run((context) -> {
DefaultApiVersionStrategy versionStrategy = context.getBean("webFluxApiVersionStrategy",
DefaultApiVersionStrategy.class);
MockServerWebExchange requestWithHeader = MockServerWebExchange
.from(MockServerHttpRequest.get("https://example.com/test/456").header("hv", "123"));
assertThat(versionStrategy.resolveVersion(requestWithHeader)).isEqualTo("123");
MockServerWebExchange requestWithQueryParameter = MockServerWebExchange
.from(MockServerHttpRequest.get("https://example.com?rpv=123"));
assertThat(versionStrategy.resolveVersion(requestWithQueryParameter)).isEqualTo("123");
MockServerWebExchange requestWithMediaType = MockServerWebExchange
.from(MockServerHttpRequest.get("https://example.com/test/456")
.header("content-type", "application/json;mtpv=123"));
assertThat(versionStrategy.resolveVersion(requestWithMediaType)).isEqualTo("123");
MockServerWebExchange requestFallbacksToApiSegment = MockServerWebExchange
.from(MockServerHttpRequest.get("https://example.com/test/456"));
assertThat(versionStrategy.resolveVersion(requestFallbacksToApiSegment)).isEqualTo("456");
});
}
@Test
void apiVersionBeansAreInjected() {
this.contextRunner.withUserConfiguration(ApiVersionConfiguration.class).run((context) -> {
@@ -421,8 +421,8 @@ public final class WebMvcAutoConfiguration {
PropertyMapper map = PropertyMapper.get();
map.from(use::getHeader).whenHasText().to(configurer::useRequestHeader);
map.from(use::getQueryParameter).whenHasText().to(configurer::useQueryParam);
map.from(use::getPathSegment).to(configurer::usePathSegment);
use.getMediaTypeParameter().forEach(configurer::useMediaTypeParameter);
map.from(use::getPathSegment).to(configurer::usePathSegment);
}
@Bean
@@ -1122,6 +1122,42 @@ class WebMvcAutoConfigurationTests {
});
}
@Test
void apiVersionUsesPathSegmentLast() {
this.contextRunner
.withPropertyValues("spring.mvc.apiversion.use.path-segment=1", "spring.mvc.apiversion.use.header=hv",
"spring.mvc.apiversion.use.query-parameter=rpv",
"spring.mvc.apiversion.use.media-type-parameter[application/json]=mtpv")
.run((context) -> {
ApiVersionStrategy versionStrategy = context.getBean("mvcApiVersionStrategy", ApiVersionStrategy.class);
MockHttpServletRequest requestWithHeader = new MockHttpServletRequest("GET",
"https://example.com/test/456");
requestWithHeader.addHeader("hv", "123");
ServletRequestPathUtils.setParsedRequestPath(RequestPath.parse("/test/456", "/"), requestWithHeader);
assertThat(versionStrategy.resolveVersion(requestWithHeader)).isEqualTo("123");
MockHttpServletRequest requestWithQueryParameter = new MockHttpServletRequest("GET",
"https://example.com/test/456");
requestWithQueryParameter.setQueryString("rpv=123");
ServletRequestPathUtils.setParsedRequestPath(RequestPath.parse("/test/456", "/"),
requestWithQueryParameter);
assertThat(versionStrategy.resolveVersion(requestWithQueryParameter)).isEqualTo("123");
MockHttpServletRequest requestWithMediaType = new MockHttpServletRequest("GET",
"https://example.com/test/456");
ServletRequestPathUtils.setParsedRequestPath(RequestPath.parse("/test/456", "/"), requestWithMediaType);
requestWithMediaType.addHeader(HttpHeaders.CONTENT_TYPE, "application/json;mtpv=123");
assertThat(versionStrategy.resolveVersion(requestWithMediaType)).isEqualTo("123");
MockHttpServletRequest requestFallbacksToApiSegment = new MockHttpServletRequest("GET",
"https://example.com/test/456");
ServletRequestPathUtils.setParsedRequestPath(RequestPath.parse("/test/456", "/"),
requestFallbacksToApiSegment);
assertThat(versionStrategy.resolveVersion(requestFallbacksToApiSegment)).isEqualTo("456");
});
}
@Test
void apiVersionBeansAreInjected() {
this.contextRunner.withUserConfiguration(ApiVersionConfiguration.class).run((context) -> {