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 82a629db860..20bf3ea917e 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 @@ -200,9 +200,13 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi return createRequestMappingInfo(requestMappings.get(0).annotation, customCondition); } - HttpExchange httpExchange = AnnotatedElementUtils.findMergedAnnotation(element, HttpExchange.class); - if (httpExchange != null) { - return createRequestMappingInfo(httpExchange, customCondition); + List> httpExchanges = getAnnotationDescriptors( + mergedAnnotations, HttpExchange.class); + if (!httpExchanges.isEmpty()) { + Assert.state(httpExchanges.size() == 1, + () -> "Multiple @HttpExchange annotations found on %s, but only one is allowed: %s" + .formatted(element, httpExchanges)); + return createRequestMappingInfo(httpExchanges.get(0).annotation, customCondition); } return null; 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 cc456deea3f..aebe253414e 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 @@ -48,10 +48,12 @@ import org.springframework.web.reactive.result.condition.PatternsRequestConditio 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 static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; import static org.mockito.Mockito.mock; /** @@ -154,6 +156,38 @@ class RequestMappingHandlerMappingTests { assertComposedAnnotationMapping(RequestMethod.PATCH); } + @Test // gh-32049 + void httpExchangeWithMultipleAnnotationsAtClassLevel() throws NoSuchMethodException { + this.handlerMapping.afterPropertiesSet(); + + Class controllerClass = MultipleClassLevelAnnotationsHttpExchangeController.class; + Method method = controllerClass.getDeclaredMethod("post"); + + assertThatIllegalStateException() + .isThrownBy(() -> this.handlerMapping.getMappingForMethod(method, controllerClass)) + .withMessageContainingAll( + "Multiple @HttpExchange annotations found on " + controllerClass, + "@" + HttpExchange.class.getName(), + "@" + ExtraHttpExchange.class.getName() + ); + } + + @Test // gh-32049 + void httpExchangeWithMultipleAnnotationsAtMethodLevel() throws NoSuchMethodException { + this.handlerMapping.afterPropertiesSet(); + + Class controllerClass = MultipleMethodLevelAnnotationsHttpExchangeController.class; + Method method = controllerClass.getDeclaredMethod("post"); + + assertThatIllegalStateException() + .isThrownBy(() -> this.handlerMapping.getMappingForMethod(method, controllerClass)) + .withMessageContainingAll( + "Multiple @HttpExchange annotations found on " + method, + "@" + PostExchange.class.getName(), + "@" + PutExchange.class.getName() + ); + } + @SuppressWarnings("DataFlowIssue") @Test void httpExchangeWithDefaultValues() throws NoSuchMethodException { @@ -313,4 +347,27 @@ class RequestMappingHandlerMappingTests { public void customValuesExchange(){} } + @HttpExchange("/exchange") + @ExtraHttpExchange + static class MultipleClassLevelAnnotationsHttpExchangeController { + + @PostExchange("/post") + void post() {} + } + + + static class MultipleMethodLevelAnnotationsHttpExchangeController { + + @PostExchange("/post") + @PutExchange("/post") + void post() {} + } + + + @HttpExchange + @Target(ElementType.TYPE) + @Retention(RetentionPolicy.RUNTIME) + @interface ExtraHttpExchange { + } + } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java index 0a54307e878..cde09327f67 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java @@ -360,9 +360,13 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi return createRequestMappingInfo(requestMappings.get(0).annotation, customCondition); } - HttpExchange httpExchange = AnnotatedElementUtils.findMergedAnnotation(element, HttpExchange.class); - if (httpExchange != null) { - return createRequestMappingInfo(httpExchange, customCondition); + List> httpExchanges = getAnnotationDescriptors( + mergedAnnotations, HttpExchange.class); + if (!httpExchanges.isEmpty()) { + Assert.state(httpExchanges.size() == 1, + () -> "Multiple @HttpExchange annotations found on %s, but only one is allowed: %s" + .formatted(element, httpExchanges)); + return createRequestMappingInfo(httpExchanges.get(0).annotation, customCondition); } return null; diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMappingTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMappingTests.java index 7e20d68a2d9..51ba6a27a38 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMappingTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMappingTests.java @@ -48,6 +48,7 @@ import org.springframework.web.context.support.StaticWebApplicationContext; import org.springframework.web.method.HandlerTypePredicate; 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.servlet.handler.PathPatternsParameterizedTest; import org.springframework.web.servlet.mvc.condition.ConsumesRequestCondition; import org.springframework.web.servlet.mvc.condition.MediaTypeExpression; @@ -58,6 +59,7 @@ import org.springframework.web.util.pattern.PathPattern; import org.springframework.web.util.pattern.PathPatternParser; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; import static org.mockito.Mockito.mock; /** @@ -276,6 +278,38 @@ class RequestMappingHandlerMappingTests { assertComposedAnnotationMapping(RequestMethod.PATCH); } + @Test // gh-32049 + void httpExchangeWithMultipleAnnotationsAtClassLevel() throws NoSuchMethodException { + RequestMappingHandlerMapping mapping = createMapping(); + + Class controllerClass = MultipleClassLevelAnnotationsHttpExchangeController.class; + Method method = controllerClass.getDeclaredMethod("post"); + + assertThatIllegalStateException() + .isThrownBy(() -> mapping.getMappingForMethod(method, controllerClass)) + .withMessageContainingAll( + "Multiple @HttpExchange annotations found on " + controllerClass, + "@" + HttpExchange.class.getName(), + "@" + ExtraHttpExchange.class.getName() + ); + } + + @Test // gh-32049 + void httpExchangeWithMultipleAnnotationsAtMethodLevel() throws NoSuchMethodException { + RequestMappingHandlerMapping mapping = createMapping(); + + Class controllerClass = MultipleMethodLevelAnnotationsHttpExchangeController.class; + Method method = controllerClass.getDeclaredMethod("post"); + + assertThatIllegalStateException() + .isThrownBy(() -> mapping.getMappingForMethod(method, controllerClass)) + .withMessageContainingAll( + "Multiple @HttpExchange annotations found on " + method, + "@" + PostExchange.class.getName(), + "@" + PutExchange.class.getName() + ); + } + @SuppressWarnings("DataFlowIssue") @Test void httpExchangeWithDefaultValues() throws NoSuchMethodException { @@ -437,6 +471,30 @@ class RequestMappingHandlerMappingTests { } + @HttpExchange("/exchange") + @ExtraHttpExchange + static class MultipleClassLevelAnnotationsHttpExchangeController { + + @PostExchange("/post") + void post() {} + } + + + static class MultipleMethodLevelAnnotationsHttpExchangeController { + + @PostExchange("/post") + @PutExchange("/post") + void post() {} + } + + + @HttpExchange + @Target(ElementType.TYPE) + @Retention(RetentionPolicy.RUNTIME) + @interface ExtraHttpExchange { + } + + private static class Foo { }