Restore ApiVersionResolver contract

Instead of making it async and having a sync subinterface variant,
this restores ApiVersionResolver to be as it was with an async
subinterface variant.

ApiVersionStrategy, and the infrastructure invoking it, remains
async first, but also accommodates sync resolvers.

This should provide a better balance with backwards compatibility
while also accommodating async version resolution as the less
common scenario.

See gh-36084
This commit is contained in:
rstoyanchev
2026-01-09 10:25:44 +00:00
parent 2f91baa71c
commit 39106b5811
8 changed files with 24 additions and 43 deletions
@@ -17,7 +17,6 @@
package org.springframework.web.reactive.accept;
import org.jspecify.annotations.Nullable;
import reactor.core.publisher.Mono;
import org.springframework.web.server.ServerWebExchange;
@@ -25,7 +24,6 @@ import org.springframework.web.server.ServerWebExchange;
* Contract to extract the version from a request.
*
* @author Rossen Stoyanchev
* @author Jonathan Kaplan
* @since 7.0
*/
@FunctionalInterface
@@ -33,21 +31,12 @@ public interface ApiVersionResolver {
/**
* Resolve the version for the given exchange.
* @param exchange the current exchange
* @return {@code Mono} emitting the version value, or an empty {@code Mono}
* @since 7.0.3
*/
Mono<String> resolveApiVersion(ServerWebExchange exchange);
/**
* Resolve the version for the given exchange.
* <p>Implementations of this method are expected to be non-blocking.
* If you need to resolve the version asynchronously, please implement the
* {@link AsyncApiVersionResolver} subinterface instead.
* @param exchange the current exchange
* @return the version value, or {@code null} if not found
* @deprecated in favor of {@link #resolveApiVersion(ServerWebExchange)}
*/
@Deprecated(since = "7.0.3", forRemoval = true)
default @Nullable String resolveVersion(ServerWebExchange exchange) {
return null;
}
@Nullable String resolveVersion(ServerWebExchange exchange);
}
@@ -23,35 +23,26 @@ import org.springframework.web.server.ServerWebExchange;
/**
* Extension of {@link ApiVersionResolver} for implementations that resolve the
* version in an imperative way without blocking.
* version in an asynchronous way.
*
* @author Rossen Stoyanchev
* @author Jonathan Kaplan
* @since 7.0.3
*/
@FunctionalInterface
public interface SyncApiVersionResolver extends ApiVersionResolver {
public interface AsyncApiVersionResolver extends ApiVersionResolver {
/**
* {@inheritDoc}
* <p>This method delegates to the synchronous
* {@link #resolveVersionValue} and wraps the result as {@code Mono}.
*/
@Override
default Mono<String> resolveApiVersion(ServerWebExchange exchange) {
return Mono.justOrEmpty(resolveVersionValue(exchange));
}
/**
* Resolve the version for the given exchange imperatively without blocking.
* Resolve the version for the given exchange.
* @param exchange the current exchange
* @return the version value, or {@code null} if not found
* @return {@code Mono} emitting the version value, or an empty {@code Mono}
*/
@Nullable String resolveVersionValue(ServerWebExchange exchange);
Mono<String> resolveVersionAsync(ServerWebExchange exchange);
@SuppressWarnings("removal")
@Override
default @Nullable String resolveVersion(ServerWebExchange exchange) {
return resolveVersionValue(exchange);
throw new UnsupportedOperationException(
"Async resolver does not support blocking resolution");
}
}
@@ -169,7 +169,9 @@ public class DefaultApiVersionStrategy implements ApiVersionStrategy {
@Override
public Mono<String> resolveApiVersion(ServerWebExchange exchange) {
return Flux.fromIterable(this.versionResolvers)
.flatMap(resolver -> resolver.resolveApiVersion(exchange))
.flatMap(resolver -> resolver instanceof AsyncApiVersionResolver asyncResolver ?
asyncResolver.resolveVersionAsync(exchange) :
Mono.justOrEmpty(resolver.resolveVersion(exchange)))
.next();
}
@@ -26,7 +26,7 @@ import org.springframework.web.server.ServerWebExchange;
* @author Rossen Stoyanchev
* @since 7.0
*/
public class HeaderApiVersionResolver implements SyncApiVersionResolver {
public class HeaderApiVersionResolver implements ApiVersionResolver {
private final String headerName;
@@ -37,7 +37,7 @@ public class HeaderApiVersionResolver implements SyncApiVersionResolver {
@Override
public @Nullable String resolveVersionValue(ServerWebExchange exchange) {
public @Nullable String resolveVersion(ServerWebExchange exchange) {
return exchange.getRequest().getHeaders().getFirst(this.headerName);
}
@@ -29,7 +29,7 @@ import org.springframework.web.server.ServerWebExchange;
* @author Rossen Stoyanchev
* @since 7.0
*/
public class MediaTypeParamApiVersionResolver implements SyncApiVersionResolver {
public class MediaTypeParamApiVersionResolver implements ApiVersionResolver {
private final MediaType compatibleMediaType;
@@ -49,7 +49,7 @@ public class MediaTypeParamApiVersionResolver implements SyncApiVersionResolver
@Override
public @Nullable String resolveVersionValue(ServerWebExchange exchange) {
public @Nullable String resolveVersion(ServerWebExchange exchange) {
HttpHeaders headers = exchange.getRequest().getHeaders();
for (MediaType mediaType : headers.getAccept()) {
if (this.compatibleMediaType.isCompatibleWith(mediaType)) {
@@ -32,7 +32,7 @@ import org.springframework.web.server.ServerWebExchange;
* @author Rossen Stoyanchev
* @since 7.0
*/
public class PathApiVersionResolver implements SyncApiVersionResolver {
public class PathApiVersionResolver implements ApiVersionResolver {
private final int pathSegmentIndex;
@@ -49,7 +49,7 @@ public class PathApiVersionResolver implements SyncApiVersionResolver {
@Override
public String resolveVersionValue(ServerWebExchange exchange) {
public String resolveVersion(ServerWebExchange exchange) {
int i = 0;
for (PathContainer.Element e : exchange.getRequest().getPath().pathWithinApplication().elements()) {
if (e instanceof PathContainer.PathSegment && i++ == this.pathSegmentIndex) {
@@ -26,7 +26,7 @@ import org.springframework.web.server.ServerWebExchange;
* @author Rossen Stoyanchev
* @since 7.0
*/
public class QueryApiVersionResolver implements SyncApiVersionResolver {
public class QueryApiVersionResolver implements ApiVersionResolver {
private final String queryParamName;
@@ -37,7 +37,7 @@ public class QueryApiVersionResolver implements SyncApiVersionResolver {
@Override
public @Nullable String resolveVersionValue(ServerWebExchange exchange) {
public @Nullable String resolveVersion(ServerWebExchange exchange) {
return exchange.getRequest().getQueryParams().getFirst(this.queryParamName);
}
@@ -22,7 +22,6 @@ 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;
@@ -382,7 +381,7 @@ class RequestPredicatesTests {
private static DefaultApiVersionStrategy apiVersionStrategy() {
return new DefaultApiVersionStrategy(
List.of(exchange -> Mono.empty()), new SemanticApiVersionParser(), true, null, false, null, null);
List.of(exchange -> null), new SemanticApiVersionParser(), true, null, false, null, null);
}
}