From 183a9466c522a26dcb44aef641d774bc8158a32e Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Mon, 3 Nov 2025 09:19:38 +0000 Subject: [PATCH] Add dedicated ApiVersionResolver implementations Closes gh-35747 --- .../web/accept/HeaderApiVersionResolver.java | 44 +++++++++++++ .../accept/HeaderApiVersionResolverTests.java | 53 +++++++++++++++ .../accept/HeaderApiVersionResolver.java | 44 +++++++++++++ .../accept/QueryApiVersionResolver.java | 44 +++++++++++++ .../reactive/config/ApiVersionConfigurer.java | 6 +- .../accept/HeaderApiVersionResolverTests.java | 54 +++++++++++++++ .../accept/QueryApiVersionResolverTests.java | 65 +++++++++++++++++++ .../annotation/ApiVersionConfigurer.java | 3 +- 8 files changed, 310 insertions(+), 3 deletions(-) create mode 100644 spring-web/src/main/java/org/springframework/web/accept/HeaderApiVersionResolver.java create mode 100644 spring-web/src/test/java/org/springframework/web/accept/HeaderApiVersionResolverTests.java create mode 100644 spring-webflux/src/main/java/org/springframework/web/reactive/accept/HeaderApiVersionResolver.java create mode 100644 spring-webflux/src/main/java/org/springframework/web/reactive/accept/QueryApiVersionResolver.java create mode 100644 spring-webflux/src/test/java/org/springframework/web/reactive/accept/HeaderApiVersionResolverTests.java create mode 100644 spring-webflux/src/test/java/org/springframework/web/reactive/accept/QueryApiVersionResolverTests.java diff --git a/spring-web/src/main/java/org/springframework/web/accept/HeaderApiVersionResolver.java b/spring-web/src/main/java/org/springframework/web/accept/HeaderApiVersionResolver.java new file mode 100644 index 00000000000..a3d3e0a84df --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/accept/HeaderApiVersionResolver.java @@ -0,0 +1,44 @@ +/* + * 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.accept; + + +import jakarta.servlet.http.HttpServletRequest; +import org.jspecify.annotations.Nullable; + +/** + * {@link ApiVersionResolver} that extract the version from a header. + * + * @author Rossen Stoyanchev + * @since 7.0 + */ +public class HeaderApiVersionResolver implements ApiVersionResolver { + + private final String headerName; + + + public HeaderApiVersionResolver(String headerName) { + this.headerName = headerName; + } + + + @Override + public @Nullable String resolveVersion(HttpServletRequest request) { + return request.getHeader(this.headerName); + } + +} diff --git a/spring-web/src/test/java/org/springframework/web/accept/HeaderApiVersionResolverTests.java b/spring-web/src/test/java/org/springframework/web/accept/HeaderApiVersionResolverTests.java new file mode 100644 index 00000000000..782c618161a --- /dev/null +++ b/spring-web/src/test/java/org/springframework/web/accept/HeaderApiVersionResolverTests.java @@ -0,0 +1,53 @@ +/* + * 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.accept; + + +import org.junit.jupiter.api.Test; + +import org.springframework.web.testfixture.servlet.MockHttpServletRequest; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Unit tests for {@link HeaderApiVersionResolver}. + * @author Rossen Stoyanchev + */ +public class HeaderApiVersionResolverTests { + + private final String headerName = "Api-Version"; + + private final HeaderApiVersionResolver resolver = new HeaderApiVersionResolver(headerName); + + + @Test + void resolve() { + String version = "1.2"; + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/path"); + request.addHeader(headerName, version); + String actual = resolver.resolveVersion(request); + assertThat(actual).isEqualTo(version); + } + + @Test + void noHeader() { + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/path"); + String version = resolver.resolveVersion(request); + assertThat(version).isNull(); + } + +} 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 new file mode 100644 index 00000000000..30576d7a947 --- /dev/null +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/accept/HeaderApiVersionResolver.java @@ -0,0 +1,44 @@ +/* + * 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 org.springframework.web.server.ServerWebExchange; + +/** + * {@link ApiVersionResolver} that extract the version from a request header. + * + * @author Rossen Stoyanchev + * @since 7.0 + */ +public class HeaderApiVersionResolver implements ApiVersionResolver { + + private final String headerName; + + + public HeaderApiVersionResolver(String headerName) { + this.headerName = headerName; + } + + + @Override + public @Nullable String resolveVersion(ServerWebExchange exchange) { + return exchange.getRequest().getHeaders().getFirst(this.headerName); + } + +} 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 new file mode 100644 index 00000000000..d42a560aaeb --- /dev/null +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/accept/QueryApiVersionResolver.java @@ -0,0 +1,44 @@ +/* + * 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 org.springframework.web.server.ServerWebExchange; + +/** + * {@link ApiVersionResolver} that extract the version from a query parameter. + * + * @author Rossen Stoyanchev + * @since 7.0 + */ +public class QueryApiVersionResolver implements ApiVersionResolver { + + private final String queryParamName; + + + public QueryApiVersionResolver(String queryParamName) { + this.queryParamName = queryParamName; + } + + + @Override + public @Nullable String resolveVersion(ServerWebExchange exchange) { + return exchange.getRequest().getQueryParams().getFirst(this.queryParamName); + } + +} diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/config/ApiVersionConfigurer.java b/spring-webflux/src/main/java/org/springframework/web/reactive/config/ApiVersionConfigurer.java index 183d563169e..59351a43aa3 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/config/ApiVersionConfigurer.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/config/ApiVersionConfigurer.java @@ -35,8 +35,10 @@ import org.springframework.web.reactive.accept.ApiVersionDeprecationHandler; import org.springframework.web.reactive.accept.ApiVersionResolver; import org.springframework.web.reactive.accept.ApiVersionStrategy; import org.springframework.web.reactive.accept.DefaultApiVersionStrategy; +import org.springframework.web.reactive.accept.HeaderApiVersionResolver; import org.springframework.web.reactive.accept.MediaTypeParamApiVersionResolver; import org.springframework.web.reactive.accept.PathApiVersionResolver; +import org.springframework.web.reactive.accept.QueryApiVersionResolver; import org.springframework.web.reactive.accept.StandardApiVersionDeprecationHandler; /** @@ -69,7 +71,7 @@ public class ApiVersionConfigurer { * @param headerName the header name to check */ public ApiVersionConfigurer useRequestHeader(String headerName) { - this.versionResolvers.add(exchange -> exchange.getRequest().getHeaders().getFirst(headerName)); + this.versionResolvers.add(new HeaderApiVersionResolver(headerName)); return this; } @@ -78,7 +80,7 @@ public class ApiVersionConfigurer { * @param paramName the parameter name to check */ public ApiVersionConfigurer useQueryParam(String paramName) { - this.versionResolvers.add(exchange -> exchange.getRequest().getQueryParams().getFirst(paramName)); + this.versionResolvers.add(new QueryApiVersionResolver(paramName)); return this; } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/accept/HeaderApiVersionResolverTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/accept/HeaderApiVersionResolverTests.java new file mode 100644 index 00000000000..8c79d6a081e --- /dev/null +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/accept/HeaderApiVersionResolverTests.java @@ -0,0 +1,54 @@ +/* + * 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.junit.jupiter.api.Test; + +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.testfixture.server.MockServerWebExchange; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.web.testfixture.http.server.reactive.MockServerHttpRequest.get; + +/** + * Unit tests for {@link HeaderApiVersionResolver}. + * @author Rossen Stoyanchev + */ +public class HeaderApiVersionResolverTests { + + private final String headerName = "Api-Version"; + + private final HeaderApiVersionResolver resolver = new HeaderApiVersionResolver(headerName); + + + @Test + void resolve() { + String version = "1.2"; + ServerWebExchange exchange = MockServerWebExchange.from(get("/").header(headerName, version)); + String actual = resolver.resolveVersion(exchange); + assertThat(actual).isEqualTo(version); + } + + @Test + void noHeader() { + ServerWebExchange exchange = MockServerWebExchange.from(get("/")); + String version = resolver.resolveVersion(exchange); + assertThat(version).isNull(); + } + +} diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/accept/QueryApiVersionResolverTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/accept/QueryApiVersionResolverTests.java new file mode 100644 index 00000000000..a0c131daa35 --- /dev/null +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/accept/QueryApiVersionResolverTests.java @@ -0,0 +1,65 @@ +/* + * 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 org.junit.jupiter.api.Test; + +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.testfixture.http.server.reactive.MockServerHttpRequest; +import org.springframework.web.testfixture.server.MockServerWebExchange; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Unit tests for {@link QueryApiVersionResolver}. + * @author Rossen Stoyanchev + */ +public class QueryApiVersionResolverTests { + + private final String queryParamName = "api-version"; + + private final QueryApiVersionResolver resolver = new QueryApiVersionResolver(queryParamName); + + + @Test + void resolve() { + ServerWebExchange exchange = initExchange("q=foo&" + queryParamName + "=1.2"); + String version = resolver.resolveVersion(exchange); + assertThat(version).isEqualTo("1.2"); + } + + @Test + void noQueryString() { + ServerWebExchange exchange = initExchange(null); + String version = resolver.resolveVersion(exchange); + assertThat(version).isNull(); + } + + @Test + void noQueryParam() { + ServerWebExchange exchange = initExchange("q=foo"); + String version = resolver.resolveVersion(exchange); + assertThat(version).isNull(); + } + + private static ServerWebExchange initExchange(@Nullable String queryString) { + return MockServerWebExchange.from(MockServerHttpRequest.get("/path?" + queryString)); + } + +} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ApiVersionConfigurer.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ApiVersionConfigurer.java index cbf9d9a4b63..92f06a09281 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ApiVersionConfigurer.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ApiVersionConfigurer.java @@ -33,6 +33,7 @@ import org.springframework.web.accept.ApiVersionParser; import org.springframework.web.accept.ApiVersionResolver; import org.springframework.web.accept.ApiVersionStrategy; import org.springframework.web.accept.DefaultApiVersionStrategy; +import org.springframework.web.accept.HeaderApiVersionResolver; import org.springframework.web.accept.InvalidApiVersionException; import org.springframework.web.accept.MediaTypeParamApiVersionResolver; import org.springframework.web.accept.PathApiVersionResolver; @@ -70,7 +71,7 @@ public class ApiVersionConfigurer { * @param headerName the header name to check */ public ApiVersionConfigurer useRequestHeader(String headerName) { - this.versionResolvers.add(request -> request.getHeader(headerName)); + this.versionResolvers.add(new HeaderApiVersionResolver(headerName)); return this; }