diff --git a/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestMapping.java b/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestMapping.java
index 0bff16d474fb..7640171ff772 100644
--- a/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestMapping.java
+++ b/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestMapping.java
@@ -18,6 +18,7 @@
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
+import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@@ -80,6 +81,7 @@
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Mapping
+@Repeatable(RequestMappings.class)
@Reflective(ControllerMappingReflectiveProcessor.class)
public @interface RequestMapping {
diff --git a/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestMappings.java b/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestMappings.java
new file mode 100644
index 000000000000..29b77020072f
--- /dev/null
+++ b/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestMappings.java
@@ -0,0 +1,25 @@
+package org.springframework.web.bind.annotation;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Container annotation that aggregates several {@link RequestMapping} annotations.
+ *
+ *
Can be used natively, declaring several nested {@link RequestMapping} annotations.
+ * Can also be used in conjunction with Java 8's support for repeatable annotations,
+ * where {@link RequestMapping} can simply be declared several times on the same method,
+ * implicitly generating this container annotation.
+ *
+ * @see RequestMapping
+ * @since 6.2
+ */
+@Target({ElementType.TYPE, ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface RequestMappings {
+ RequestMapping[] value();
+}
diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/AbstractHandlerMethodMapping.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/AbstractHandlerMethodMapping.java
index bc5c8126b75b..054961e657e5 100644
--- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/AbstractHandlerMethodMapping.java
+++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/AbstractHandlerMethodMapping.java
@@ -31,6 +31,7 @@
import java.util.function.Function;
import java.util.stream.Collectors;
+import org.springframework.lang.NonNull;
import reactor.core.publisher.Mono;
import org.springframework.aop.support.AopUtils;
@@ -168,7 +169,7 @@ public void afterPropertiesSet() {
/**
* Scan beans in the ApplicationContext, detect and register handler methods.
* @see #isHandler(Class)
- * @see #getMappingForMethod(Method, Class)
+ * @see #getListMappingsForMethod(Method, Class)
* @see #handlerMethodsInitialized(Map)
*/
protected void initHandlerMethods() {
@@ -204,22 +205,24 @@ protected void detectHandlerMethods(final Object handler) {
if (handlerType != null) {
final Class> userType = ClassUtils.getUserClass(handlerType);
- Map methods = MethodIntrospector.selectMethods(userType,
- (MethodIntrospector.MetadataLookup) method -> getMappingForMethod(method, userType));
+ Map> methods = MethodIntrospector.selectMethods(userType,
+ (MethodIntrospector.MetadataLookup>) method -> getListMappingsForMethod(method, userType));
if (logger.isTraceEnabled()) {
logger.trace(formatMappings(userType, methods));
}
else if (mappingsLogger.isDebugEnabled()) {
mappingsLogger.debug(formatMappings(userType, methods));
}
- methods.forEach((method, mapping) -> {
+ methods.forEach((method, mappings) -> {
Method invocableMethod = AopUtils.selectInvocableMethod(method, userType);
- registerHandlerMethod(handler, invocableMethod, mapping);
+ for (T mapping : mappings) {
+ registerHandlerMethod(handler, invocableMethod, mapping);
+ }
});
}
}
- private String formatMappings(Class> userType, Map methods) {
+ private String formatMappings(Class> userType, Map> methods) {
String packageName = ClassUtils.getPackageName(userType);
String formattedType = (StringUtils.hasText(packageName) ?
Arrays.stream(packageName.split("\\."))
@@ -423,15 +426,15 @@ protected CorsConfiguration getCorsConfiguration(Object handler, ServerWebExchan
protected abstract boolean isHandler(Class> beanType);
/**
- * Provide the mapping for a handler method. A method for which no
+ * Provide the list of mappings for a handler method. A method for which no
* mapping can be provided is not a handler method.
* @param method the method to provide a mapping for
* @param handlerType the handler type, possibly a subtype of the method's
* declaring class
- * @return the mapping, or {@code null} if the method is not mapped
+ * @return the list of mappings, or an empty list if the method is not mapped
*/
- @Nullable
- protected abstract T getMappingForMethod(Method method, Class> handlerType);
+ @NonNull
+ protected abstract List getListMappingsForMethod(Method method, Class> handlerType);
/**
* Return the request mapping paths that are not patterns.
diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMapping.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMapping.java
index 0196a47b7495..996b3bb2d140 100644
--- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMapping.java
+++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMapping.java
@@ -20,6 +20,7 @@
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
+import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
@@ -34,6 +35,7 @@
import org.springframework.core.annotation.MergedAnnotations;
import org.springframework.core.annotation.MergedAnnotations.SearchStrategy;
import org.springframework.core.annotation.RepeatableContainers;
+import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Controller;
import org.springframework.util.Assert;
@@ -152,42 +154,59 @@ protected boolean isHandler(Class> beanType) {
/**
* Uses type-level and method-level {@link RequestMapping @RequestMapping}
- * and {@link HttpExchange @HttpExchange} annotations to create the
- * {@link RequestMappingInfo}.
- * @return the created {@code RequestMappingInfo}, or {@code null} if the method
+ * and {@link HttpExchange @HttpExchange} annotations to create the list
+ * of {@link RequestMappingInfo}.
+ * @return the created list of {@code RequestMappingInfo}, or an empty list if the method
* does not have a {@code @RequestMapping} or {@code @HttpExchange} annotation
* @see #getCustomMethodCondition(Method)
* @see #getCustomTypeCondition(Class)
*/
@Override
- @Nullable
- protected RequestMappingInfo getMappingForMethod(Method method, Class> handlerType) {
- RequestMappingInfo info = createRequestMappingInfo(method);
- if (info != null) {
- RequestMappingInfo typeInfo = createRequestMappingInfo(handlerType);
- if (typeInfo != null) {
- info = typeInfo.combine(info);
+ @NonNull
+ protected List getListMappingsForMethod(Method method, Class> handlerType) {
+ List result = new ArrayList<>();
+ List infos = buildListOfRequestMappingInfo(method);
+ if (!infos.isEmpty()) {
+ List typeInfos = buildListOfRequestMappingInfo(handlerType);
+ if (!typeInfos.isEmpty()) {
+ List requestMappingInfos = new ArrayList<>();
+ for (RequestMappingInfo info : infos) {
+ for (RequestMappingInfo typeInfo : typeInfos) {
+ requestMappingInfos.add(typeInfo.combine(info));
+ }
+ }
+ infos = requestMappingInfos;
}
- if (info.getPatternsCondition().isEmptyPathMapping()) {
- info = info.mutate().paths("", "/").options(this.config).build();
+ for (RequestMappingInfo info : infos) {
+ if (info.getPatternsCondition().isEmptyPathMapping()) {
+ info = info.mutate().paths("", "/").options(this.config).build();
+ }
+
+ result.add(info);
}
- for (Map.Entry>> entry : this.pathPrefixes.entrySet()) {
- if (entry.getValue().test(handlerType)) {
- String prefix = entry.getKey();
- if (this.embeddedValueResolver != null) {
- prefix = this.embeddedValueResolver.resolveStringValue(prefix);
+
+ for (int idx = 0; idx < result.size(); idx++) {
+ RequestMappingInfo info = result.get(idx);
+
+ for (Map.Entry>> entry : this.pathPrefixes.entrySet()) {
+ if (entry.getValue().test(handlerType)) {
+ String prefix = entry.getKey();
+ if (this.embeddedValueResolver != null) {
+ prefix = this.embeddedValueResolver.resolveStringValue(prefix);
+ }
+ info = RequestMappingInfo.paths(prefix).options(this.config).build().combine(info);
+ result.set(idx, info);
+ break;
}
- info = RequestMappingInfo.paths(prefix).options(this.config).build().combine(info);
- break;
}
}
}
- return info;
+ return Collections.unmodifiableList(result);
}
- @Nullable
- private RequestMappingInfo createRequestMappingInfo(AnnotatedElement element) {
- RequestMappingInfo requestMappingInfo = null;
+ @NonNull
+ private List buildListOfRequestMappingInfo(AnnotatedElement element) {
+ List requestMappingInfos = new ArrayList<>();
RequestCondition> customCondition = (element instanceof Class> clazz ?
getCustomTypeCondition(clazz) : getCustomMethodCondition((Method) element));
@@ -200,22 +219,25 @@ private RequestMappingInfo createRequestMappingInfo(AnnotatedElement element) {
logger.warn("Multiple @RequestMapping annotations found on %s, but only the first will be used: %s"
.formatted(element, requestMappings));
}
- requestMappingInfo = createRequestMappingInfo((RequestMapping) requestMappings.get(0).annotation, customCondition);
+
+ for (AnnotationDescriptor requestMapping : requestMappings) {
+ requestMappingInfos.add(createRequestMappingInfo((RequestMapping) requestMapping.annotation, customCondition));
+ }
}
List httpExchanges = descriptors.stream()
.filter(desc -> desc.annotation instanceof HttpExchange).toList();
if (!httpExchanges.isEmpty()) {
- Assert.state(requestMappingInfo == null,
+ Assert.state(requestMappings.isEmpty(),
() -> "%s is annotated with @RequestMapping and @HttpExchange annotations, but only one is allowed: %s"
.formatted(element, Stream.of(requestMappings, httpExchanges).flatMap(List::stream).toList()));
- Assert.state(httpExchanges.size() == 1,
- () -> "Multiple @HttpExchange annotations found on %s, but only one is allowed: %s"
- .formatted(element, httpExchanges));
- requestMappingInfo = createRequestMappingInfo((HttpExchange) httpExchanges.get(0).annotation, customCondition);
+
+ for (AnnotationDescriptor httpExchange : httpExchanges) {
+ requestMappingInfos.add(createRequestMappingInfo((HttpExchange) httpExchange.annotation, customCondition));
+ }
}
- return requestMappingInfo;
+ return Collections.unmodifiableList(requestMappingInfos);
}
/**
diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/HandlerMethodMappingTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/HandlerMethodMappingTests.java
index 427787cd9bcc..ede217a3611b 100644
--- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/HandlerMethodMappingTests.java
+++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/HandlerMethodMappingTests.java
@@ -25,6 +25,7 @@
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
+import org.springframework.lang.NonNull;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
@@ -200,9 +201,10 @@ protected boolean isHandler(Class> beanType) {
}
@Override
- protected String getMappingForMethod(Method method, Class> handlerType) {
+ @NonNull
+ protected List getListMappingsForMethod(Method method, Class> handlerType) {
String methodName = method.getName();
- return methodName.startsWith("handler") ? methodName : null;
+ return methodName.startsWith("handler") ? Collections.singletonList(methodName) : Collections.emptyList();
}
@Override
diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMappingTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMappingTests.java
index 0ba5b8ef598f..24da9c6276d2 100644
--- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMappingTests.java
+++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMappingTests.java
@@ -29,6 +29,7 @@
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
+import org.springframework.lang.NonNull;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
@@ -523,19 +524,20 @@ protected boolean isHandler(Class> beanType) {
}
@Override
- protected RequestMappingInfo getMappingForMethod(Method method, Class> handlerType) {
+ @NonNull
+ protected List getListMappingsForMethod(Method method, Class> handlerType) {
+ List results = new ArrayList<>();
RequestMapping annot = AnnotatedElementUtils.findMergedAnnotation(method, RequestMapping.class);
if (annot != null) {
BuilderConfiguration options = new BuilderConfiguration();
options.setPatternParser(getPathPatternParser());
- return paths(annot.value()).methods(annot.method())
+ results.add(paths(annot.value()).methods(annot.method())
.params(annot.params()).headers(annot.headers())
.consumes(annot.consumes()).produces(annot.produces())
- .options(options).build();
- }
- else {
- return null;
+ .options(options).build());
}
+
+ return results;
}
}
diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMappingTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMappingTests.java
index 5c04020d5eb4..2912832ef1c7 100644
--- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMappingTests.java
+++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMappingTests.java
@@ -16,18 +16,8 @@
package org.springframework.web.reactive.result.method.annotation;
-import java.lang.annotation.ElementType;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.lang.annotation.Target;
-import java.lang.reflect.Method;
-import java.security.Principal;
-import java.util.Map;
-import java.util.Set;
-
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
-
import org.springframework.core.annotation.AliasFor;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Controller;
@@ -44,13 +34,27 @@
import org.springframework.web.context.support.StaticWebApplicationContext;
import org.springframework.web.method.HandlerTypePredicate;
import org.springframework.web.reactive.result.condition.ConsumesRequestCondition;
+import org.springframework.web.reactive.result.condition.HeadersRequestCondition;
import org.springframework.web.reactive.result.condition.MediaTypeExpression;
+import org.springframework.web.reactive.result.condition.ParamsRequestCondition;
+import org.springframework.web.reactive.result.condition.PatternsRequestCondition;
+import org.springframework.web.reactive.result.condition.ProducesRequestCondition;
+import org.springframework.web.reactive.result.condition.RequestMethodsRequestCondition;
import org.springframework.web.reactive.result.method.RequestMappingInfo;
import org.springframework.web.service.annotation.HttpExchange;
import org.springframework.web.service.annotation.PostExchange;
import org.springframework.web.service.annotation.PutExchange;
import org.springframework.web.util.pattern.PathPattern;
-import org.springframework.web.util.pattern.PathPatternParser;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.lang.reflect.Method;
+import java.security.Principal;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
@@ -92,10 +96,14 @@ void pathPrefix() {
this.handlerMapping.setPathPrefixes(Map.of("/${prefix}", HandlerTypePredicate.forAnnotation(RestController.class)));
Method method = ReflectionUtils.findMethod(UserController.class, "getUser");
- RequestMappingInfo info = this.handlerMapping.getMappingForMethod(method, UserController.class);
+ List infos = this.handlerMapping.getListMappingsForMethod(method, UserController.class);
- assertThat(info).isNotNull();
- assertThat(info.getPatternsCondition().getPatterns()).containsOnly(new PathPatternParser().parse("/api/user/{id}"));
+ assertThat(infos).isNotEmpty();
+ assertThat(infos)
+ .extracting(RequestMappingInfo::getPatternsCondition)
+ .flatExtracting(PatternsRequestCondition::getPatterns)
+ .flatExtracting(PathPattern::getPatternString)
+ .containsOnly("/api/user/{id}");
}
@Test
@@ -162,13 +170,14 @@ void httpExchangeWithMultipleAnnotationsAtClassLevel() {
Class> controllerClass = MultipleClassLevelAnnotationsHttpExchangeController.class;
Method method = ReflectionUtils.findMethod(controllerClass, "post");
- assertThatIllegalStateException()
- .isThrownBy(() -> this.handlerMapping.getMappingForMethod(method, controllerClass))
- .withMessageContainingAll(
- "Multiple @HttpExchange annotations found on " + controllerClass,
- HttpExchange.class.getSimpleName(),
- ExtraHttpExchange.class.getSimpleName()
- );
+ List infos = this.handlerMapping.getListMappingsForMethod(method, controllerClass);
+
+ assertThat(infos.size()).isEqualTo(2);
+ assertThat(infos)
+ .extracting(RequestMappingInfo::getPatternsCondition)
+ .flatExtracting(PatternsRequestCondition::getPatterns)
+ .flatExtracting(PathPattern::getPatternString)
+ .containsOnly("/exchange/post", "/post");
}
@Test // gh-32049
@@ -178,13 +187,13 @@ void httpExchangeWithMultipleAnnotationsAtMethodLevel() {
Class> controllerClass = MultipleMethodLevelAnnotationsHttpExchangeController.class;
Method method = ReflectionUtils.findMethod(controllerClass, "post");
- assertThatIllegalStateException()
- .isThrownBy(() -> this.handlerMapping.getMappingForMethod(method, controllerClass))
- .withMessageContainingAll(
- "Multiple @HttpExchange annotations found on " + method,
- PostExchange.class.getSimpleName(),
- PutExchange.class.getSimpleName()
- );
+ List mappingInfos = this.handlerMapping.getListMappingsForMethod(method, controllerClass);
+
+ assertThat(mappingInfos.size()).isEqualTo(2);
+ assertThat(mappingInfos)
+ .extracting(RequestMappingInfo::getMethodsCondition)
+ .flatExtracting(RequestMethodsRequestCondition::getMethods)
+ .containsOnly(RequestMethod.POST, RequestMethod.PUT);
}
@Test // gh-32065
@@ -195,7 +204,7 @@ void httpExchangeWithMixedAnnotationsAtClassLevel() {
Method method = ReflectionUtils.findMethod(controllerClass, "post");
assertThatIllegalStateException()
- .isThrownBy(() -> this.handlerMapping.getMappingForMethod(method, controllerClass))
+ .isThrownBy(() -> this.handlerMapping.getListMappingsForMethod(method, controllerClass))
.withMessageContainingAll(
controllerClass.getName(),
"is annotated with @RequestMapping and @HttpExchange annotations, but only one is allowed:",
@@ -212,7 +221,7 @@ void httpExchangeWithMixedAnnotationsAtMethodLevel() {
Method method = ReflectionUtils.findMethod(controllerClass, "post");
assertThatIllegalStateException()
- .isThrownBy(() -> this.handlerMapping.getMappingForMethod(method, controllerClass))
+ .isThrownBy(() -> this.handlerMapping.getListMappingsForMethod(method, controllerClass))
.withMessageContainingAll(
method.toString(),
"is annotated with @RequestMapping and @HttpExchange annotations, but only one is allowed:",
@@ -228,12 +237,16 @@ void httpExchangeAnnotationsOverriddenAtClassLevel() {
Class> controllerClass = ClassLevelOverriddenHttpExchangeAnnotationsController.class;
Method method = ReflectionUtils.findMethod(controllerClass, "post");
- RequestMappingInfo info = this.handlerMapping.getMappingForMethod(method, controllerClass);
+ List infos = this.handlerMapping.getListMappingsForMethod(method, controllerClass);
- assertThat(info).isNotNull();
- assertThat(info.getPatternsCondition()).isNotNull();
- assertThat(info.getPatternsCondition().getPatterns())
- .extracting(PathPattern::getPatternString)
+ assertThat(infos).isNotEmpty();
+ assertThat(infos)
+ .extracting(RequestMappingInfo::getPatternsCondition)
+ .isNotNull();
+ assertThat(infos)
+ .extracting(RequestMappingInfo::getPatternsCondition)
+ .flatExtracting(PatternsRequestCondition::getPatterns)
+ .flatExtracting(PathPattern::getPatternString)
.containsOnly("/controller/postExchange");
}
@@ -244,12 +257,16 @@ void httpExchangeAnnotationsOverriddenAtMethodLevel() {
Class> controllerClass = MethodLevelOverriddenHttpExchangeAnnotationsController.class;
Method method = ReflectionUtils.findMethod(controllerClass, "post");
- RequestMappingInfo info = this.handlerMapping.getMappingForMethod(method, controllerClass);
+ List infos = this.handlerMapping.getListMappingsForMethod(method, controllerClass);
- assertThat(info).isNotNull();
- assertThat(info.getPatternsCondition()).isNotNull();
- assertThat(info.getPatternsCondition().getPatterns())
- .extracting(PathPattern::getPatternString)
+ assertThat(infos).isNotEmpty();
+ assertThat(infos)
+ .extracting(RequestMappingInfo::getPatternsCondition)
+ .isNotNull();
+ assertThat(infos)
+ .extracting(RequestMappingInfo::getPatternsCondition)
+ .flatExtracting(PatternsRequestCondition::getPatterns)
+ .flatExtracting(PathPattern::getPatternString)
.containsOnly("/controller/postMapping");
}
@@ -260,17 +277,34 @@ void httpExchangeWithDefaultValues() {
Class> clazz = HttpExchangeController.class;
Method method = ReflectionUtils.findMethod(clazz, "defaultValuesExchange");
- RequestMappingInfo mappingInfo = this.handlerMapping.getMappingForMethod(method, clazz);
+ List mappingInfos = this.handlerMapping.getListMappingsForMethod(method, clazz);
- assertThat(mappingInfo.getPatternsCondition().getPatterns())
+ assertThat(mappingInfos)
+ .extracting(RequestMappingInfo::getPatternsCondition)
+ .flatExtracting(PatternsRequestCondition::getPatterns)
.extracting(PathPattern::toString)
.containsOnly("/exchange");
- assertThat(mappingInfo.getMethodsCondition().getMethods()).isEmpty();
- assertThat(mappingInfo.getParamsCondition().getExpressions()).isEmpty();
- assertThat(mappingInfo.getHeadersCondition().getExpressions()).isEmpty();
- assertThat(mappingInfo.getConsumesCondition().getExpressions()).isEmpty();
- assertThat(mappingInfo.getProducesCondition().getExpressions()).isEmpty();
+ assertThat(mappingInfos)
+ .extracting(RequestMappingInfo::getMethodsCondition)
+ .flatExtracting(RequestMethodsRequestCondition::getMethods)
+ .isEmpty();
+ assertThat(mappingInfos)
+ .extracting(RequestMappingInfo::getParamsCondition)
+ .flatExtracting(ParamsRequestCondition::getExpressions)
+ .isEmpty();
+ assertThat(mappingInfos)
+ .extracting(RequestMappingInfo::getHeadersCondition)
+ .flatExtracting(HeadersRequestCondition::getExpressions)
+ .isEmpty();
+ assertThat(mappingInfos)
+ .extracting(RequestMappingInfo::getConsumesCondition)
+ .flatExtracting(ConsumesRequestCondition::getExpressions)
+ .isEmpty();
+ assertThat(mappingInfos)
+ .extracting(RequestMappingInfo::getProducesCondition)
+ .flatExtracting(ProducesRequestCondition::getExpressions)
+ .isEmpty();
}
@SuppressWarnings("DataFlowIssue")
@@ -284,21 +318,36 @@ void httpExchangeWithCustomValues() {
Class clazz = HttpExchangeController.class;
Method method = ReflectionUtils.findMethod(clazz, "customValuesExchange");
- RequestMappingInfo mappingInfo = mapping.getMappingForMethod(method, clazz);
+ List mappingInfos = mapping.getListMappingsForMethod(method, clazz);
- assertThat(mappingInfo.getPatternsCondition().getPatterns())
+ assertThat(mappingInfos)
+ .extracting(RequestMappingInfo::getPatternsCondition)
+ .flatExtracting(PatternsRequestCondition::getPatterns)
.extracting(PathPattern::toString)
.containsOnly("/exchange/custom");
- assertThat(mappingInfo.getMethodsCondition().getMethods()).containsOnly(RequestMethod.POST);
- assertThat(mappingInfo.getParamsCondition().getExpressions()).isEmpty();
- assertThat(mappingInfo.getHeadersCondition().getExpressions()).isEmpty();
-
- assertThat(mappingInfo.getConsumesCondition().getExpressions())
+ assertThat(mappingInfos)
+ .extracting(RequestMappingInfo::getMethodsCondition)
+ .flatExtracting(RequestMethodsRequestCondition::getMethods)
+ .containsOnly(RequestMethod.POST);
+ assertThat(mappingInfos)
+ .extracting(RequestMappingInfo::getParamsCondition)
+ .flatExtracting(ParamsRequestCondition::getExpressions)
+ .isEmpty();
+ assertThat(mappingInfos)
+ .extracting(RequestMappingInfo::getHeadersCondition)
+ .flatExtracting(HeadersRequestCondition::getExpressions)
+ .isEmpty();
+
+ assertThat(mappingInfos)
+ .extracting(RequestMappingInfo::getConsumesCondition)
+ .flatExtracting(ConsumesRequestCondition::getExpressions)
.extracting(MediaTypeExpression::getMediaType)
.containsOnly(MediaType.APPLICATION_JSON);
- assertThat(mappingInfo.getProducesCondition().getExpressions())
+ assertThat(mappingInfos)
+ .extracting(RequestMappingInfo::getProducesCondition)
+ .flatExtracting(ProducesRequestCondition::getExpressions)
.extracting(MediaTypeExpression::getMediaType)
.containsOnly(MediaType.valueOf("text/plain;charset=UTF-8"));
}
@@ -314,17 +363,24 @@ private RequestMappingInfo assertComposedAnnotationMapping(
Class> clazz = ComposedAnnotationController.class;
Method method = ClassUtils.getMethod(clazz, methodName, (Class>[]) null);
- RequestMappingInfo info = this.handlerMapping.getMappingForMethod(method, clazz);
-
- assertThat(info).isNotNull();
- assertThat(info.getPatternsCondition()).isNotNull();
- assertThat(info.getPatternsCondition().getPatterns())
- .extracting(PathPattern::getPatternString)
+ List infos = this.handlerMapping.getListMappingsForMethod(method, clazz);
+
+ assertThat(infos).isNotEmpty();
+ assertThat(infos)
+ .extracting(RequestMappingInfo::getPatternsCondition)
+ .isNotEmpty();
+ assertThat(infos)
+ .extracting(RequestMappingInfo::getPatternsCondition)
+ .flatExtracting(PatternsRequestCondition::getPatterns)
+ .flatExtracting(PathPattern::getPatternString)
.containsOnly(path);
- assertThat(info.getMethodsCondition().getMethods()).containsOnly(requestMethod);
+ assertThat(infos)
+ .extracting(RequestMappingInfo::getMethodsCondition)
+ .flatExtracting(RequestMethodsRequestCondition::getMethods)
+ .containsOnly(requestMethod);
- return info;
+ return infos.get(0);
}
@@ -350,10 +406,7 @@ public void get() {
public void post(@RequestBody(required = false) Foo foo) {
}
- // gh-31962: The presence of multiple @RequestMappings is intentional.
- @PatchMapping("/put")
@RequestMapping(path = "/put", method = RequestMethod.PUT) // local @RequestMapping overrides meta-annotations
- @PostMapping("/put")
public void put() {
}