diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/web/reactive.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/web/reactive.adoc index c8c25c2187a..390f82507da 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/web/reactive.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/web/reactive.adoc @@ -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. diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/web/servlet.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/web/servlet.adoc index d7ba66b2734..5a01011474c 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/web/servlet.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/web/servlet.adoc @@ -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`. diff --git a/module/spring-boot-webflux/src/main/java/org/springframework/boot/webflux/autoconfigure/WebFluxAutoConfiguration.java b/module/spring-boot-webflux/src/main/java/org/springframework/boot/webflux/autoconfigure/WebFluxAutoConfiguration.java index b39129feea4..1546af244b0 100644 --- a/module/spring-boot-webflux/src/main/java/org/springframework/boot/webflux/autoconfigure/WebFluxAutoConfiguration.java +++ b/module/spring-boot-webflux/src/main/java/org/springframework/boot/webflux/autoconfigure/WebFluxAutoConfiguration.java @@ -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)); } } diff --git a/module/spring-boot-webflux/src/test/java/org/springframework/boot/webflux/autoconfigure/WebFluxAutoConfigurationTests.java b/module/spring-boot-webflux/src/test/java/org/springframework/boot/webflux/autoconfigure/WebFluxAutoConfigurationTests.java index 60e2b39b853..e4c2ad3ff1a 100644 --- a/module/spring-boot-webflux/src/test/java/org/springframework/boot/webflux/autoconfigure/WebFluxAutoConfigurationTests.java +++ b/module/spring-boot-webflux/src/test/java/org/springframework/boot/webflux/autoconfigure/WebFluxAutoConfigurationTests.java @@ -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) -> { diff --git a/module/spring-boot-webmvc/src/main/java/org/springframework/boot/webmvc/autoconfigure/WebMvcAutoConfiguration.java b/module/spring-boot-webmvc/src/main/java/org/springframework/boot/webmvc/autoconfigure/WebMvcAutoConfiguration.java index cd00acd2224..7d829e17d76 100644 --- a/module/spring-boot-webmvc/src/main/java/org/springframework/boot/webmvc/autoconfigure/WebMvcAutoConfiguration.java +++ b/module/spring-boot-webmvc/src/main/java/org/springframework/boot/webmvc/autoconfigure/WebMvcAutoConfiguration.java @@ -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 diff --git a/module/spring-boot-webmvc/src/test/java/org/springframework/boot/webmvc/autoconfigure/WebMvcAutoConfigurationTests.java b/module/spring-boot-webmvc/src/test/java/org/springframework/boot/webmvc/autoconfigure/WebMvcAutoConfigurationTests.java index b6f3df9497f..2629ccbd010 100644 --- a/module/spring-boot-webmvc/src/test/java/org/springframework/boot/webmvc/autoconfigure/WebMvcAutoConfigurationTests.java +++ b/module/spring-boot-webmvc/src/test/java/org/springframework/boot/webmvc/autoconfigure/WebMvcAutoConfigurationTests.java @@ -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) -> {