API versioning support for Spring MVC
See gh-34566
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2025 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.
|
||||
@@ -264,9 +264,8 @@ public class DelegatingWebMvcConfigurationTests {
|
||||
};
|
||||
|
||||
RequestMappingHandlerMapping annotationsMapping = webMvcConfig.requestMappingHandlerMapping(
|
||||
webMvcConfig.mvcContentNegotiationManager(),
|
||||
webMvcConfig.mvcConversionService(),
|
||||
webMvcConfig.mvcResourceUrlProvider());
|
||||
webMvcConfig.mvcContentNegotiationManager(), webMvcConfig.mvcApiVersionStrategy(),
|
||||
webMvcConfig.mvcConversionService(), webMvcConfig.mvcResourceUrlProvider());
|
||||
|
||||
assertThat(annotationsMapping).isNotNull();
|
||||
configAssertion.accept(annotationsMapping.getUrlPathHelper(), annotationsMapping.getPathMatcher());
|
||||
@@ -330,9 +329,8 @@ public class DelegatingWebMvcConfigurationTests {
|
||||
};
|
||||
|
||||
RequestMappingHandlerMapping annotationsMapping = webMvcConfig.requestMappingHandlerMapping(
|
||||
webMvcConfig.mvcContentNegotiationManager(),
|
||||
webMvcConfig.mvcConversionService(),
|
||||
webMvcConfig.mvcResourceUrlProvider());
|
||||
webMvcConfig.mvcContentNegotiationManager(), webMvcConfig.mvcApiVersionStrategy(),
|
||||
webMvcConfig.mvcConversionService(), webMvcConfig.mvcResourceUrlProvider());
|
||||
|
||||
assertThat(annotationsMapping).isNotNull();
|
||||
assertThat(annotationsMapping.getPatternParser())
|
||||
|
||||
@@ -126,7 +126,7 @@ class WebMvcConfigurationSupportExtensionTests {
|
||||
@Test
|
||||
void handlerMappings() throws Exception {
|
||||
RequestMappingHandlerMapping rmHandlerMapping = this.config.requestMappingHandlerMapping(
|
||||
this.config.mvcContentNegotiationManager(),
|
||||
this.config.mvcContentNegotiationManager(), this.config.mvcApiVersionStrategy(),
|
||||
this.config.mvcConversionService(), this.config.mvcResourceUrlProvider());
|
||||
rmHandlerMapping.setApplicationContext(this.context);
|
||||
rmHandlerMapping.afterPropertiesSet();
|
||||
@@ -266,8 +266,8 @@ class WebMvcConfigurationSupportExtensionTests {
|
||||
NativeWebRequest webRequest = new ServletWebRequest(request);
|
||||
|
||||
RequestMappingHandlerMapping mapping = this.config.requestMappingHandlerMapping(
|
||||
this.config.mvcContentNegotiationManager(), this.config.mvcConversionService(),
|
||||
this.config.mvcResourceUrlProvider());
|
||||
this.config.mvcContentNegotiationManager(), this.config.mvcApiVersionStrategy(),
|
||||
this.config.mvcConversionService(), this.config.mvcResourceUrlProvider());
|
||||
|
||||
request.setParameter("f", "json");
|
||||
ContentNegotiationManager manager = mapping.getContentNegotiationManager();
|
||||
@@ -347,7 +347,7 @@ class WebMvcConfigurationSupportExtensionTests {
|
||||
* plus WebMvcConfigurer can switch to extending WebMvcConfigurationSupport directly for
|
||||
* more advanced configuration needs.
|
||||
*/
|
||||
private class TestWebMvcConfigurationSupport extends WebMvcConfigurationSupport implements WebMvcConfigurer {
|
||||
private static class TestWebMvcConfigurationSupport extends WebMvcConfigurationSupport implements WebMvcConfigurer {
|
||||
|
||||
@Override
|
||||
public void addFormatters(FormatterRegistry registry) {
|
||||
@@ -384,6 +384,11 @@ class WebMvcConfigurationSupportExtensionTests {
|
||||
configurer.favorParameter(true).parameterName("f");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureApiVersioning(ApiVersionConfigurer configurer) {
|
||||
configurer.useRequestHeader("X-API-Version");
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("deprecation")
|
||||
public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
/*
|
||||
* Copyright 2002-2025 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.servlet.mvc.condition;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.web.accept.DefaultApiVersionStrategy;
|
||||
import org.springframework.web.accept.NotAcceptableApiVersionException;
|
||||
import org.springframework.web.accept.SemanticApiVersionParser;
|
||||
import org.springframework.web.testfixture.servlet.MockHttpServletRequest;
|
||||
|
||||
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
|
||||
import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link VersionRequestCondition}.
|
||||
* @author Rossen Stoyanchev
|
||||
*/
|
||||
public class VersionRequestConditionTests {
|
||||
|
||||
private DefaultApiVersionStrategy strategy;
|
||||
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
this.strategy = initVersionStrategy(null);
|
||||
}
|
||||
|
||||
private static DefaultApiVersionStrategy initVersionStrategy(@Nullable String defaultValue) {
|
||||
return new DefaultApiVersionStrategy(
|
||||
List.of(request -> request.getParameter("api-version")),
|
||||
new SemanticApiVersionParser(), true, defaultValue);
|
||||
}
|
||||
|
||||
@Test
|
||||
void combineMethodLevelOnly() {
|
||||
VersionRequestCondition condition = emptyCondition().combine(condition("1.1"));
|
||||
assertThat(condition.getVersion()).isEqualTo("1.1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void combineTypeLevelOnly() {
|
||||
VersionRequestCondition condition = condition("1.1").combine(emptyCondition());
|
||||
assertThat(condition.getVersion()).isEqualTo("1.1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void combineTypeAndMethodLevel() {
|
||||
assertThat(condition("1.1").combine(condition("1.2")).getVersion()).isEqualTo("1.2");
|
||||
}
|
||||
|
||||
@Test
|
||||
void fixedVersionMatch() {
|
||||
String conditionVersion = "1.2";
|
||||
this.strategy.addSupportedVersion("1.1", "1.3");
|
||||
|
||||
testMatch("v1.1", conditionVersion, true, false);
|
||||
testMatch("v1.2", conditionVersion, false, false);
|
||||
testMatch("v1.3", conditionVersion, false, true);
|
||||
}
|
||||
|
||||
@Test
|
||||
void baselineVersionMatch() {
|
||||
String conditionVersion = "1.2+";
|
||||
this.strategy.addSupportedVersion("1.1", "1.3");
|
||||
|
||||
testMatch("v1.1", conditionVersion, true, false);
|
||||
testMatch("v1.2", conditionVersion, false, false);
|
||||
testMatch("v1.3", conditionVersion, false, false);
|
||||
}
|
||||
|
||||
private void testMatch(
|
||||
String requestVersion, String conditionVersion, boolean notCompatible, boolean notAcceptable) {
|
||||
|
||||
MockHttpServletRequest request = requestWithVersion(requestVersion);
|
||||
VersionRequestCondition condition = condition(conditionVersion);
|
||||
VersionRequestCondition match = condition.getMatchingCondition(request);
|
||||
|
||||
if (notCompatible) {
|
||||
assertThat(match).isNull();
|
||||
return;
|
||||
}
|
||||
|
||||
assertThat(match).isSameAs(condition);
|
||||
|
||||
if (notAcceptable) {
|
||||
assertThatThrownBy(() -> condition.handleMatch(request)).isInstanceOf(NotAcceptableApiVersionException.class);
|
||||
return;
|
||||
}
|
||||
|
||||
condition.handleMatch(request);
|
||||
}
|
||||
|
||||
@Test
|
||||
void missingRequiredVersion() {
|
||||
assertThatThrownBy(() -> condition("1.2").getMatchingCondition(new MockHttpServletRequest("GET", "/path")))
|
||||
.hasMessage("400 BAD_REQUEST \"API version is required.\"");
|
||||
}
|
||||
|
||||
@Test
|
||||
void defaultVersion() {
|
||||
String version = "1.2";
|
||||
this.strategy = initVersionStrategy(version);
|
||||
VersionRequestCondition condition = condition(version);
|
||||
VersionRequestCondition match = condition.getMatchingCondition(new MockHttpServletRequest("GET", "/path"));
|
||||
|
||||
assertThat(match).isSameAs(condition);
|
||||
}
|
||||
|
||||
@Test
|
||||
void unsupportedVersion() {
|
||||
assertThatThrownBy(() -> condition("1.2").getMatchingCondition(requestWithVersion("1.3")))
|
||||
.hasMessage("400 BAD_REQUEST \"Invalid API version: '1.3.0'.\"");
|
||||
}
|
||||
|
||||
@Test
|
||||
void compare() {
|
||||
testCompare("1.1", "1", "1.1");
|
||||
testCompare("1.1.1", "1", "1.1", "1.1.1");
|
||||
testCompare("10", "1.1", "10");
|
||||
testCompare("10", "2", "10");
|
||||
}
|
||||
|
||||
private void testCompare(String expected, String... versions) {
|
||||
List<VersionRequestCondition> list = Arrays.stream(versions)
|
||||
.map(this::condition)
|
||||
.sorted((c1, c2) -> c1.compareTo(c2, new MockHttpServletRequest()))
|
||||
.toList();
|
||||
|
||||
assertThat(list.get(0)).isEqualTo(condition(expected));
|
||||
}
|
||||
|
||||
private VersionRequestCondition condition(String v) {
|
||||
this.strategy.addSupportedVersion(v.endsWith("+") ? v.substring(0, v.length() - 1) : v);
|
||||
return new VersionRequestCondition(v, this.strategy);
|
||||
}
|
||||
|
||||
private VersionRequestCondition emptyCondition() {
|
||||
return new VersionRequestCondition();
|
||||
}
|
||||
|
||||
private MockHttpServletRequest requestWithVersion(String v) {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/path");
|
||||
request.addParameter("api-version", v);
|
||||
return request;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -40,7 +40,7 @@ class RequestMappingInfoHandlerMethodMappingNamingStrategyTests {
|
||||
HandlerMethod handlerMethod = new HandlerMethod(new TestController(), method);
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
RequestMappingInfo rmi = new RequestMappingInfo("foo", null, null, null, null, null, null, null);
|
||||
RequestMappingInfo rmi = new RequestMappingInfo("foo", null, null, null, null, null, null, null, null);
|
||||
|
||||
HandlerMethodMappingNamingStrategy<RequestMappingInfo> strategy = new RequestMappingInfoHandlerMethodMappingNamingStrategy();
|
||||
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
/*
|
||||
* Copyright 2002-2025 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.servlet.mvc.method.annotation;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import jakarta.servlet.ServletException;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
|
||||
import org.springframework.web.servlet.DispatcherServlet;
|
||||
import org.springframework.web.servlet.config.annotation.ApiVersionConfigurer;
|
||||
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
import org.springframework.web.testfixture.servlet.MockHttpServletRequest;
|
||||
import org.springframework.web.testfixture.servlet.MockHttpServletResponse;
|
||||
import org.springframework.web.testfixture.servlet.MockServletConfig;
|
||||
|
||||
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
|
||||
|
||||
/**
|
||||
* Integration tests for API versioning.
|
||||
* @author Rossen Stoyanchev
|
||||
*/
|
||||
public class RequestMappingVersionHandlerMethodTests {
|
||||
|
||||
private DispatcherServlet dispatcherServlet;
|
||||
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws ServletException {
|
||||
AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
|
||||
context.setServletConfig(new MockServletConfig());
|
||||
context.register(WebConfig.class, TestController.class);
|
||||
context.afterPropertiesSet();
|
||||
|
||||
this.dispatcherServlet = new DispatcherServlet(context);
|
||||
this.dispatcherServlet.init(new MockServletConfig());
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void initialVersion() throws Exception {
|
||||
assertThat(requestWithVersion("1.0").getContentAsString()).isEqualTo("none");
|
||||
assertThat(requestWithVersion("1.1").getContentAsString()).isEqualTo("none");
|
||||
}
|
||||
|
||||
@Test
|
||||
void baselineVersion() throws Exception {
|
||||
assertThat(requestWithVersion("1.2").getContentAsString()).isEqualTo("1.2");
|
||||
assertThat(requestWithVersion("1.3").getContentAsString()).isEqualTo("1.2");
|
||||
}
|
||||
|
||||
@Test
|
||||
void fixedVersion() throws Exception {
|
||||
assertThat(requestWithVersion("1.5").getContentAsString()).isEqualTo("1.5");
|
||||
|
||||
MockHttpServletResponse response = requestWithVersion("1.6");
|
||||
assertThat(response.getStatus()).isEqualTo(400);
|
||||
}
|
||||
|
||||
private MockHttpServletResponse requestWithVersion(String version) throws ServletException, IOException {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/");
|
||||
request.addHeader("X-API-VERSION", version);
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
this.dispatcherServlet.service(request, response);
|
||||
return response;
|
||||
}
|
||||
|
||||
|
||||
@EnableWebMvc
|
||||
private static class WebConfig implements WebMvcConfigurer {
|
||||
|
||||
@Override
|
||||
public void configureApiVersioning(ApiVersionConfigurer configurer) {
|
||||
configurer.useRequestHeader("X-API-Version").addSupportedVersions("1", "1.1", "1.3", "1.6");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@RestController
|
||||
private static class TestController {
|
||||
|
||||
@GetMapping
|
||||
String noVersion() {
|
||||
return getBody("none");
|
||||
}
|
||||
|
||||
@GetMapping(version = "1.2+")
|
||||
String version1_2() {
|
||||
return getBody("1.2");
|
||||
}
|
||||
|
||||
@GetMapping(version = "1.5")
|
||||
String version1_5() {
|
||||
return getBody("1.5");
|
||||
}
|
||||
|
||||
private static String getBody(String version) {
|
||||
return version;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user