From 699da7c383fc594ef76fcc23a26983f8eadbfd7b Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Wed, 17 Jan 2024 17:44:38 +0100 Subject: [PATCH] =?UTF-8?q?Log=20warning=20if=20multiple=20@=E2=81=A0Reque?= =?UTF-8?q?stMapping=20annotations=20are=20declared?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../controller/ann-requestmapping.adoc | 12 +++++ .../mvc-controller/ann-requestmapping.adoc | 12 +++++ .../web/bind/annotation/DeleteMapping.java | 9 +++- .../web/bind/annotation/GetMapping.java | 9 +++- .../web/bind/annotation/PatchMapping.java | 9 +++- .../web/bind/annotation/PostMapping.java | 9 +++- .../web/bind/annotation/PutMapping.java | 9 +++- .../web/bind/annotation/RequestMapping.java | 7 +++ .../RequestMappingHandlerMapping.java | 52 +++++++++++++++++-- .../RequestMappingHandlerMappingTests.java | 16 +++++- .../RequestMappingHandlerMapping.java | 51 ++++++++++++++++-- .../RequestMappingHandlerMappingTests.java | 13 ++++- 12 files changed, 191 insertions(+), 17 deletions(-) diff --git a/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-requestmapping.adoc b/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-requestmapping.adoc index 90e22574f8be..09b30a4a43cf 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-requestmapping.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-requestmapping.adoc @@ -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 `@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: [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 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 logic. This is a more advanced option that requires sub-classing `RequestMappingHandlerMapping` and overriding the `getCustomMethodCondition` method, where diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-requestmapping.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-requestmapping.adoc index 5e573f1e470f..fe929fda35e7 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-requestmapping.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-requestmapping.adoc @@ -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. 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: [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 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 logic. This is a more advanced option that requires subclassing `RequestMappingHandlerMapping` and overriding the `getCustomMethodCondition` method, where diff --git a/spring-web/src/main/java/org/springframework/web/bind/annotation/DeleteMapping.java b/spring-web/src/main/java/org/springframework/web/bind/annotation/DeleteMapping.java index 1b9150ccf95b..4681a719a9ba 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/annotation/DeleteMapping.java +++ b/spring-web/src/main/java/org/springframework/web/bind/annotation/DeleteMapping.java @@ -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"); * you may not use this file except in compliance with the License. @@ -31,6 +31,13 @@ *

Specifically, {@code @DeleteMapping} is a composed annotation that * acts as a shortcut for {@code @RequestMapping(method = RequestMethod.DELETE)}. * + *

NOTE: 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 * @since 4.3 * @see GetMapping diff --git a/spring-web/src/main/java/org/springframework/web/bind/annotation/GetMapping.java b/spring-web/src/main/java/org/springframework/web/bind/annotation/GetMapping.java index c9fc39bda34b..22092f77fc51 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/annotation/GetMapping.java +++ b/spring-web/src/main/java/org/springframework/web/bind/annotation/GetMapping.java @@ -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"); * you may not use this file except in compliance with the License. @@ -31,6 +31,13 @@ *

Specifically, {@code @GetMapping} is a composed annotation that * acts as a shortcut for {@code @RequestMapping(method = RequestMethod.GET)}. * + *

NOTE: 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 * @since 4.3 * @see PostMapping diff --git a/spring-web/src/main/java/org/springframework/web/bind/annotation/PatchMapping.java b/spring-web/src/main/java/org/springframework/web/bind/annotation/PatchMapping.java index 72fd7111919f..c11f39e4b0a7 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/annotation/PatchMapping.java +++ b/spring-web/src/main/java/org/springframework/web/bind/annotation/PatchMapping.java @@ -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"); * you may not use this file except in compliance with the License. @@ -31,6 +31,13 @@ *

Specifically, {@code @PatchMapping} is a composed annotation that * acts as a shortcut for {@code @RequestMapping(method = RequestMethod.PATCH)}. * + *

NOTE: 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 * @since 4.3 * @see GetMapping diff --git a/spring-web/src/main/java/org/springframework/web/bind/annotation/PostMapping.java b/spring-web/src/main/java/org/springframework/web/bind/annotation/PostMapping.java index f5a304038b33..18a0b47db553 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/annotation/PostMapping.java +++ b/spring-web/src/main/java/org/springframework/web/bind/annotation/PostMapping.java @@ -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"); * you may not use this file except in compliance with the License. @@ -31,6 +31,13 @@ *

Specifically, {@code @PostMapping} is a composed annotation that * acts as a shortcut for {@code @RequestMapping(method = RequestMethod.POST)}. * + *

NOTE: 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 * @since 4.3 * @see GetMapping diff --git a/spring-web/src/main/java/org/springframework/web/bind/annotation/PutMapping.java b/spring-web/src/main/java/org/springframework/web/bind/annotation/PutMapping.java index 0040291dbefb..8e8cb005d0a4 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/annotation/PutMapping.java +++ b/spring-web/src/main/java/org/springframework/web/bind/annotation/PutMapping.java @@ -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"); * you may not use this file except in compliance with the License. @@ -31,6 +31,13 @@ *

Specifically, {@code @PutMapping} is a composed annotation that * acts as a shortcut for {@code @RequestMapping(method = RequestMethod.PUT)}. * + *

NOTE: 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 * @since 4.3 * @see GetMapping 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 624281339e25..0bff16d474fb 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 @@ -54,6 +54,13 @@ * {@link PutMapping @PutMapping}, {@link DeleteMapping @DeleteMapping}, or * {@link PatchMapping @PatchMapping}. * + *

NOTE: 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. + * *

NOTE: When using controller interfaces (e.g. for AOP proxying), * make sure to consistently put all your mapping annotations — such * as {@code @RequestMapping} and {@code @SessionAttributes} — on 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 3bb494ac1738..e96cb1f9fd1c 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 @@ -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 @@ private RequestMappingInfo createRequestMappingInfo(AnnotatedElement element) { 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> 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 @@ private String resolveCorsAnnotationValue(String value) { } } + private static class AnnotationDescriptor { + + private final A annotation; + private final Annotation source; + + AnnotationDescriptor(MergedAnnotation 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(); + } + + } + } 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 71299abb82b0..cc456deea3f6 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 @@ -36,7 +36,6 @@ 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 @@ * * @author Rossen Stoyanchev * @author Olga Maciaszek-Sharma + * @author Sam Brannen */ class RequestMappingHandlerMappingTests { @@ -231,7 +231,9 @@ private RequestMappingInfo assertComposedAnnotationMapping( @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 @@ public void get() { 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 @@ private static class Foo { } + @RequestMapping + @Target(ElementType.TYPE) + @Retention(RetentionPolicy.RUNTIME) + @interface ExtraRequestMapping { + } + + @RequestMapping(method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) 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 92a445a650d3..fd1f041aa831 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 @@ -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,6 +16,7 @@ package org.springframework.web.servlet.mvc.method.annotation; +import java.lang.annotation.Annotation; import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Method; import java.lang.reflect.Parameter; @@ -30,7 +31,10 @@ 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; @@ -343,9 +347,20 @@ private RequestMappingInfo createRequestMappingInfo(AnnotatedElement element) { 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> 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); @@ -594,4 +609,32 @@ private String resolveCorsAnnotationValue(String value) { } } + private static class AnnotationDescriptor { + + private final A annotation; + private final Annotation source; + + AnnotationDescriptor(MergedAnnotation 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(); + } + + } + } 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 b960fa98ac0a..4a20ef66663a 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 @@ -40,7 +40,6 @@ 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; @@ -363,7 +362,9 @@ private RequestMappingInfo assertComposedAnnotationMapping( @Controller + // gh-31962: The presence of multiple @RequestMappings is intentional. @RequestMapping(consumes = MediaType.APPLICATION_JSON_VALUE) + @ExtraRequestMapping static class ComposedAnnotationController { @RequestMapping @@ -382,7 +383,10 @@ public void get() { 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() { } @@ -396,6 +400,11 @@ public void patch() { } + @RequestMapping + @Target(ElementType.TYPE) + @Retention(RetentionPolicy.RUNTIME) + @interface ExtraRequestMapping { + } @RequestMapping(method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE,