From b878771dcafcdf410d595e87b73cf2b5fc0dd2f9 Mon Sep 17 00:00:00 2001 From: Jonathan Kaplan Date: Tue, 30 Dec 2025 12:47:39 -0500 Subject: [PATCH 1/4] Update ApiVersionResolver to return Mono String See gh-36084 Signed-off-by: Jonathan Kaplan --- .../reactive/accept/ApiVersionResolver.java | 13 ++ .../reactive/accept/ApiVersionStrategy.java | 63 +++++++++ .../accept/DefaultApiVersionStrategy.java | 12 ++ .../handler/AbstractHandlerMapping.java | 21 ++- .../DefaultApiVersionStrategiesTests.java | 132 +++++++++++++++--- 5 files changed, 222 insertions(+), 19 deletions(-) diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/accept/ApiVersionResolver.java b/spring-webflux/src/main/java/org/springframework/web/reactive/accept/ApiVersionResolver.java index 77c7baae398..3e28bf585a2 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/accept/ApiVersionResolver.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/accept/ApiVersionResolver.java @@ -17,6 +17,7 @@ package org.springframework.web.reactive.accept; import org.jspecify.annotations.Nullable; +import reactor.core.publisher.Mono; import org.springframework.web.server.ServerWebExchange; @@ -24,6 +25,7 @@ import org.springframework.web.server.ServerWebExchange; * Contract to extract the version from a request. * * @author Rossen Stoyanchev + * @author Jonathan Kaplan * @since 7.0 */ @FunctionalInterface @@ -37,4 +39,15 @@ interface ApiVersionResolver { */ @Nullable String resolveVersion(ServerWebExchange exchange); + /** + * Asynchronously resolve the version for the given request exchange. + * This method wraps the synchronous {@code resolveVersion} method + * and provides a reactive alternative. + * @param exchange the current request exchange + * @return a {@code Mono} emitting the version value, or an empty {@code Mono} if no version is found + */ + default Mono resolveVersionAsync(ServerWebExchange exchange){ + return Mono.justOrEmpty(this.resolveVersion(exchange)); + } + } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/accept/ApiVersionStrategy.java b/spring-webflux/src/main/java/org/springframework/web/reactive/accept/ApiVersionStrategy.java index ff8cc2038e6..a9f66846660 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/accept/ApiVersionStrategy.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/accept/ApiVersionStrategy.java @@ -17,6 +17,7 @@ package org.springframework.web.reactive.accept; import org.jspecify.annotations.Nullable; +import reactor.core.publisher.Mono; import org.springframework.web.accept.InvalidApiVersionException; import org.springframework.web.accept.MissingApiVersionException; @@ -27,6 +28,7 @@ import org.springframework.web.server.ServerWebExchange; * to manage API versioning for an application. * * @author Rossen Stoyanchev + * @author Jonathan Kaplan * @since 7.0 * @see DefaultApiVersionStrategy */ @@ -37,10 +39,24 @@ public interface ApiVersionStrategy { * @param exchange the current exchange * @return the version, if present or {@code null} * @see ApiVersionResolver + * @deprecated as of 7.0.3, in favor of + * {@link #resolveVersionAsync(ServerWebExchange)} */ + @Deprecated(forRemoval = true, since = "7.0.3") @Nullable String resolveVersion(ServerWebExchange exchange); + + /** + * Resolve the version value from a request asynchronously. + * @param exchange the current server exchange containing the request details + * @return a {@code Mono} emitting the resolved version as a {@code String}, + * or an empty {@code Mono} if no version is resolved + */ + default Mono resolveVersionAsync(ServerWebExchange exchange) { + return Mono.justOrEmpty(this.resolveVersion(exchange)); + } + /** * Parse the version of a request into an Object. * @param version the value to parse @@ -59,6 +75,25 @@ public interface ApiVersionStrategy { void validateVersion(@Nullable Comparable requestVersion, ServerWebExchange exchange) throws MissingApiVersionException, InvalidApiVersionException; + /** + * Validates the provided request version against the requirements and constraints + * of the API. If the validation succeeds, the version is returned, otherwise an + * error is emitted. + * @param requestVersion the version to validate, which may be null + * @param exchange the current server exchange representing the HTTP request and response + * @return a {@code Mono} emitting the validated request version as a {@code Comparable}, + * or an error if validation fails + */ + default Mono> validateVersionAsync(@Nullable Comparable requestVersion, ServerWebExchange exchange) { + try { + this.validateVersion(requestVersion, exchange); + return Mono.justOrEmpty(requestVersion); + } + catch (MissingApiVersionException | InvalidApiVersionException ex) { + return Mono.error(ex); + } + } + /** * Return a default version to use for requests that don't specify one. */ @@ -70,6 +105,8 @@ public interface ApiVersionStrategy { * @param exchange the current exchange * @return the parsed request version, or the default version */ + @SuppressWarnings({"DeprecatedIsStillUsed", "DuplicatedCode"}) + @Deprecated(forRemoval = true, since = "7.0.3") default @Nullable Comparable resolveParseAndValidateVersion(ServerWebExchange exchange) { String value = resolveVersion(exchange); Comparable version; @@ -88,6 +125,32 @@ public interface ApiVersionStrategy { return version; } + /** + * Convenience method to return the API version from the given request exchange, parse and validate + * the version, and return the result as a reactive {@code Mono} stream. If no version + * is resolved, the default version is used. + * @param exchange the current server exchange containing the request details + * @return a {@code Mono} emitting the resolved, parsed, and validated version as a {@code Comparable}, + * or an error in case parsing or validation fails + */ + @SuppressWarnings("Convert2MethodRef") + default Mono> resolveParseAndValidateVersionAsync(ServerWebExchange exchange) { + return this.resolveVersionAsync(exchange) + .switchIfEmpty(Mono.justOrEmpty(this.getDefaultVersion()) + .mapNotNull(comparable -> comparable.toString())) + .>handle((version, sink) -> { + try { + sink.next(this.parseVersion(version)); + } + catch (Exception ex) { + sink.error(new InvalidApiVersionException(version, null, ex)); + } + }) + .flatMap(version -> this.validateVersionAsync(version, exchange)) + .switchIfEmpty(this.validateVersionAsync(null, exchange)); + + } + /** * Check if the requested API version is deprecated, and if so handle it * accordingly, e.g. by setting response headers to signal the deprecation, diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/accept/DefaultApiVersionStrategy.java b/spring-webflux/src/main/java/org/springframework/web/reactive/accept/DefaultApiVersionStrategy.java index da641e2e14b..7d6f6d8e3a4 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/accept/DefaultApiVersionStrategy.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/accept/DefaultApiVersionStrategy.java @@ -20,9 +20,12 @@ import java.util.ArrayList; import java.util.List; import java.util.Set; import java.util.TreeSet; +import java.util.function.Function; import java.util.function.Predicate; import org.jspecify.annotations.Nullable; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; import org.springframework.util.Assert; import org.springframework.web.accept.ApiVersionParser; @@ -152,6 +155,7 @@ public class DefaultApiVersionStrategy implements ApiVersionStrategy { } } + @SuppressWarnings("removal") @Override public @Nullable String resolveVersion(ServerWebExchange exchange) { for (ApiVersionResolver resolver : this.versionResolvers) { @@ -163,6 +167,14 @@ public class DefaultApiVersionStrategy implements ApiVersionStrategy { return null; } + @Override + public Mono resolveVersionAsync(ServerWebExchange exchange) { + return Flux.fromIterable(this.versionResolvers) + .mapNotNull(resolver -> resolver.resolveVersionAsync(exchange)) + .flatMap(Function.identity()) + .next(); + } + @Override public Comparable parseVersion(String version) { return this.versionParser.parseVersion(version); diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/handler/AbstractHandlerMapping.java b/spring-webflux/src/main/java/org/springframework/web/reactive/handler/AbstractHandlerMapping.java index b20577bea99..903ec701b40 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/handler/AbstractHandlerMapping.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/handler/AbstractHandlerMapping.java @@ -184,8 +184,9 @@ public abstract class AbstractHandlerMapping extends ApplicationObjectSupport @Override public Mono getHandler(ServerWebExchange exchange) { - initApiVersion(exchange); - return getHandlerInternal(exchange).map(handler -> { + this.initApiVersion(exchange); + return this.initApiVersionAsync(exchange).then( + getHandlerInternal(exchange).map(handler -> { if (logger.isDebugEnabled()) { logger.debug(exchange.getLogPrefix() + "Mapped to " + handler); } @@ -210,9 +211,11 @@ public abstract class AbstractHandlerMapping extends ApplicationObjectSupport } } return handler; - }); + })); } + @Deprecated(since = "7.0.3", forRemoval = true) + @SuppressWarnings({"removal", "DeprecatedIsStillUsed"}) private void initApiVersion(ServerWebExchange exchange) { if (this.apiVersionStrategy != null) { Comparable version = exchange.getAttribute(API_VERSION_ATTRIBUTE); @@ -225,6 +228,18 @@ public abstract class AbstractHandlerMapping extends ApplicationObjectSupport } } + private Mono> initApiVersionAsync(ServerWebExchange exchange) { + if (this.apiVersionStrategy != null) { + if (exchange.getAttribute(API_VERSION_ATTRIBUTE) == null) { + return this.apiVersionStrategy + .resolveParseAndValidateVersionAsync(exchange) + .doOnNext(version -> exchange.getAttributes() + .put(API_VERSION_ATTRIBUTE, version)); + } + } + return Mono.empty(); + } + /** * Look up a handler for the given request, returning an empty {@code Mono} * if no specific one is found. This method is called by {@link #getHandler}. diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/accept/DefaultApiVersionStrategiesTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/accept/DefaultApiVersionStrategiesTests.java index 1b46a2bbb0a..f6886795712 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/accept/DefaultApiVersionStrategiesTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/accept/DefaultApiVersionStrategiesTests.java @@ -21,6 +21,7 @@ import java.util.function.Predicate; import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; +import reactor.test.StepVerifier; import org.springframework.web.accept.InvalidApiVersionException; import org.springframework.web.accept.MissingApiVersionException; @@ -35,6 +36,7 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; /** * Unit tests for {@link org.springframework.web.accept.DefaultApiVersionStrategy}. * @author Rossen Stoyanchev + * @author Jonathan Kaplan */ public class DefaultApiVersionStrategiesTests { @@ -48,6 +50,116 @@ public class DefaultApiVersionStrategiesTests { assertThat(strategy.getDefaultVersion()).isEqualTo(parser.parseVersion(version)); } + @Test + void missingRequiredVersionReactively() { + + testValidateReactively(null, apiVersionStrategy()) + .expectErrorSatisfies(throwable -> + assertThat(throwable).isInstanceOf(MissingApiVersionException.class) + .hasMessage(("400 BAD_REQUEST \"API version is " + + "required.\"") + )) + .verify(); + } + + @Test + void validateSupportedVersionReactively() { + String version = "1.2"; + DefaultApiVersionStrategy strategy = apiVersionStrategy(); + strategy.addSupportedVersion(version); + testValidateReactively(version, strategy) + .expectNextMatches(next -> next.toString().equals("1.2.0")) + .verifyComplete(); + } + + @Test + void validateSupportedVersionForDefaultVersionReactively() { + String defaultVersion = "1.2"; + DefaultApiVersionStrategy strategy = apiVersionStrategy(defaultVersion, false, null); + + testValidateReactively(defaultVersion, strategy) + .expectNextMatches(next -> next.toString().equals("1.2.0")) + .verifyComplete(); + } + + @Test + void validateUnsupportedVersionReactively() { + testValidateReactively("1.2", apiVersionStrategy()) + .expectErrorSatisfies(throwable -> + assertThat(throwable).isInstanceOf(InvalidApiVersionException.class) + .hasMessage(("400 BAD_REQUEST \"Invalid API " + + "version: '1.2.0'.\"") + )) + .verify(); + + } + + @Test + void validateDetectedVersionReactively() { + String version = "1.2"; + DefaultApiVersionStrategy strategy = apiVersionStrategy(null, true, null); + strategy.addMappedVersion(version); + testValidateReactively(version, strategy) + .expectNextMatches(next -> next.toString().equals("1.2.0")) + .verifyComplete(); + } + + @Test + void validateWhenDetectedVersionOffReactively() { + String version = "1.2"; + DefaultApiVersionStrategy strategy = apiVersionStrategy(); + strategy.addMappedVersion(version); + testValidateReactively(version, strategy) + .expectError(InvalidApiVersionException.class) + .verify(); + } + + @Test + void validateSupportedWithPredicateReactively() { + SemanticApiVersionParser.Version parsedVersion = parser.parseVersion("1.2"); + testValidateReactively("1.2", apiVersionStrategy(null, false, version -> version.equals(parsedVersion))) + .expectNextMatches(next -> next.toString().equals("1.2.0")) + .verifyComplete(); + } + + @Test + void validateUnsupportedWithPredicateReactively() { + DefaultApiVersionStrategy strategy = apiVersionStrategy(null, false, version -> version.equals("1.2")); + testValidateReactively("1.2", strategy) + .verifyError(InvalidApiVersionException.class); + } + + @Test + void versionRequiredAndDefaultVersionSetReactively() { + assertThatIllegalArgumentException() + .isThrownBy(() -> + new org.springframework.web.accept.DefaultApiVersionStrategy( + List.of(request -> request.getParameter("api-version")), new SemanticApiVersionParser(), + true, "1.2", true, version -> true, null)) + .withMessage("versionRequired cannot be set to true if a defaultVersion is also configured"); + } + + private static DefaultApiVersionStrategy apiVersionStrategy() { + return apiVersionStrategy(null, false, null); + } + + private static DefaultApiVersionStrategy apiVersionStrategy( + @Nullable String defaultVersion, boolean detectSupportedVersions, + @Nullable Predicate> supportedVersionPredicate) { + + return new DefaultApiVersionStrategy( + List.of(exchange -> exchange.getRequest().getQueryParams().getFirst("api-version")), + parser, null, defaultVersion, detectSupportedVersions, supportedVersionPredicate, null); + } + + private StepVerifier.FirstStep> testValidateReactively(@Nullable String version, DefaultApiVersionStrategy strategy) { + MockServerHttpRequest.BaseBuilder requestBuilder = MockServerHttpRequest.get("/"); + if (version != null) { + requestBuilder.queryParam("api-version", version); + } + return StepVerifier.create(strategy.resolveParseAndValidateVersionAsync(MockServerWebExchange.builder(requestBuilder).build())); + } + @Test void missingRequiredVersion() { assertThatThrownBy(() -> testValidate(null, apiVersionStrategy())) @@ -109,25 +221,13 @@ public class DefaultApiVersionStrategiesTests { void versionRequiredAndDefaultVersionSet() { assertThatIllegalArgumentException() .isThrownBy(() -> - new org.springframework.web.accept.DefaultApiVersionStrategy( - List.of(request -> request.getParameter("api-version")), new SemanticApiVersionParser(), - true, "1.2", true, version -> true, null)) + new org.springframework.web.accept.DefaultApiVersionStrategy( + List.of(request -> request.getParameter("api-version")), new SemanticApiVersionParser(), + true, "1.2", true, version -> true, null)) .withMessage("versionRequired cannot be set to true if a defaultVersion is also configured"); } - private static DefaultApiVersionStrategy apiVersionStrategy() { - return apiVersionStrategy(null, false, null); - } - - private static DefaultApiVersionStrategy apiVersionStrategy( - @Nullable String defaultVersion, boolean detectSupportedVersions, - @Nullable Predicate> supportedVersionPredicate) { - - return new DefaultApiVersionStrategy( - List.of(exchange -> exchange.getRequest().getQueryParams().getFirst("api-version")), - parser, null, defaultVersion, detectSupportedVersions, supportedVersionPredicate, null); - } - + @SuppressWarnings("removal") private void testValidate(@Nullable String version, DefaultApiVersionStrategy strategy) { MockServerHttpRequest.BaseBuilder requestBuilder = MockServerHttpRequest.get("/"); if (version != null) { From 7d33a872786fcc64c1ad793ea1f303c68ee2bdf8 Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Thu, 8 Jan 2026 12:55:30 +0000 Subject: [PATCH 2/4] Polishing contribution See gh-36084 --- .../reactive/accept/ApiVersionResolver.java | 19 +- .../reactive/accept/ApiVersionStrategy.java | 93 +++++----- .../accept/DefaultApiVersionStrategy.java | 4 +- .../handler/AbstractHandlerMapping.java | 2 +- .../DefaultApiVersionStrategiesTests.java | 168 +++++------------- 5 files changed, 99 insertions(+), 187 deletions(-) diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/accept/ApiVersionResolver.java b/spring-webflux/src/main/java/org/springframework/web/reactive/accept/ApiVersionResolver.java index 3e28bf585a2..084198518b9 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/accept/ApiVersionResolver.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/accept/ApiVersionResolver.java @@ -34,20 +34,21 @@ interface ApiVersionResolver { /** * Resolve the version for the given exchange. - * @param exchange the current exchange - * @return the version value, or {@code null} if not found - */ - @Nullable String resolveVersion(ServerWebExchange exchange); - - /** - * Asynchronously resolve the version for the given request exchange. * This method wraps the synchronous {@code resolveVersion} method * and provides a reactive alternative. - * @param exchange the current request exchange - * @return a {@code Mono} emitting the version value, or an empty {@code Mono} if no version is found + * @param exchange the current exchange + * @return {@code Mono} emitting the version value, or an empty {@code Mono} + * @since 7.0.3 */ default Mono resolveVersionAsync(ServerWebExchange exchange){ return Mono.justOrEmpty(this.resolveVersion(exchange)); } + /** + * Resolve the version for the given exchange. + * @param exchange the current exchange + * @return the version value, or {@code null} if not found + */ + @Nullable String resolveVersion(ServerWebExchange exchange); + } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/accept/ApiVersionStrategy.java b/spring-webflux/src/main/java/org/springframework/web/reactive/accept/ApiVersionStrategy.java index a9f66846660..38ce07b6def 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/accept/ApiVersionStrategy.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/accept/ApiVersionStrategy.java @@ -34,29 +34,29 @@ import org.springframework.web.server.ServerWebExchange; */ public interface ApiVersionStrategy { + /** + * Resolve the version value from a request. + * @param exchange the current exchange + * @return a {@code Mono} emitting the raw version as a {@code String}, + * or an empty {@code Mono} if no version is found + * @since 7.0.3 + * @see ApiVersionResolver + */ + default Mono resolveVersionAsync(ServerWebExchange exchange) { + return Mono.justOrEmpty(resolveVersion(exchange)); + } + /** * Resolve the version value from a request, e.g. from a request header. * @param exchange the current exchange * @return the version, if present or {@code null} * @see ApiVersionResolver - * @deprecated as of 7.0.3, in favor of - * {@link #resolveVersionAsync(ServerWebExchange)} + * @deprecated as of 7.0.3, in favor of {@link #resolveVersionAsync(ServerWebExchange)} */ - @Deprecated(forRemoval = true, since = "7.0.3") + @Deprecated(since = "7.0.3", forRemoval = true) @Nullable String resolveVersion(ServerWebExchange exchange); - - /** - * Resolve the version value from a request asynchronously. - * @param exchange the current server exchange containing the request details - * @return a {@code Mono} emitting the resolved version as a {@code String}, - * or an empty {@code Mono} if no version is resolved - */ - default Mono resolveVersionAsync(ServerWebExchange exchange) { - return Mono.justOrEmpty(this.resolveVersion(exchange)); - } - /** * Parse the version of a request into an Object. * @param version the value to parse @@ -75,6 +75,36 @@ public interface ApiVersionStrategy { void validateVersion(@Nullable Comparable requestVersion, ServerWebExchange exchange) throws MissingApiVersionException, InvalidApiVersionException; + /** + * Return a default version to use for requests that don't specify one. + */ + @Nullable Comparable getDefaultVersion(); + + /** + * Convenience method to resolve, parse, and validate the request version, + * or return the default version if configured. + * @param exchange the current exchange + * @return a {@code Mono} emitting the request version, a validation error, + * or an empty {@code Mono} if there is no version + * @since 7.0.3 + */ + @SuppressWarnings("Convert2MethodRef") + default Mono> resolveParseAndValidate(ServerWebExchange exchange) { + return this.resolveVersionAsync(exchange) + .switchIfEmpty(Mono.justOrEmpty(this.getDefaultVersion()) + .mapNotNull(comparable -> comparable.toString())) + .>handle((version, sink) -> { + try { + sink.next(this.parseVersion(version)); + } + catch (Exception ex) { + sink.error(new InvalidApiVersionException(version, null, ex)); + } + }) + .flatMap(version -> this.validateVersionAsync(version, exchange)) + .switchIfEmpty(this.validateVersionAsync(null, exchange)); + } + /** * Validates the provided request version against the requirements and constraints * of the API. If the validation succeeds, the version is returned, otherwise an @@ -94,19 +124,14 @@ public interface ApiVersionStrategy { } } - /** - * Return a default version to use for requests that don't specify one. - */ - @Nullable Comparable getDefaultVersion(); - /** * Convenience method to return the parsed and validated request version, * or the default version if configured. * @param exchange the current exchange * @return the parsed request version, or the default version + * @deprecated in favor of {@link #resolveParseAndValidate(ServerWebExchange)} */ - @SuppressWarnings({"DeprecatedIsStillUsed", "DuplicatedCode"}) - @Deprecated(forRemoval = true, since = "7.0.3") + @Deprecated(since = "7.0.3", forRemoval = true) default @Nullable Comparable resolveParseAndValidateVersion(ServerWebExchange exchange) { String value = resolveVersion(exchange); Comparable version; @@ -125,32 +150,6 @@ public interface ApiVersionStrategy { return version; } - /** - * Convenience method to return the API version from the given request exchange, parse and validate - * the version, and return the result as a reactive {@code Mono} stream. If no version - * is resolved, the default version is used. - * @param exchange the current server exchange containing the request details - * @return a {@code Mono} emitting the resolved, parsed, and validated version as a {@code Comparable}, - * or an error in case parsing or validation fails - */ - @SuppressWarnings("Convert2MethodRef") - default Mono> resolveParseAndValidateVersionAsync(ServerWebExchange exchange) { - return this.resolveVersionAsync(exchange) - .switchIfEmpty(Mono.justOrEmpty(this.getDefaultVersion()) - .mapNotNull(comparable -> comparable.toString())) - .>handle((version, sink) -> { - try { - sink.next(this.parseVersion(version)); - } - catch (Exception ex) { - sink.error(new InvalidApiVersionException(version, null, ex)); - } - }) - .flatMap(version -> this.validateVersionAsync(version, exchange)) - .switchIfEmpty(this.validateVersionAsync(null, exchange)); - - } - /** * Check if the requested API version is deprecated, and if so handle it * accordingly, e.g. by setting response headers to signal the deprecation, diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/accept/DefaultApiVersionStrategy.java b/spring-webflux/src/main/java/org/springframework/web/reactive/accept/DefaultApiVersionStrategy.java index 7d6f6d8e3a4..ba8fcd7570a 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/accept/DefaultApiVersionStrategy.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/accept/DefaultApiVersionStrategy.java @@ -20,7 +20,6 @@ import java.util.ArrayList; import java.util.List; import java.util.Set; import java.util.TreeSet; -import java.util.function.Function; import java.util.function.Predicate; import org.jspecify.annotations.Nullable; @@ -170,8 +169,7 @@ public class DefaultApiVersionStrategy implements ApiVersionStrategy { @Override public Mono resolveVersionAsync(ServerWebExchange exchange) { return Flux.fromIterable(this.versionResolvers) - .mapNotNull(resolver -> resolver.resolveVersionAsync(exchange)) - .flatMap(Function.identity()) + .flatMap(resolver -> resolver.resolveVersionAsync(exchange)) .next(); } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/handler/AbstractHandlerMapping.java b/spring-webflux/src/main/java/org/springframework/web/reactive/handler/AbstractHandlerMapping.java index 903ec701b40..4cd924aa15b 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/handler/AbstractHandlerMapping.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/handler/AbstractHandlerMapping.java @@ -232,7 +232,7 @@ public abstract class AbstractHandlerMapping extends ApplicationObjectSupport if (this.apiVersionStrategy != null) { if (exchange.getAttribute(API_VERSION_ATTRIBUTE) == null) { return this.apiVersionStrategy - .resolveParseAndValidateVersionAsync(exchange) + .resolveParseAndValidate(exchange) .doOnNext(version -> exchange.getAttributes() .put(API_VERSION_ATTRIBUTE, version)); } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/accept/DefaultApiVersionStrategiesTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/accept/DefaultApiVersionStrategiesTests.java index f6886795712..ad6fb739218 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/accept/DefaultApiVersionStrategiesTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/accept/DefaultApiVersionStrategiesTests.java @@ -21,6 +21,7 @@ import java.util.function.Predicate; import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; import reactor.test.StepVerifier; import org.springframework.web.accept.InvalidApiVersionException; @@ -31,7 +32,6 @@ import org.springframework.web.testfixture.server.MockServerWebExchange; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; -import static org.assertj.core.api.Assertions.assertThatThrownBy; /** * Unit tests for {@link org.springframework.web.accept.DefaultApiVersionStrategy}. @@ -51,91 +51,80 @@ public class DefaultApiVersionStrategiesTests { } @Test - void missingRequiredVersionReactively() { - - testValidateReactively(null, apiVersionStrategy()) - .expectErrorSatisfies(throwable -> - assertThat(throwable).isInstanceOf(MissingApiVersionException.class) - .hasMessage(("400 BAD_REQUEST \"API version is " + - "required.\"") - )) + void missingRequiredVersion() { + StepVerifier.create(testValidate(null, apiVersionStrategy())) + .expectErrorSatisfies(ex -> assertThat(ex) + .isInstanceOf(MissingApiVersionException.class) + .hasMessage(("400 BAD_REQUEST \"API version is required.\""))) .verify(); } @Test - void validateSupportedVersionReactively() { + void validateSupportedVersion() { String version = "1.2"; DefaultApiVersionStrategy strategy = apiVersionStrategy(); strategy.addSupportedVersion(version); - testValidateReactively(version, strategy) - .expectNextMatches(next -> next.toString().equals("1.2.0")) - .verifyComplete(); + Mono result = testValidate(version, strategy); + StepVerifier.create(result).expectNext("1.2.0").verifyComplete(); } @Test - void validateSupportedVersionForDefaultVersionReactively() { + void validateSupportedVersionForDefaultVersion() { String defaultVersion = "1.2"; DefaultApiVersionStrategy strategy = apiVersionStrategy(defaultVersion, false, null); - - testValidateReactively(defaultVersion, strategy) - .expectNextMatches(next -> next.toString().equals("1.2.0")) - .verifyComplete(); + Mono result = testValidate(defaultVersion, strategy); + StepVerifier.create(result).expectNext("1.2.0").verifyComplete(); } @Test - void validateUnsupportedVersionReactively() { - testValidateReactively("1.2", apiVersionStrategy()) - .expectErrorSatisfies(throwable -> - assertThat(throwable).isInstanceOf(InvalidApiVersionException.class) - .hasMessage(("400 BAD_REQUEST \"Invalid API " + - "version: '1.2.0'.\"") - )) + void validateUnsupportedVersion() { + StepVerifier.create(testValidate("1.2", apiVersionStrategy())) + .expectErrorSatisfies(ex -> assertThat(ex) + .isInstanceOf(InvalidApiVersionException.class) + .hasMessage(("400 BAD_REQUEST \"Invalid API version: '1.2.0'.\""))) .verify(); - } @Test - void validateDetectedVersionReactively() { + void validateDetectedVersion() { String version = "1.2"; DefaultApiVersionStrategy strategy = apiVersionStrategy(null, true, null); strategy.addMappedVersion(version); - testValidateReactively(version, strategy) - .expectNextMatches(next -> next.toString().equals("1.2.0")) - .verifyComplete(); + Mono result = testValidate(version, strategy); + StepVerifier.create(result).expectNext("1.2.0").verifyComplete(); } @Test - void validateWhenDetectedVersionOffReactively() { + void validateWhenDetectedVersionOff() { String version = "1.2"; DefaultApiVersionStrategy strategy = apiVersionStrategy(); strategy.addMappedVersion(version); - testValidateReactively(version, strategy) - .expectError(InvalidApiVersionException.class) - .verify(); + Mono result = testValidate(version, strategy); + StepVerifier.create(result).expectError(InvalidApiVersionException.class).verify(); } @Test - void validateSupportedWithPredicateReactively() { + void validateSupportedWithPredicate() { SemanticApiVersionParser.Version parsedVersion = parser.parseVersion("1.2"); - testValidateReactively("1.2", apiVersionStrategy(null, false, version -> version.equals(parsedVersion))) - .expectNextMatches(next -> next.toString().equals("1.2.0")) - .verifyComplete(); + ApiVersionStrategy strategy = apiVersionStrategy(null, false, version -> version.equals(parsedVersion)); + Mono result = testValidate("1.2", strategy); + StepVerifier.create(result).expectNext("1.2.0").verifyComplete(); } @Test - void validateUnsupportedWithPredicateReactively() { - DefaultApiVersionStrategy strategy = apiVersionStrategy(null, false, version -> version.equals("1.2")); - testValidateReactively("1.2", strategy) - .verifyError(InvalidApiVersionException.class); + void validateUnsupportedWithPredicate() { + ApiVersionStrategy strategy = apiVersionStrategy(null, false, version -> version.equals("1.2")); + Mono result = testValidate("1.2", strategy); + StepVerifier.create(result).verifyError(InvalidApiVersionException.class); } @Test - void versionRequiredAndDefaultVersionSetReactively() { + void versionRequiredAndDefaultVersionSet() { assertThatIllegalArgumentException() - .isThrownBy(() -> - new org.springframework.web.accept.DefaultApiVersionStrategy( - List.of(request -> request.getParameter("api-version")), new SemanticApiVersionParser(), - true, "1.2", true, version -> true, null)) + .isThrownBy(() -> { + ApiVersionResolver resolver = new QueryApiVersionResolver("api-version"); + new DefaultApiVersionStrategy(List.of(resolver), parser, true, "1.2", true, v -> true, null); + }) .withMessage("versionRequired cannot be set to true if a defaultVersion is also configured"); } @@ -152,88 +141,13 @@ public class DefaultApiVersionStrategiesTests { parser, null, defaultVersion, detectSupportedVersions, supportedVersionPredicate, null); } - private StepVerifier.FirstStep> testValidateReactively(@Nullable String version, DefaultApiVersionStrategy strategy) { - MockServerHttpRequest.BaseBuilder requestBuilder = MockServerHttpRequest.get("/"); - if (version != null) { - requestBuilder.queryParam("api-version", version); - } - return StepVerifier.create(strategy.resolveParseAndValidateVersionAsync(MockServerWebExchange.builder(requestBuilder).build())); - } - - @Test - void missingRequiredVersion() { - assertThatThrownBy(() -> testValidate(null, apiVersionStrategy())) - .isInstanceOf(MissingApiVersionException.class) - .hasMessage("400 BAD_REQUEST \"API version is required.\""); - } - - @Test - void validateSupportedVersion() { - String version = "1.2"; - DefaultApiVersionStrategy strategy = apiVersionStrategy(); - strategy.addSupportedVersion(version); - testValidate(version, strategy); - } - - @Test - void validateSupportedVersionForDefaultVersion() { - String defaultVersion = "1.2"; - DefaultApiVersionStrategy strategy = apiVersionStrategy(defaultVersion, false, null); - testValidate(defaultVersion, strategy); - } - - @Test - void validateUnsupportedVersion() { - assertThatThrownBy(() -> testValidate("1.2", apiVersionStrategy())) - .isInstanceOf(InvalidApiVersionException.class) - .hasMessage("400 BAD_REQUEST \"Invalid API version: '1.2.0'.\""); - } - - @Test - void validateDetectedVersion() { - String version = "1.2"; - DefaultApiVersionStrategy strategy = apiVersionStrategy(null, true, null); - strategy.addMappedVersion(version); - testValidate(version, strategy); - } - - @Test - void validateWhenDetectedVersionOff() { - String version = "1.2"; - DefaultApiVersionStrategy strategy = apiVersionStrategy(); - strategy.addMappedVersion(version); - assertThatThrownBy(() -> testValidate(version, strategy)).isInstanceOf(InvalidApiVersionException.class); - } - - @Test - void validateSupportedWithPredicate() { - SemanticApiVersionParser.Version parsedVersion = parser.parseVersion("1.2"); - testValidate("1.2", apiVersionStrategy(null, false, version -> version.equals(parsedVersion))); - } - - @Test - void validateUnsupportedWithPredicate() { - DefaultApiVersionStrategy strategy = apiVersionStrategy(null, false, version -> version.equals("1.2")); - assertThatThrownBy(() -> testValidate("1.2", strategy)).isInstanceOf(InvalidApiVersionException.class); - } - - @Test - void versionRequiredAndDefaultVersionSet() { - assertThatIllegalArgumentException() - .isThrownBy(() -> - new org.springframework.web.accept.DefaultApiVersionStrategy( - List.of(request -> request.getParameter("api-version")), new SemanticApiVersionParser(), - true, "1.2", true, version -> true, null)) - .withMessage("versionRequired cannot be set to true if a defaultVersion is also configured"); - } - - @SuppressWarnings("removal") - private void testValidate(@Nullable String version, DefaultApiVersionStrategy strategy) { - MockServerHttpRequest.BaseBuilder requestBuilder = MockServerHttpRequest.get("/"); + private Mono testValidate(@Nullable String version, ApiVersionStrategy strategy) { + MockServerHttpRequest.BaseBuilder builder = MockServerHttpRequest.get("/"); if (version != null) { - requestBuilder.queryParam("api-version", version); + builder.queryParam("api-version", version); } - strategy.resolveParseAndValidateVersion(MockServerWebExchange.builder(requestBuilder).build()); + MockServerWebExchange exchange = MockServerWebExchange.builder(builder).build(); + return strategy.resolveParseAndValidate(exchange).map(Object::toString); } } From dcf5d69d94a67a1d4dbf61bbf8210de24f41a503 Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Thu, 8 Jan 2026 13:10:58 +0000 Subject: [PATCH 3/4] Rename async methods See gh-36084 --- .../web/reactive/accept/ApiVersionResolver.java | 2 +- .../web/reactive/accept/ApiVersionStrategy.java | 10 +++++----- .../web/reactive/accept/DefaultApiVersionStrategy.java | 4 ++-- .../web/reactive/handler/AbstractHandlerMapping.java | 2 +- .../accept/DefaultApiVersionStrategiesTests.java | 2 +- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/accept/ApiVersionResolver.java b/spring-webflux/src/main/java/org/springframework/web/reactive/accept/ApiVersionResolver.java index 084198518b9..020c8ce1ecc 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/accept/ApiVersionResolver.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/accept/ApiVersionResolver.java @@ -40,7 +40,7 @@ interface ApiVersionResolver { * @return {@code Mono} emitting the version value, or an empty {@code Mono} * @since 7.0.3 */ - default Mono resolveVersionAsync(ServerWebExchange exchange){ + default Mono resolveApiVersion(ServerWebExchange exchange){ return Mono.justOrEmpty(this.resolveVersion(exchange)); } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/accept/ApiVersionStrategy.java b/spring-webflux/src/main/java/org/springframework/web/reactive/accept/ApiVersionStrategy.java index 38ce07b6def..2e084aa7a29 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/accept/ApiVersionStrategy.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/accept/ApiVersionStrategy.java @@ -42,7 +42,7 @@ public interface ApiVersionStrategy { * @since 7.0.3 * @see ApiVersionResolver */ - default Mono resolveVersionAsync(ServerWebExchange exchange) { + default Mono resolveApiVersion(ServerWebExchange exchange) { return Mono.justOrEmpty(resolveVersion(exchange)); } @@ -51,7 +51,7 @@ public interface ApiVersionStrategy { * @param exchange the current exchange * @return the version, if present or {@code null} * @see ApiVersionResolver - * @deprecated as of 7.0.3, in favor of {@link #resolveVersionAsync(ServerWebExchange)} + * @deprecated as of 7.0.3, in favor of {@link #resolveApiVersion(ServerWebExchange)} */ @Deprecated(since = "7.0.3", forRemoval = true) @Nullable @@ -89,8 +89,8 @@ public interface ApiVersionStrategy { * @since 7.0.3 */ @SuppressWarnings("Convert2MethodRef") - default Mono> resolveParseAndValidate(ServerWebExchange exchange) { - return this.resolveVersionAsync(exchange) + default Mono> resolveParseAndValidateApiVersion(ServerWebExchange exchange) { + return this.resolveApiVersion(exchange) .switchIfEmpty(Mono.justOrEmpty(this.getDefaultVersion()) .mapNotNull(comparable -> comparable.toString())) .>handle((version, sink) -> { @@ -129,7 +129,7 @@ public interface ApiVersionStrategy { * or the default version if configured. * @param exchange the current exchange * @return the parsed request version, or the default version - * @deprecated in favor of {@link #resolveParseAndValidate(ServerWebExchange)} + * @deprecated in favor of {@link #resolveParseAndValidateApiVersion(ServerWebExchange)} */ @Deprecated(since = "7.0.3", forRemoval = true) default @Nullable Comparable resolveParseAndValidateVersion(ServerWebExchange exchange) { diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/accept/DefaultApiVersionStrategy.java b/spring-webflux/src/main/java/org/springframework/web/reactive/accept/DefaultApiVersionStrategy.java index ba8fcd7570a..a6fd704c2e5 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/accept/DefaultApiVersionStrategy.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/accept/DefaultApiVersionStrategy.java @@ -167,9 +167,9 @@ public class DefaultApiVersionStrategy implements ApiVersionStrategy { } @Override - public Mono resolveVersionAsync(ServerWebExchange exchange) { + public Mono resolveApiVersion(ServerWebExchange exchange) { return Flux.fromIterable(this.versionResolvers) - .flatMap(resolver -> resolver.resolveVersionAsync(exchange)) + .flatMap(resolver -> resolver.resolveApiVersion(exchange)) .next(); } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/handler/AbstractHandlerMapping.java b/spring-webflux/src/main/java/org/springframework/web/reactive/handler/AbstractHandlerMapping.java index 4cd924aa15b..c9e3d30abf6 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/handler/AbstractHandlerMapping.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/handler/AbstractHandlerMapping.java @@ -232,7 +232,7 @@ public abstract class AbstractHandlerMapping extends ApplicationObjectSupport if (this.apiVersionStrategy != null) { if (exchange.getAttribute(API_VERSION_ATTRIBUTE) == null) { return this.apiVersionStrategy - .resolveParseAndValidate(exchange) + .resolveParseAndValidateApiVersion(exchange) .doOnNext(version -> exchange.getAttributes() .put(API_VERSION_ATTRIBUTE, version)); } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/accept/DefaultApiVersionStrategiesTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/accept/DefaultApiVersionStrategiesTests.java index ad6fb739218..c2be3b1fe8c 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/accept/DefaultApiVersionStrategiesTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/accept/DefaultApiVersionStrategiesTests.java @@ -147,7 +147,7 @@ public class DefaultApiVersionStrategiesTests { builder.queryParam("api-version", version); } MockServerWebExchange exchange = MockServerWebExchange.builder(builder).build(); - return strategy.resolveParseAndValidate(exchange).map(Object::toString); + return strategy.resolveParseAndValidateApiVersion(exchange).map(Object::toString); } } From 8ff89ffda27f48a961a5ddf28cea988c8e94f4c1 Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Thu, 8 Jan 2026 13:26:18 +0000 Subject: [PATCH 4/4] Update contribution - deprecate sync method on ApiVersionResolver - add SyncApiVersionResolver - refactor resolverParseAndValidateApiVersion method See gh-36084 --- .../reactive/accept/ApiVersionResolver.java | 13 +++-- .../reactive/accept/ApiVersionStrategy.java | 47 +++++++-------- .../accept/HeaderApiVersionResolver.java | 4 +- .../MediaTypeParamApiVersionResolver.java | 4 +- .../accept/PathApiVersionResolver.java | 4 +- .../accept/QueryApiVersionResolver.java | 4 +- .../accept/SyncApiVersionResolver.java | 57 +++++++++++++++++++ .../DefaultApiVersionStrategiesTests.java | 2 +- .../server/RequestPredicatesTests.java | 3 +- .../VersionRequestConditionTests.java | 3 +- 10 files changed, 96 insertions(+), 45 deletions(-) create mode 100644 spring-webflux/src/main/java/org/springframework/web/reactive/accept/SyncApiVersionResolver.java diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/accept/ApiVersionResolver.java b/spring-webflux/src/main/java/org/springframework/web/reactive/accept/ApiVersionResolver.java index 020c8ce1ecc..0d468b3357f 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/accept/ApiVersionResolver.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/accept/ApiVersionResolver.java @@ -29,8 +29,7 @@ import org.springframework.web.server.ServerWebExchange; * @since 7.0 */ @FunctionalInterface -public -interface ApiVersionResolver { +public interface ApiVersionResolver { /** * Resolve the version for the given exchange. @@ -40,15 +39,17 @@ interface ApiVersionResolver { * @return {@code Mono} emitting the version value, or an empty {@code Mono} * @since 7.0.3 */ - default Mono resolveApiVersion(ServerWebExchange exchange){ - return Mono.justOrEmpty(this.resolveVersion(exchange)); - } + Mono resolveApiVersion(ServerWebExchange exchange); /** * Resolve the version for the given exchange. * @param exchange the current exchange * @return the version value, or {@code null} if not found + * @deprecated in favor of {@link #resolveApiVersion(ServerWebExchange)} */ - @Nullable String resolveVersion(ServerWebExchange exchange); + @Deprecated(since = "7.0.3", forRemoval = true) + default @Nullable String resolveVersion(ServerWebExchange exchange) { + return null; + } } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/accept/ApiVersionStrategy.java b/spring-webflux/src/main/java/org/springframework/web/reactive/accept/ApiVersionStrategy.java index 2e084aa7a29..fc71a6c5257 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/accept/ApiVersionStrategy.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/accept/ApiVersionStrategy.java @@ -88,40 +88,31 @@ public interface ApiVersionStrategy { * or an empty {@code Mono} if there is no version * @since 7.0.3 */ - @SuppressWarnings("Convert2MethodRef") default Mono> resolveParseAndValidateApiVersion(ServerWebExchange exchange) { - return this.resolveApiVersion(exchange) - .switchIfEmpty(Mono.justOrEmpty(this.getDefaultVersion()) - .mapNotNull(comparable -> comparable.toString())) - .>handle((version, sink) -> { + + Mono> result = resolveApiVersion(exchange) + .map(value -> { + Comparable version; try { - sink.next(this.parseVersion(version)); + version = parseVersion(value); } catch (Exception ex) { - sink.error(new InvalidApiVersionException(version, null, ex)); + throw new InvalidApiVersionException(value, null, ex); } - }) - .flatMap(version -> this.validateVersionAsync(version, exchange)) - .switchIfEmpty(this.validateVersionAsync(null, exchange)); - } + validateVersion(version, exchange); + return version; + }); - /** - * Validates the provided request version against the requirements and constraints - * of the API. If the validation succeeds, the version is returned, otherwise an - * error is emitted. - * @param requestVersion the version to validate, which may be null - * @param exchange the current server exchange representing the HTTP request and response - * @return a {@code Mono} emitting the validated request version as a {@code Comparable}, - * or an error if validation fails - */ - default Mono> validateVersionAsync(@Nullable Comparable requestVersion, ServerWebExchange exchange) { - try { - this.validateVersion(requestVersion, exchange); - return Mono.justOrEmpty(requestVersion); - } - catch (MissingApiVersionException | InvalidApiVersionException ex) { - return Mono.error(ex); - } + return result.switchIfEmpty(Mono.fromSupplier(() -> { + Comparable defaultVersion = getDefaultVersion(); + if (defaultVersion != null) { + return defaultVersion; + } + else { + validateVersion(null, exchange); + return null; + } + })); } /** diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/accept/HeaderApiVersionResolver.java b/spring-webflux/src/main/java/org/springframework/web/reactive/accept/HeaderApiVersionResolver.java index 4d7871dec63..07863cefd91 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/accept/HeaderApiVersionResolver.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/accept/HeaderApiVersionResolver.java @@ -26,7 +26,7 @@ import org.springframework.web.server.ServerWebExchange; * @author Rossen Stoyanchev * @since 7.0 */ -public class HeaderApiVersionResolver implements ApiVersionResolver { +public class HeaderApiVersionResolver implements SyncApiVersionResolver { private final String headerName; @@ -37,7 +37,7 @@ public class HeaderApiVersionResolver implements ApiVersionResolver { @Override - public @Nullable String resolveVersion(ServerWebExchange exchange) { + public @Nullable String resolveVersionValue(ServerWebExchange exchange) { return exchange.getRequest().getHeaders().getFirst(this.headerName); } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/accept/MediaTypeParamApiVersionResolver.java b/spring-webflux/src/main/java/org/springframework/web/reactive/accept/MediaTypeParamApiVersionResolver.java index c9967091088..db9b9acd3b5 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/accept/MediaTypeParamApiVersionResolver.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/accept/MediaTypeParamApiVersionResolver.java @@ -29,7 +29,7 @@ import org.springframework.web.server.ServerWebExchange; * @author Rossen Stoyanchev * @since 7.0 */ -public class MediaTypeParamApiVersionResolver implements ApiVersionResolver { +public class MediaTypeParamApiVersionResolver implements SyncApiVersionResolver { private final MediaType compatibleMediaType; @@ -49,7 +49,7 @@ public class MediaTypeParamApiVersionResolver implements ApiVersionResolver { @Override - public @Nullable String resolveVersion(ServerWebExchange exchange) { + public @Nullable String resolveVersionValue(ServerWebExchange exchange) { HttpHeaders headers = exchange.getRequest().getHeaders(); for (MediaType mediaType : headers.getAccept()) { if (this.compatibleMediaType.isCompatibleWith(mediaType)) { diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/accept/PathApiVersionResolver.java b/spring-webflux/src/main/java/org/springframework/web/reactive/accept/PathApiVersionResolver.java index 2da6819498b..06de39d20be 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/accept/PathApiVersionResolver.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/accept/PathApiVersionResolver.java @@ -32,7 +32,7 @@ import org.springframework.web.server.ServerWebExchange; * @author Rossen Stoyanchev * @since 7.0 */ -public class PathApiVersionResolver implements ApiVersionResolver { +public class PathApiVersionResolver implements SyncApiVersionResolver { private final int pathSegmentIndex; @@ -49,7 +49,7 @@ public class PathApiVersionResolver implements ApiVersionResolver { @Override - public String resolveVersion(ServerWebExchange exchange) { + public String resolveVersionValue(ServerWebExchange exchange) { int i = 0; for (PathContainer.Element e : exchange.getRequest().getPath().pathWithinApplication().elements()) { if (e instanceof PathContainer.PathSegment && i++ == this.pathSegmentIndex) { diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/accept/QueryApiVersionResolver.java b/spring-webflux/src/main/java/org/springframework/web/reactive/accept/QueryApiVersionResolver.java index 16606c75cbc..f81f832bfea 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/accept/QueryApiVersionResolver.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/accept/QueryApiVersionResolver.java @@ -26,7 +26,7 @@ import org.springframework.web.server.ServerWebExchange; * @author Rossen Stoyanchev * @since 7.0 */ -public class QueryApiVersionResolver implements ApiVersionResolver { +public class QueryApiVersionResolver implements SyncApiVersionResolver { private final String queryParamName; @@ -37,7 +37,7 @@ public class QueryApiVersionResolver implements ApiVersionResolver { @Override - public @Nullable String resolveVersion(ServerWebExchange exchange) { + public @Nullable String resolveVersionValue(ServerWebExchange exchange) { return exchange.getRequest().getQueryParams().getFirst(this.queryParamName); } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/accept/SyncApiVersionResolver.java b/spring-webflux/src/main/java/org/springframework/web/reactive/accept/SyncApiVersionResolver.java new file mode 100644 index 00000000000..bca707274d2 --- /dev/null +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/accept/SyncApiVersionResolver.java @@ -0,0 +1,57 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.reactive.accept; + +import org.jspecify.annotations.Nullable; +import reactor.core.publisher.Mono; + +import org.springframework.web.server.ServerWebExchange; + +/** + * An extension of {@link ApiVersionResolver}s for implementations that can + * resolve the version in an imperative way without blocking. + * + * @author Rossen Stoyanchev + * @since 7.0.3 + */ +@FunctionalInterface +public interface SyncApiVersionResolver extends ApiVersionResolver { + + /** + * {@inheritDoc} + *

This method delegates to the synchronous + * {@link #resolveVersionValue} and wraps the result as {@code Mono}. + */ + @Override + default Mono resolveApiVersion(ServerWebExchange exchange) { + return Mono.justOrEmpty(resolveVersionValue(exchange)); + } + + /** + * Resolve the version for the given exchange imperatively without blocking. + * @param exchange the current exchange + * @return the version value, or {@code null} if not found + */ + @Nullable String resolveVersionValue(ServerWebExchange exchange); + + @SuppressWarnings("removal") + @Override + default @Nullable String resolveVersion(ServerWebExchange exchange) { + return resolveVersionValue(exchange); + } + +} diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/accept/DefaultApiVersionStrategiesTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/accept/DefaultApiVersionStrategiesTests.java index c2be3b1fe8c..99015f12f41 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/accept/DefaultApiVersionStrategiesTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/accept/DefaultApiVersionStrategiesTests.java @@ -137,7 +137,7 @@ public class DefaultApiVersionStrategiesTests { @Nullable Predicate> supportedVersionPredicate) { return new DefaultApiVersionStrategy( - List.of(exchange -> exchange.getRequest().getQueryParams().getFirst("api-version")), + List.of(new QueryApiVersionResolver("api-version")), parser, null, defaultVersion, detectSupportedVersions, supportedVersionPredicate, null); } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/RequestPredicatesTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/RequestPredicatesTests.java index ec779c637f9..c06b7c4f784 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/RequestPredicatesTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/RequestPredicatesTests.java @@ -22,6 +22,7 @@ import java.util.List; import java.util.function.Function; import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -380,7 +381,7 @@ class RequestPredicatesTests { private static DefaultApiVersionStrategy apiVersionStrategy() { return new DefaultApiVersionStrategy( - List.of(exchange -> null), new SemanticApiVersionParser(), true, null, false, null, null); + List.of(exchange -> Mono.empty()), new SemanticApiVersionParser(), true, null, false, null, null); } } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/condition/VersionRequestConditionTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/condition/VersionRequestConditionTests.java index e658ec41da0..06acba7573d 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/condition/VersionRequestConditionTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/condition/VersionRequestConditionTests.java @@ -28,6 +28,7 @@ import org.springframework.web.accept.NotAcceptableApiVersionException; import org.springframework.web.accept.SemanticApiVersionParser; import org.springframework.web.reactive.HandlerMapping; import org.springframework.web.reactive.accept.DefaultApiVersionStrategy; +import org.springframework.web.reactive.accept.QueryApiVersionResolver; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.testfixture.http.server.reactive.MockServerHttpRequest; import org.springframework.web.testfixture.server.MockServerWebExchange; @@ -51,7 +52,7 @@ public class VersionRequestConditionTests { private static DefaultApiVersionStrategy initVersionStrategy(@Nullable String defaultVersion) { return new DefaultApiVersionStrategy( - List.of(exchange -> exchange.getRequest().getQueryParams().getFirst("api-version")), + List.of(new QueryApiVersionResolver("api-version")), new SemanticApiVersionParser(), null, defaultVersion, false, null, null); }