Skip to content

Commit 8376e1e

Browse files
committed
Support @RequestMapping as meta-annotation
Issue: SPR-12296
1 parent 60b19c7 commit 8376e1e

File tree

5 files changed

+213
-56
lines changed

5 files changed

+213
-56
lines changed

spring-web/src/main/java/org/springframework/web/bind/annotation/RequestMapping.java

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,9 @@
5757
* As a consequence, such an argument will never be {@code null}.
5858
* <i>Note that session access may not be thread-safe, in particular in a
5959
* Servlet environment: Consider switching the
60-
* {@link org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter#setSynchronizeOnSession "synchronizeOnSession"}
61-
* flag to "true" if multiple requests are allowed to access a session concurrently.</i>
60+
* {@link org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter#setSynchronizeOnSession
61+
* "synchronizeOnSession"} flag to "true" if multiple requests are allowed to
62+
* access a session concurrently.</i>
6263
* <li>{@link org.springframework.web.context.request.WebRequest} or
6364
* {@link org.springframework.web.context.request.NativeWebRequest}.
6465
* Allows for generic request parameter access as well as request/session
@@ -297,20 +298,31 @@
297298

298299
/**
299300
* The primary mapping expressed by this annotation.
300-
* <p>In a Servlet environment: the path mapping URIs (e.g. "/myPath.do").
301-
* Ant-style path patterns are also supported (e.g. "/myPath/*.do").
302-
* At the method level, relative paths (e.g. "edit.do") are supported
303-
* within the primary mapping expressed at the type level.
304-
* Path mapping URIs may contain placeholders (e.g. "/${connect}")
305-
* <p>In a Portlet environment: the mapped portlet modes
301+
* <p>In a Servlet environment this is an alias for {@link #path()}.
302+
* For example {@code @RequestMapping("/foo")} is equivalent to
303+
* {@code @RequestMapping(path="/foo")}.
304+
* <p>In a Portlet environment this is the mapped portlet modes
306305
* (i.e. "EDIT", "VIEW", "HELP" or any custom modes).
307306
* <p><b>Supported at the type level as well as at the method level!</b>
308307
* When used at the type level, all method-level mappings inherit
309308
* this primary mapping, narrowing it for a specific handler method.
310-
* @see org.springframework.web.bind.annotation.ValueConstants#DEFAULT_NONE
311309
*/
312310
String[] value() default {};
313311

312+
/**
313+
* In a Servlet environment only: the path mapping URIs (e.g. "/myPath.do").
314+
* Ant-style path patterns are also supported (e.g. "/myPath/*.do").
315+
* At the method level, relative paths (e.g. "edit.do") are supported within
316+
* the primary mapping expressed at the type level. Path mapping URIs may
317+
* contain placeholders (e.g. "/${connect}")
318+
* <p><b>Supported at the type level as well as at the method level!</b>
319+
* When used at the type level, all method-level mappings inherit
320+
* this primary mapping, narrowing it for a specific handler method.
321+
* @see org.springframework.web.bind.annotation.ValueConstants#DEFAULT_NONE
322+
* @since 4.2
323+
*/
324+
String[] path() default {};
325+
314326
/**
315327
* The HTTP request methods to map to, narrowing the primary mapping:
316328
* GET, POST, HEAD, OPTIONS, PUT, PATCH, DELETE, TRACE.

spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/MvcUriComponentsBuilder.java

Lines changed: 30 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@
4040
import org.springframework.core.DefaultParameterNameDiscoverer;
4141
import org.springframework.core.MethodParameter;
4242
import org.springframework.core.ParameterNameDiscoverer;
43-
import org.springframework.core.annotation.AnnotationUtils;
43+
import org.springframework.core.annotation.AnnotatedElementUtils;
44+
import org.springframework.core.annotation.AnnotationAttributes;
4445
import org.springframework.objenesis.Objenesis;
4546
import org.springframework.objenesis.SpringObjenesis;
4647
import org.springframework.util.AntPathMatcher;
@@ -377,16 +378,40 @@ private static UriComponentsBuilder getBaseUrlToUse(UriComponentsBuilder baseUrl
377378

378379
private static String getTypeRequestMapping(Class<?> controllerType) {
379380
Assert.notNull(controllerType, "'controllerType' must not be null");
380-
RequestMapping annot = AnnotationUtils.findAnnotation(controllerType, RequestMapping.class);
381-
if (annot == null || ObjectUtils.isEmpty(annot.value()) || StringUtils.isEmpty(annot.value()[0])) {
381+
String annotType = RequestMapping.class.getName();
382+
AnnotationAttributes attrs = AnnotatedElementUtils.getAnnotationAttributes(controllerType, annotType);
383+
if (attrs == null) {
382384
return "/";
383385
}
384-
if (annot.value().length > 1 && logger.isWarnEnabled()) {
386+
String[] paths = attrs.getStringArray("path");
387+
paths = ObjectUtils.isEmpty(paths) ? attrs.getStringArray("value") : paths;
388+
if (ObjectUtils.isEmpty(paths) || StringUtils.isEmpty(paths[0])) {
389+
return "/";
390+
}
391+
if (paths.length > 1 && logger.isWarnEnabled()) {
385392
logger.warn("Multiple paths on controller " + controllerType.getName() + ", using first one");
386393
}
387-
return annot.value()[0];
394+
return paths[0];
395+
}
396+
397+
private static String getMethodRequestMapping(Method method) {
398+
String annotType = RequestMapping.class.getName();
399+
AnnotationAttributes attrs = AnnotatedElementUtils.getAnnotationAttributes(method, annotType);
400+
if (attrs == null) {
401+
throw new IllegalArgumentException("No @RequestMapping on: " + method.toGenericString());
402+
}
403+
String[] paths = attrs.getStringArray("path");
404+
paths = ObjectUtils.isEmpty(paths) ? attrs.getStringArray("value") : paths;
405+
if (ObjectUtils.isEmpty(paths) || StringUtils.isEmpty(paths[0])) {
406+
return "/";
407+
}
408+
if (paths.length > 1 && logger.isWarnEnabled()) {
409+
logger.warn("Multiple paths on method " + method.toGenericString() + ", using first one");
410+
}
411+
return paths[0];
388412
}
389413

414+
390415
private static Method getMethod(Class<?> controllerType, String methodName, Object... args) {
391416
Method match = null;
392417
for (Method method : controllerType.getDeclaredMethods()) {
@@ -405,20 +430,6 @@ private static Method getMethod(Class<?> controllerType, String methodName, Obje
405430
return match;
406431
}
407432

408-
private static String getMethodRequestMapping(Method method) {
409-
RequestMapping annot = AnnotationUtils.findAnnotation(method, RequestMapping.class);
410-
if (annot == null) {
411-
throw new IllegalArgumentException("No @RequestMapping on: " + method.toGenericString());
412-
}
413-
if (ObjectUtils.isEmpty(annot.value()) || StringUtils.isEmpty(annot.value()[0])) {
414-
return "/";
415-
}
416-
if (annot.value().length > 1 && logger.isWarnEnabled()) {
417-
logger.warn("Multiple paths on method " + method.toGenericString() + ", using first one");
418-
}
419-
return annot.value()[0];
420-
}
421-
422433
private static UriComponents applyContributors(UriComponentsBuilder builder, Method method, Object... args) {
423434
CompositeUriComponentsContributor contributor = getConfiguredUriComponentsContributor();
424435
if (contributor == null) {

spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java

Lines changed: 87 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,19 @@
1616

1717
package org.springframework.web.servlet.mvc.method.annotation;
1818

19+
import java.lang.reflect.AnnotatedElement;
1920
import java.lang.reflect.Method;
2021
import java.util.ArrayList;
2122
import java.util.List;
2223

2324
import org.springframework.context.EmbeddedValueResolverAware;
25+
import org.springframework.core.annotation.AnnotatedElementUtils;
26+
import org.springframework.core.annotation.AnnotationAttributes;
2427
import org.springframework.core.annotation.AnnotationUtils;
2528
import org.springframework.stereotype.Controller;
2629
import org.springframework.util.Assert;
2730
import org.springframework.util.CollectionUtils;
31+
import org.springframework.util.ObjectUtils;
2832
import org.springframework.util.StringValueResolver;
2933
import org.springframework.web.accept.ContentNegotiationManager;
3034
import org.springframework.web.bind.annotation.CrossOrigin;
@@ -190,15 +194,11 @@ protected boolean isHandler(Class<?> beanType) {
190194
*/
191195
@Override
192196
protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {
193-
RequestMappingInfo info = null;
194-
RequestMapping methodAnnotation = AnnotationUtils.findAnnotation(method, RequestMapping.class);
195-
if (methodAnnotation != null) {
196-
RequestCondition<?> methodCondition = getCustomMethodCondition(method);
197-
info = createRequestMappingInfo(methodAnnotation, methodCondition);
198-
RequestMapping typeAnnotation = AnnotationUtils.findAnnotation(handlerType, RequestMapping.class);
199-
if (typeAnnotation != null) {
200-
RequestCondition<?> typeCondition = getCustomTypeCondition(handlerType);
201-
info = createRequestMappingInfo(typeAnnotation, typeCondition).combine(info);
197+
RequestMappingInfo info = createRequestMappingInfo(method);
198+
if (info != null) {
199+
RequestMappingInfo typeInfo = createRequestMappingInfo(handlerType);
200+
if (typeInfo != null) {
201+
info = typeInfo.combine(info);
202202
}
203203
}
204204
return info;
@@ -235,20 +235,83 @@ protected RequestCondition<?> getCustomMethodCondition(Method method) {
235235
}
236236

237237
/**
238-
* Created a RequestMappingInfo from a RequestMapping annotation.
238+
* Transitional method used to invoke one of two createRequestMappingInfo
239+
* variants one of which is deprecated.
239240
*/
240-
protected RequestMappingInfo createRequestMappingInfo(RequestMapping annotation, RequestCondition<?> customCondition) {
241-
String[] patterns = resolveEmbeddedValuesInPatterns(annotation.value());
242-
return new RequestMappingInfo(
243-
annotation.name(),
244-
new PatternsRequestCondition(patterns, getUrlPathHelper(), getPathMatcher(),
245-
this.useSuffixPatternMatch, this.useTrailingSlashMatch, this.fileExtensions),
246-
new RequestMethodsRequestCondition(annotation.method()),
247-
new ParamsRequestCondition(annotation.params()),
248-
new HeadersRequestCondition(annotation.headers()),
249-
new ConsumesRequestCondition(annotation.consumes(), annotation.headers()),
250-
new ProducesRequestCondition(annotation.produces(), annotation.headers(), this.contentNegotiationManager),
251-
customCondition);
241+
private RequestMappingInfo createRequestMappingInfo(AnnotatedElement annotatedElement) {
242+
RequestMapping annotation;
243+
AnnotationAttributes attributes;
244+
RequestCondition<?> customCondition;
245+
String annotationType = RequestMapping.class.getName();
246+
if (annotatedElement instanceof Class<?>) {
247+
Class<?> type = (Class<?>) annotatedElement;
248+
annotation = AnnotationUtils.findAnnotation(type, RequestMapping.class);
249+
attributes = AnnotatedElementUtils.getAnnotationAttributes(type, annotationType);
250+
customCondition = getCustomTypeCondition(type);
251+
}
252+
else {
253+
Method method = (Method) annotatedElement;
254+
annotation = AnnotationUtils.findAnnotation(method, RequestMapping.class);
255+
attributes = AnnotatedElementUtils.getAnnotationAttributes(method, annotationType);
256+
customCondition = getCustomMethodCondition(method);
257+
}
258+
RequestMappingInfo info = null;
259+
if (annotation != null) {
260+
info = createRequestMappingInfo(annotation, customCondition);
261+
if (info == null) {
262+
info = createRequestMappingInfo(attributes, customCondition);
263+
}
264+
}
265+
return info;
266+
}
267+
268+
/**
269+
* Create a RequestMappingInfo from a RequestMapping annotation.
270+
* @deprecated as of 4.2 after the introduction of support for
271+
* {@code @RequestMapping} as meta-annotation. Please use
272+
* {@link #createRequestMappingInfo(AnnotationAttributes, RequestCondition)}.
273+
*/
274+
@Deprecated
275+
protected RequestMappingInfo createRequestMappingInfo(RequestMapping annotation,
276+
RequestCondition<?> customCondition) {
277+
278+
return null;
279+
}
280+
281+
/**
282+
* Create a RequestMappingInfo from the attributes of an
283+
* {@code @RequestMapping} annotation or a meta-annotation, i.e. a custom
284+
* annotation annotated with {@code @RequestMapping}.
285+
* @since 4.2
286+
*/
287+
protected RequestMappingInfo createRequestMappingInfo(AnnotationAttributes attributes,
288+
RequestCondition<?> customCondition) {
289+
290+
String mappingName = attributes.getString("name");
291+
292+
String[] paths = attributes.getStringArray("path");
293+
paths = ObjectUtils.isEmpty(paths) ? attributes.getStringArray("value") : paths;
294+
PatternsRequestCondition patternsCondition = new PatternsRequestCondition(
295+
resolveEmbeddedValuesInPatterns(paths), getUrlPathHelper(), getPathMatcher(),
296+
this.useSuffixPatternMatch, this.useTrailingSlashMatch, this.fileExtensions);
297+
298+
RequestMethod[] methods = (RequestMethod[]) attributes.get("method");
299+
RequestMethodsRequestCondition methodsCondition = new RequestMethodsRequestCondition(methods);
300+
301+
String[] params = attributes.getStringArray("params");
302+
ParamsRequestCondition paramsCondition = new ParamsRequestCondition(params);
303+
304+
String[] headers = attributes.getStringArray("headers");
305+
String[] consumes = attributes.getStringArray("consumes");
306+
String[] produces = attributes.getStringArray("produces");
307+
308+
HeadersRequestCondition headersCondition = new HeadersRequestCondition(headers);
309+
ConsumesRequestCondition consumesCondition = new ConsumesRequestCondition(consumes, headers);
310+
ProducesRequestCondition producesCondition = new ProducesRequestCondition(produces,
311+
headers, this.contentNegotiationManager);
312+
313+
return new RequestMappingInfo(mappingName, patternsCondition, methodsCondition, paramsCondition,
314+
headersCondition, consumesCondition, producesCondition, customCondition);
252315
}
253316

254317
/**
@@ -320,7 +383,8 @@ else if (annotation.allowCredentials().equalsIgnoreCase("false")) {
320383
config.setAllowCredentials(false);
321384
}
322385
else if (!annotation.allowCredentials().isEmpty()) {
323-
throw new IllegalStateException("AllowCredentials value must be \"true\", \"false\" or \"\" (empty string), current value is " + annotation.allowCredentials());
386+
throw new IllegalStateException("AllowCredentials value must be \"true\", \"false\" " +
387+
"or \"\" (empty string), current value is " + annotation.allowCredentials());
324388
}
325389
if (annotation.maxAge() != -1 && config.getMaxAge() == null) {
326390
config.setMaxAge(annotation.maxAge());

spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/MvcUriComponentsBuilderTests.java

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@
2020
import static org.junit.Assert.*;
2121
import static org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder.*;
2222

23+
import java.lang.annotation.Documented;
24+
import java.lang.annotation.ElementType;
25+
import java.lang.annotation.Retention;
26+
import java.lang.annotation.RetentionPolicy;
27+
import java.lang.annotation.Target;
2328
import java.util.Arrays;
2429
import java.util.List;
2530

@@ -34,12 +39,15 @@
3439
import org.springframework.format.annotation.DateTimeFormat;
3540
import org.springframework.format.annotation.DateTimeFormat.ISO;
3641
import org.springframework.http.HttpEntity;
42+
import org.springframework.http.MediaType;
3743
import org.springframework.mock.web.test.MockHttpServletRequest;
3844
import org.springframework.mock.web.test.MockServletContext;
45+
import org.springframework.stereotype.Controller;
3946
import org.springframework.util.MultiValueMap;
4047
import org.springframework.web.bind.annotation.PathVariable;
4148
import org.springframework.web.bind.annotation.RequestBody;
4249
import org.springframework.web.bind.annotation.RequestMapping;
50+
import org.springframework.web.bind.annotation.RequestMethod;
4351
import org.springframework.web.bind.annotation.RequestParam;
4452
import org.springframework.web.context.request.RequestContextHolder;
4553
import org.springframework.web.context.request.ServletRequestAttributes;
@@ -198,6 +206,12 @@ public void testFromMethodNameWithCustomBaseUrlViaInstance() throws Exception {
198206
assertEquals("http://example.org:9090/base", builder.toUriString());
199207
}
200208

209+
@Test
210+
public void testFromMethodNameWithMetaAnnotation() throws Exception {
211+
UriComponents uriComponents = fromMethodName(MetaAnnotationController.class, "handleInput").build();
212+
assertThat(uriComponents.toUriString(), is("http://localhost/input"));
213+
}
214+
201215
@Test
202216
public void testFromMethodCall() {
203217
UriComponents uriComponents = fromMethodCall(on(ControllerWithMethods.class).myMethod(null)).build();
@@ -408,14 +422,37 @@ public String showCreate(@PathVariable Integer userId) {
408422
}
409423
}
410424

425+
@SuppressWarnings("unused")
426+
@Controller
427+
static class MetaAnnotationController {
428+
429+
@RequestMapping
430+
public void handle() {
431+
}
432+
433+
@PostJson(path="/input")
434+
public void handleInput() {
435+
}
436+
437+
}
438+
439+
@RequestMapping(method = RequestMethod.POST,
440+
produces = MediaType.APPLICATION_JSON_VALUE,
441+
consumes = MediaType.APPLICATION_JSON_VALUE)
442+
@Target({ElementType.METHOD, ElementType.TYPE})
443+
@Retention(RetentionPolicy.RUNTIME)
444+
@Documented
445+
@interface PostJson {
446+
String[] path() default {};
447+
}
448+
411449
@EnableWebMvc
412450
static class WebConfig extends WebMvcConfigurerAdapter {
413451

414452
@Bean
415453
public PersonsAddressesController controller() {
416454
return new PersonsAddressesController();
417455
}
418-
419456
}
420457

421458
}

0 commit comments

Comments
 (0)