Reject multiple @⁠HttpExchange declarations in MVC and WebFlux

This commit updates the RequestMappingHandlerMapping implementations in
Spring MVC and Spring WebFlux so that multiple @⁠HttpExchange
declarations on the same element are rejected.

Closes gh-32049
This commit is contained in:
Sam Brannen
2024-01-18 15:27:12 +01:00
parent c5c77b93fe
commit b8b31ff8a1
4 changed files with 129 additions and 6 deletions

View File

@@ -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<AnnotationDescriptor<HttpExchange>> 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;

View File

@@ -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 {
}
}

View File

@@ -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<AnnotationDescriptor<HttpExchange>> 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;

View File

@@ -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 {
}