Log warning if multiple @⁠RequestMapping annotations are declared

If multiple request mapping annotations are discovered, Spring MVC and
Spring WebFlux now log a warning similar to the following (without
newlines).

Multiple @⁠RequestMapping annotations found on
void org.example.MyController.put(), but only the first will be used:
[
@⁠org.springframework.web.bind.annotation.PutMapping(consumes={}, headers={}, name="", params={}, path={"/put"}, produces={}, value={"/put"}),
@⁠org.springframework.web.bind.annotation.PostMapping(consumes={}, headers={}, name="", params={}, path={"/put"}, produces={}, value={"/put"})
]

Closes gh-31962
This commit is contained in:
Sam Brannen
2024-01-17 17:44:38 +01:00
parent 5bf74cae11
commit 699da7c383
12 changed files with 191 additions and 17 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 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.
@@ -16,18 +16,23 @@
package org.springframework.web.reactive.result.method.annotation;
import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Predicate;
import org.springframework.context.EmbeddedValueResolverAware;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.annotation.MergedAnnotation;
import org.springframework.core.annotation.MergedAnnotationPredicates;
import org.springframework.core.annotation.MergedAnnotations;
import org.springframework.core.annotation.MergedAnnotations.SearchStrategy;
import org.springframework.core.annotation.RepeatableContainers;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Controller;
import org.springframework.util.Assert;
@@ -182,9 +187,20 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi
RequestCondition<?> customCondition = (element instanceof Class<?> clazz ?
getCustomTypeCondition(clazz) : getCustomMethodCondition((Method) element));
RequestMapping requestMapping = AnnotatedElementUtils.findMergedAnnotation(element, RequestMapping.class);
if (requestMapping != null) {
return createRequestMappingInfo(requestMapping, customCondition);
MergedAnnotations mergedAnnotations = MergedAnnotations.from(element, SearchStrategy.TYPE_HIERARCHY,
RepeatableContainers.none());
List<AnnotationDescriptor<RequestMapping>> requestMappings = mergedAnnotations.stream(RequestMapping.class)
.filter(MergedAnnotationPredicates.firstRunOf(MergedAnnotation::getAggregateIndex))
.map(AnnotationDescriptor::new)
.distinct()
.toList();
if (!requestMappings.isEmpty()) {
if (requestMappings.size() > 1 && logger.isWarnEnabled()) {
logger.warn("Multiple @RequestMapping annotations found on %s, but only the first will be used: %s"
.formatted(element, requestMappings));
}
return createRequestMappingInfo(requestMappings.get(0).annotation, customCondition);
}
HttpExchange httpExchange = AnnotatedElementUtils.findMergedAnnotation(element, HttpExchange.class);
@@ -414,4 +430,32 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi
}
}
private static class AnnotationDescriptor<A extends Annotation> {
private final A annotation;
private final Annotation source;
AnnotationDescriptor(MergedAnnotation<A> mergedAnnotation) {
this.annotation = mergedAnnotation.synthesize();
this.source = (mergedAnnotation.getDistance() > 0 ?
mergedAnnotation.getRoot().synthesize() : this.annotation);
}
@Override
public boolean equals(Object obj) {
return (obj instanceof AnnotationDescriptor<?> that && this.annotation.equals(that.annotation));
}
@Override
public int hashCode() {
return this.annotation.hashCode();
}
@Override
public String toString() {
return this.source.toString();
}
}
}

View File

@@ -36,7 +36,6 @@ import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
@@ -60,6 +59,7 @@ import static org.mockito.Mockito.mock;
*
* @author Rossen Stoyanchev
* @author Olga Maciaszek-Sharma
* @author Sam Brannen
*/
class RequestMappingHandlerMappingTests {
@@ -231,7 +231,9 @@ class RequestMappingHandlerMappingTests {
@Controller @SuppressWarnings("unused")
// gh-31962: The presence of multiple @RequestMappings is intentional.
@RequestMapping(consumes = MediaType.APPLICATION_JSON_VALUE)
@ExtraRequestMapping
static class ComposedAnnotationController {
@RequestMapping
@@ -250,7 +252,10 @@ class RequestMappingHandlerMappingTests {
public void post(@RequestBody(required = false) Foo foo) {
}
@PutMapping("/put")
// 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() {
}
@@ -267,6 +272,13 @@ class RequestMappingHandlerMappingTests {
}
@RequestMapping
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@interface ExtraRequestMapping {
}
@RequestMapping(method = RequestMethod.POST,
produces = MediaType.APPLICATION_JSON_VALUE,
consumes = MediaType.APPLICATION_JSON_VALUE)