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

@ -28,6 +28,12 @@ because, arguably, most controller methods should be mapped to a specific HTTP m
using `@RequestMapping`, which, by default, matches to all HTTP methods. At the same time, a using `@RequestMapping`, which, by default, matches to all HTTP methods. At the same time, a
`@RequestMapping` is still needed at the class level to express shared mappings. `@RequestMapping` is still needed at the class level to express shared mappings.
NOTE: `@RequestMapping` cannot be used in conjunction with other `@RequestMapping`
annotations that are declared on the same element (class, interface, or method). If
multiple `@RequestMapping` annotations are detected on the same element, a warning will
be logged, and only the first mapping will be used. This also applies to composed
`@RequestMapping` annotations such as `@GetMapping`, `@PostMapping`, etc.
The following example uses type and method level mappings: The following example uses type and method level mappings:
[tabs] [tabs]
@ -439,6 +445,12 @@ controller methods should be mapped to a specific HTTP method versus using `@Req
which, by default, matches to all HTTP methods. If you need an example of how to implement which, by default, matches to all HTTP methods. If you need an example of how to implement
a composed annotation, look at how those are declared. a composed annotation, look at how those are declared.
NOTE: `@RequestMapping` cannot be used in conjunction with other `@RequestMapping`
annotations that are declared on the same element (class, interface, or method). If
multiple `@RequestMapping` annotations are detected on the same element, a warning will
be logged, and only the first mapping will be used. This also applies to composed
`@RequestMapping` annotations such as `@GetMapping`, `@PostMapping`, etc.
Spring WebFlux also supports custom request mapping attributes with custom request matching Spring WebFlux also supports custom request mapping attributes with custom request matching
logic. This is a more advanced option that requires sub-classing logic. This is a more advanced option that requires sub-classing
`RequestMappingHandlerMapping` and overriding the `getCustomMethodCondition` method, where `RequestMappingHandlerMapping` and overriding the `getCustomMethodCondition` method, where

View File

@ -30,6 +30,12 @@ arguably, most controller methods should be mapped to a specific HTTP method ver
using `@RequestMapping`, which, by default, matches to all HTTP methods. using `@RequestMapping`, which, by default, matches to all HTTP methods.
A `@RequestMapping` is still needed at the class level to express shared mappings. A `@RequestMapping` is still needed at the class level to express shared mappings.
NOTE: `@RequestMapping` cannot be used in conjunction with other `@RequestMapping`
annotations that are declared on the same element (class, interface, or method). If
multiple `@RequestMapping` annotations are detected on the same element, a warning will
be logged, and only the first mapping will be used. This also applies to composed
`@RequestMapping` annotations such as `@GetMapping`, `@PostMapping`, etc.
The following example has type and method level mappings: The following example has type and method level mappings:
[tabs] [tabs]
@ -489,6 +495,12 @@ controller methods should be mapped to a specific HTTP method versus using `@Req
which, by default, matches to all HTTP methods. If you need an example of how to implement which, by default, matches to all HTTP methods. If you need an example of how to implement
a composed annotation, look at how those are declared. a composed annotation, look at how those are declared.
NOTE: `@RequestMapping` cannot be used in conjunction with other `@RequestMapping`
annotations that are declared on the same element (class, interface, or method). If
multiple `@RequestMapping` annotations are detected on the same element, a warning will
be logged, and only the first mapping will be used. This also applies to composed
`@RequestMapping` annotations such as `@GetMapping`, `@PostMapping`, etc.
Spring MVC also supports custom request-mapping attributes with custom request-matching Spring MVC also supports custom request-mapping attributes with custom request-matching
logic. This is a more advanced option that requires subclassing logic. This is a more advanced option that requires subclassing
`RequestMappingHandlerMapping` and overriding the `getCustomMethodCondition` method, where `RequestMappingHandlerMapping` and overriding the `getCustomMethodCondition` method, where

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2016 the original author or authors. * Copyright 2002-2024 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -31,6 +31,13 @@ import org.springframework.core.annotation.AliasFor;
* <p>Specifically, {@code @DeleteMapping} is a <em>composed annotation</em> that * <p>Specifically, {@code @DeleteMapping} is a <em>composed annotation</em> that
* acts as a shortcut for {@code @RequestMapping(method = RequestMethod.DELETE)}. * acts as a shortcut for {@code @RequestMapping(method = RequestMethod.DELETE)}.
* *
* <p><strong>NOTE:</strong> This annotation cannot be used in conjunction with
* other {@code @RequestMapping} annotations that are declared on the same method.
* If multiple {@code @RequestMapping} annotations are detected on the same method,
* a warning will be logged, and only the first mapping will be used. This applies
* to {@code @RequestMapping} as well as composed {@code @RequestMapping} annotations
* such as {@code @GetMapping}, {@code @PostMapping}, etc.
*
* @author Sam Brannen * @author Sam Brannen
* @since 4.3 * @since 4.3
* @see GetMapping * @see GetMapping

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2016 the original author or authors. * Copyright 2002-2024 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -31,6 +31,13 @@ import org.springframework.core.annotation.AliasFor;
* <p>Specifically, {@code @GetMapping} is a <em>composed annotation</em> that * <p>Specifically, {@code @GetMapping} is a <em>composed annotation</em> that
* acts as a shortcut for {@code @RequestMapping(method = RequestMethod.GET)}. * acts as a shortcut for {@code @RequestMapping(method = RequestMethod.GET)}.
* *
* <p><strong>NOTE:</strong> This annotation cannot be used in conjunction with
* other {@code @RequestMapping} annotations that are declared on the same method.
* If multiple {@code @RequestMapping} annotations are detected on the same method,
* a warning will be logged, and only the first mapping will be used. This applies
* to {@code @RequestMapping} as well as composed {@code @RequestMapping} annotations
* such as {@code @PutMapping}, {@code @PostMapping}, etc.
*
* @author Sam Brannen * @author Sam Brannen
* @since 4.3 * @since 4.3
* @see PostMapping * @see PostMapping

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2016 the original author or authors. * Copyright 2002-2024 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -31,6 +31,13 @@ import org.springframework.core.annotation.AliasFor;
* <p>Specifically, {@code @PatchMapping} is a <em>composed annotation</em> that * <p>Specifically, {@code @PatchMapping} is a <em>composed annotation</em> that
* acts as a shortcut for {@code @RequestMapping(method = RequestMethod.PATCH)}. * acts as a shortcut for {@code @RequestMapping(method = RequestMethod.PATCH)}.
* *
* <p><strong>NOTE:</strong> This annotation cannot be used in conjunction with
* other {@code @RequestMapping} annotations that are declared on the same method.
* If multiple {@code @RequestMapping} annotations are detected on the same method,
* a warning will be logged, and only the first mapping will be used. This applies
* to {@code @RequestMapping} as well as composed {@code @RequestMapping} annotations
* such as {@code @GetMapping}, {@code @PostMapping}, etc.
*
* @author Sam Brannen * @author Sam Brannen
* @since 4.3 * @since 4.3
* @see GetMapping * @see GetMapping

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2016 the original author or authors. * Copyright 2002-2024 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -31,6 +31,13 @@ import org.springframework.core.annotation.AliasFor;
* <p>Specifically, {@code @PostMapping} is a <em>composed annotation</em> that * <p>Specifically, {@code @PostMapping} is a <em>composed annotation</em> that
* acts as a shortcut for {@code @RequestMapping(method = RequestMethod.POST)}. * acts as a shortcut for {@code @RequestMapping(method = RequestMethod.POST)}.
* *
* <p><strong>NOTE:</strong> This annotation cannot be used in conjunction with
* other {@code @RequestMapping} annotations that are declared on the same method.
* If multiple {@code @RequestMapping} annotations are detected on the same method,
* a warning will be logged, and only the first mapping will be used. This applies
* to {@code @RequestMapping} as well as composed {@code @RequestMapping} annotations
* such as {@code @GetMapping}, {@code @PutMapping}, etc.
*
* @author Sam Brannen * @author Sam Brannen
* @since 4.3 * @since 4.3
* @see GetMapping * @see GetMapping

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2016 the original author or authors. * Copyright 2002-2024 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -31,6 +31,13 @@ import org.springframework.core.annotation.AliasFor;
* <p>Specifically, {@code @PutMapping} is a <em>composed annotation</em> that * <p>Specifically, {@code @PutMapping} is a <em>composed annotation</em> that
* acts as a shortcut for {@code @RequestMapping(method = RequestMethod.PUT)}. * acts as a shortcut for {@code @RequestMapping(method = RequestMethod.PUT)}.
* *
* <p><strong>NOTE:</strong> This annotation cannot be used in conjunction with
* other {@code @RequestMapping} annotations that are declared on the same method.
* If multiple {@code @RequestMapping} annotations are detected on the same method,
* a warning will be logged, and only the first mapping will be used. This applies
* to {@code @RequestMapping} as well as composed {@code @RequestMapping} annotations
* such as {@code @GetMapping}, {@code @PostMapping}, etc.
*
* @author Sam Brannen * @author Sam Brannen
* @since 4.3 * @since 4.3
* @see GetMapping * @see GetMapping

View File

@ -54,6 +54,13 @@ import org.springframework.core.annotation.AliasFor;
* {@link PutMapping @PutMapping}, {@link DeleteMapping @DeleteMapping}, or * {@link PutMapping @PutMapping}, {@link DeleteMapping @DeleteMapping}, or
* {@link PatchMapping @PatchMapping}. * {@link PatchMapping @PatchMapping}.
* *
* <p><strong>NOTE:</strong> This annotation cannot be used in conjunction with
* other {@code @RequestMapping} annotations that are declared on the same element
* (class, interface, or method). If multiple {@code @RequestMapping} annotations
* are detected on the same element, a warning will be logged, and only the first
* mapping will be used. This also applies to composed {@code @RequestMapping}
* annotations such as {@code @GetMapping}, {@code @PostMapping}, etc.
*
* <p><b>NOTE:</b> When using controller interfaces (e.g. for AOP proxying), * <p><b>NOTE:</b> When using controller interfaces (e.g. for AOP proxying),
* make sure to consistently put <i>all</i> your mapping annotations &mdash; such * make sure to consistently put <i>all</i> your mapping annotations &mdash; such
* as {@code @RequestMapping} and {@code @SessionAttributes} &mdash; on * as {@code @RequestMapping} and {@code @SessionAttributes} &mdash; on

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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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; package org.springframework.web.reactive.result.method.annotation;
import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement; import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.lang.reflect.Parameter; import java.lang.reflect.Parameter;
import java.util.Collections; import java.util.Collections;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.function.Predicate; import java.util.function.Predicate;
import org.springframework.context.EmbeddedValueResolverAware; import org.springframework.context.EmbeddedValueResolverAware;
import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.annotation.MergedAnnotation; import org.springframework.core.annotation.MergedAnnotation;
import org.springframework.core.annotation.MergedAnnotationPredicates;
import org.springframework.core.annotation.MergedAnnotations; 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.lang.Nullable;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.util.Assert; import org.springframework.util.Assert;
@ -182,9 +187,20 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi
RequestCondition<?> customCondition = (element instanceof Class<?> clazz ? RequestCondition<?> customCondition = (element instanceof Class<?> clazz ?
getCustomTypeCondition(clazz) : getCustomMethodCondition((Method) element)); getCustomTypeCondition(clazz) : getCustomMethodCondition((Method) element));
RequestMapping requestMapping = AnnotatedElementUtils.findMergedAnnotation(element, RequestMapping.class); MergedAnnotations mergedAnnotations = MergedAnnotations.from(element, SearchStrategy.TYPE_HIERARCHY,
if (requestMapping != null) { RepeatableContainers.none());
return createRequestMappingInfo(requestMapping, customCondition); 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); 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.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PostMapping; 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.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestMethod;
@ -60,6 +59,7 @@ import static org.mockito.Mockito.mock;
* *
* @author Rossen Stoyanchev * @author Rossen Stoyanchev
* @author Olga Maciaszek-Sharma * @author Olga Maciaszek-Sharma
* @author Sam Brannen
*/ */
class RequestMappingHandlerMappingTests { class RequestMappingHandlerMappingTests {
@ -231,7 +231,9 @@ class RequestMappingHandlerMappingTests {
@Controller @SuppressWarnings("unused") @Controller @SuppressWarnings("unused")
// gh-31962: The presence of multiple @RequestMappings is intentional.
@RequestMapping(consumes = MediaType.APPLICATION_JSON_VALUE) @RequestMapping(consumes = MediaType.APPLICATION_JSON_VALUE)
@ExtraRequestMapping
static class ComposedAnnotationController { static class ComposedAnnotationController {
@RequestMapping @RequestMapping
@ -250,7 +252,10 @@ class RequestMappingHandlerMappingTests {
public void post(@RequestBody(required = false) Foo foo) { 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() { public void put() {
} }
@ -267,6 +272,13 @@ class RequestMappingHandlerMappingTests {
} }
@RequestMapping
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@interface ExtraRequestMapping {
}
@RequestMapping(method = RequestMethod.POST, @RequestMapping(method = RequestMethod.POST,
produces = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE,
consumes = MediaType.APPLICATION_JSON_VALUE) consumes = MediaType.APPLICATION_JSON_VALUE)

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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -16,6 +16,7 @@
package org.springframework.web.servlet.mvc.method.annotation; package org.springframework.web.servlet.mvc.method.annotation;
import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement; import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.lang.reflect.Parameter; import java.lang.reflect.Parameter;
@ -30,7 +31,10 @@ import jakarta.servlet.http.HttpServletRequest;
import org.springframework.context.EmbeddedValueResolverAware; import org.springframework.context.EmbeddedValueResolverAware;
import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.annotation.MergedAnnotation; import org.springframework.core.annotation.MergedAnnotation;
import org.springframework.core.annotation.MergedAnnotationPredicates;
import org.springframework.core.annotation.MergedAnnotations; 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.lang.Nullable;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.util.Assert; import org.springframework.util.Assert;
@ -343,9 +347,20 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi
RequestCondition<?> customCondition = (element instanceof Class<?> clazz ? RequestCondition<?> customCondition = (element instanceof Class<?> clazz ?
getCustomTypeCondition(clazz) : getCustomMethodCondition((Method) element)); getCustomTypeCondition(clazz) : getCustomMethodCondition((Method) element));
RequestMapping requestMapping = AnnotatedElementUtils.findMergedAnnotation(element, RequestMapping.class); MergedAnnotations mergedAnnotations = MergedAnnotations.from(element, SearchStrategy.TYPE_HIERARCHY,
if (requestMapping != null) { RepeatableContainers.none());
return createRequestMappingInfo(requestMapping, customCondition); 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); HttpExchange httpExchange = AnnotatedElementUtils.findMergedAnnotation(element, HttpExchange.class);
@ -594,4 +609,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

@ -40,7 +40,6 @@ import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PostMapping; 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.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestMethod;
@ -363,7 +362,9 @@ class RequestMappingHandlerMappingTests {
@Controller @Controller
// gh-31962: The presence of multiple @RequestMappings is intentional.
@RequestMapping(consumes = MediaType.APPLICATION_JSON_VALUE) @RequestMapping(consumes = MediaType.APPLICATION_JSON_VALUE)
@ExtraRequestMapping
static class ComposedAnnotationController { static class ComposedAnnotationController {
@RequestMapping @RequestMapping
@ -382,7 +383,10 @@ class RequestMappingHandlerMappingTests {
public void post(@RequestBody(required = false) Foo foo) { 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() { public void put() {
} }
@ -396,6 +400,11 @@ class RequestMappingHandlerMappingTests {
} }
@RequestMapping
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@interface ExtraRequestMapping {
}
@RequestMapping(method = RequestMethod.POST, @RequestMapping(method = RequestMethod.POST,
produces = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE,