Add option to not use detected versions

Closes gh-34775
This commit is contained in:
rstoyanchev
2025-04-28 13:35:01 +01:00
parent 0c6a26a381
commit 6d6f657858
5 changed files with 121 additions and 42 deletions

View File

@@ -48,6 +48,10 @@ public class DefaultApiVersionStrategy implements ApiVersionStrategy {
private final Set<Comparable<?>> supportedVersions = new TreeSet<>();
private final boolean detectSupportedVersions;
private final Set<Comparable<?>> detectedVersions = new TreeSet<>();
/**
* Create an instance.
@@ -59,10 +63,13 @@ public class DefaultApiVersionStrategy implements ApiVersionStrategy {
* validation fails with {@link MissingApiVersionException}
* @param defaultVersion a default version to assign to requests that
* don't specify one
* @param detectSupportedVersions whether to use API versions that appear in
* mappings for supported version validation (true), or use only explicitly
* configured versions (false).
*/
public DefaultApiVersionStrategy(
List<ApiVersionResolver> versionResolvers, ApiVersionParser<?> versionParser,
boolean versionRequired, @Nullable String defaultVersion) {
boolean versionRequired, @Nullable String defaultVersion, boolean detectSupportedVersions) {
Assert.notEmpty(versionResolvers, "At least one ApiVersionResolver is required");
Assert.notNull(versionParser, "ApiVersionParser is required");
@@ -71,6 +78,7 @@ public class DefaultApiVersionStrategy implements ApiVersionStrategy {
this.versionParser = versionParser;
this.versionRequired = (versionRequired && defaultVersion == null);
this.defaultVersion = (defaultVersion != null ? versionParser.parseVersion(defaultVersion) : null);
this.detectSupportedVersions = detectSupportedVersions;
}
@@ -80,11 +88,15 @@ public class DefaultApiVersionStrategy implements ApiVersionStrategy {
}
/**
* Add to the list of known, supported versions to check against in
* {@link ApiVersionStrategy#validateVersion}. Request versions that are not
* in the supported result in {@link InvalidApiVersionException}
* in {@link ApiVersionStrategy#validateVersion}.
* @param versions the versions to add
* Add to the list of supported versions to check against in
* {@link ApiVersionStrategy#validateVersion} before raising
* {@link InvalidApiVersionException} for unknown versions.
* <p>By default, actual version values that appear in request mappings are
* considered supported, and use of this method is optional. However, if you
* prefer to use only explicitly configured, supported versions, then set
* {@code detectSupportedVersions} flag to {@code false}.
* @param versions the supported versions to add
* @see #addMappedVersion(String...)
*/
public void addSupportedVersion(String... versions) {
for (String version : versions) {
@@ -92,6 +104,21 @@ public class DefaultApiVersionStrategy implements ApiVersionStrategy {
}
}
/**
* Internal method to add to the list of actual version values that appear in
* request mappings, which allows supported versions to be discovered rather
* than {@link #addSupportedVersion(String...) configured}.
* <p>If you prefer to use explicitly configured, supported versions only,
* set the {@code detectSupportedVersions} flag to {@code false}.
* @param versions the versions to add
* @see #addSupportedVersion(String...)
*/
public void addMappedVersion(String... versions) {
for (String version : versions) {
this.detectedVersions.add(parseVersion(version));
}
}
@Override
public @Nullable String resolveVersion(ServerWebExchange exchange) {
for (ApiVersionResolver resolver : this.versionResolvers) {
@@ -118,15 +145,24 @@ public class DefaultApiVersionStrategy implements ApiVersionStrategy {
return;
}
if (!this.supportedVersions.contains(requestVersion)) {
if (!isSupportedVersion(requestVersion)) {
throw new InvalidApiVersionException(requestVersion.toString());
}
}
private boolean isSupportedVersion(Comparable<?> requestVersion) {
return (this.supportedVersions.contains(requestVersion) ||
this.detectSupportedVersions && this.detectedVersions.contains(requestVersion));
}
@Override
public String toString() {
return "DefaultApiVersionStrategy[supportedVersions=" + this.supportedVersions +
", versionRequired=" + this.versionRequired + ", defaultVersion=" + this.defaultVersion + "]";
return "DefaultApiVersionStrategy[" +
"supportedVersions=" + this.supportedVersions + ", " +
"mappedVersions=" + this.detectedVersions + ", " +
"detectSupportedVersions=" + this.detectSupportedVersions + ", " +
"versionRequired=" + this.versionRequired + ", " +
"defaultVersion=" + this.defaultVersion + "]";
}
}

View File

@@ -26,6 +26,7 @@ import java.util.Set;
import org.jspecify.annotations.Nullable;
import org.springframework.web.accept.ApiVersionParser;
import org.springframework.web.accept.InvalidApiVersionException;
import org.springframework.web.accept.SemanticApiVersionParser;
import org.springframework.web.reactive.accept.ApiVersionResolver;
import org.springframework.web.reactive.accept.ApiVersionStrategy;
@@ -50,6 +51,8 @@ public class ApiVersionConfigurer {
private final Set<String> supportedVersions = new LinkedHashSet<>();
private boolean detectSupportedVersions = true;
/**
* Add a resolver that extracts the API version from a request header.
@@ -125,20 +128,35 @@ public class ApiVersionConfigurer {
}
/**
* Add to the list of supported versions to validate request versions against.
* Request versions that are not supported result in
* {@link org.springframework.web.accept.InvalidApiVersionException}.
* <p>Note that the set of supported versions is populated from versions
* listed in controller mappings. Therefore, typically you do not have to
* manage this list except for the initial API version, when controller
* don't have to have a version to start.
* @param versions supported versions to add
* Add to the list of supported versions to check against before raising
* {@link InvalidApiVersionException} for unknown versions.
* <p>By default, actual version values that appear in request mappings are
* used for validation. Therefore, use of this method is optional. However,
* if you prefer to use explicitly configured, supported versions only, then
* set {@link #detectSupportedVersions} to {@code false}.
* <p>Note that the initial API version, if not explicitly declared in any
* request mappings, may need to be declared here instead as a supported
* version.
* @param versions supported version values to add
*/
public ApiVersionConfigurer addSupportedVersions(String... versions) {
Collections.addAll(this.supportedVersions, versions);
return this;
}
/**
* Whether to use versions from mappings for supported version validation.
* <p>By default, this is {@code true} in which case mapped versions are
* considered supported versions. Set this to {@code false} if you want to
* use only explicitly configured {@link #addSupportedVersions(String...)
* supported versions}.
* @param detect whether to use detected versions for validation
*/
public ApiVersionConfigurer detectSupportedVersions(boolean detect) {
this.detectSupportedVersions = detect;
return this;
}
protected @Nullable ApiVersionStrategy getApiVersionStrategy() {
if (this.versionResolvers.isEmpty()) {
return null;
@@ -146,7 +164,7 @@ public class ApiVersionConfigurer {
DefaultApiVersionStrategy strategy = new DefaultApiVersionStrategy(this.versionResolvers,
(this.versionParser != null ? this.versionParser : new SemanticApiVersionParser()),
this.versionRequired, this.defaultVersion);
this.versionRequired, this.defaultVersion, this.detectSupportedVersions);
this.supportedVersions.forEach(strategy::addSupportedVersion);

View File

@@ -239,7 +239,7 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi
if (requestMappingInfo != null && this.apiVersionStrategy instanceof DefaultApiVersionStrategy davs) {
String version = requestMappingInfo.getVersionCondition().getVersion();
if (version != null) {
davs.addSupportedVersion(version);
davs.addMappedVersion(version);
}
}

View File

@@ -36,39 +36,64 @@ import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy;
*/
public class DefaultApiVersionStrategiesTests {
private final SemanticApiVersionParser parser = new SemanticApiVersionParser();
private static final SemanticApiVersionParser parser = new SemanticApiVersionParser();
private static final ServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/"));
@Test
void defaultVersion() {
SemanticApiVersionParser.Version version = this.parser.parseVersion("1.2.3");
ApiVersionStrategy strategy = initVersionStrategy(version.toString());
assertThat(strategy.getDefaultVersion()).isEqualTo(version);
void defaultVersionIsParsed() {
String version = "1.2.3";
ApiVersionStrategy strategy = apiVersionStrategy(version, false);
assertThat(strategy.getDefaultVersion()).isEqualTo(parser.parseVersion(version));
}
@Test
void supportedVersions() {
SemanticApiVersionParser.Version v1 = this.parser.parseVersion("1");
SemanticApiVersionParser.Version v2 = this.parser.parseVersion("2");
SemanticApiVersionParser.Version v9 = this.parser.parseVersion("9");
void validateSupportedVersion() {
String version = "1.2";
DefaultApiVersionStrategy strategy = apiVersionStrategy();
strategy.addSupportedVersion(version);
validateVersion(version, strategy);
}
DefaultApiVersionStrategy strategy = initVersionStrategy(null);
strategy.addSupportedVersion(v1.toString());
strategy.addSupportedVersion(v2.toString());
ServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/"));
strategy.validateVersion(v1, exchange);
strategy.validateVersion(v2, exchange);
assertThatThrownBy(() -> strategy.validateVersion(v9, exchange))
@Test
void validateUnsupportedVersion() {
assertThatThrownBy(() -> validateVersion("1.2", apiVersionStrategy()))
.isInstanceOf(InvalidApiVersionException.class);
}
private static DefaultApiVersionStrategy initVersionStrategy(@Nullable String defaultValue) {
return new DefaultApiVersionStrategy(
@Test
void validateDetectedSupportedVersion() {
String version = "1.2";
DefaultApiVersionStrategy strategy = apiVersionStrategy(null, true);
strategy.addMappedVersion(version);
validateVersion(version, strategy);
}
@Test
void validateWhenDetectSupportedVersionsIsOff() {
String version = "1.2";
DefaultApiVersionStrategy strategy = apiVersionStrategy();
strategy.addMappedVersion(version);
assertThatThrownBy(() -> strategy.validateVersion(version, exchange))
.isInstanceOf(InvalidApiVersionException.class);
}
private static DefaultApiVersionStrategy apiVersionStrategy() {
return apiVersionStrategy(null, false);
}
private static DefaultApiVersionStrategy apiVersionStrategy(
@Nullable String defaultValue, boolean detectSupportedVersions) {
return new DefaultApiVersionStrategy(
List.of(exchange -> exchange.getRequest().getQueryParams().getFirst("api-version")),
new SemanticApiVersionParser(), true, defaultValue);
parser, true, defaultValue, detectSupportedVersions);
}
private static void validateVersion(String version, DefaultApiVersionStrategy strategy) {
strategy.validateVersion(parser.parseVersion(version), exchange);
}
}

View File

@@ -50,7 +50,7 @@ public class VersionRequestConditionTests {
private static DefaultApiVersionStrategy initVersionStrategy(@Nullable String defaultValue) {
return new DefaultApiVersionStrategy(
List.of(exchange -> exchange.getRequest().getQueryParams().getFirst("api-version")),
new SemanticApiVersionParser(), true, defaultValue);
new SemanticApiVersionParser(), true, defaultValue, false);
}
@Test