From e8b784fa8406eae8ddad52756615dabc9ec1ede2 Mon Sep 17 00:00:00 2001 From: Youssef Awichawi Date: Fri, 26 Jan 2024 13:44:33 +0100 Subject: [PATCH] Support multiple RequestMapping and HttpExchange annotations --- .../web/bind/annotation/RequestMapping.java | 2 + .../web/bind/annotation/RequestMappings.java | 25 +++ .../method/AbstractHandlerMethodMapping.java | 23 ++- .../RequestMappingHandlerMapping.java | 82 +++++--- .../method/HandlerMethodMappingTests.java | 6 +- ...RequestMappingInfoHandlerMappingTests.java | 14 +- .../RequestMappingHandlerMappingTests.java | 185 +++++++++++------- 7 files changed, 223 insertions(+), 114 deletions(-) create mode 100644 spring-web/src/main/java/org/springframework/web/bind/annotation/RequestMappings.java 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() { }