From 8376e1eca13b62b34abb40af2dfaab243568c813 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Tue, 17 Feb 2015 17:00:21 -0500 Subject: [PATCH] Support @RequestMapping as meta-annotation Issue: SPR-12296 --- .../web/bind/annotation/RequestMapping.java | 30 +++-- .../annotation/MvcUriComponentsBuilder.java | 49 +++++--- .../RequestMappingHandlerMapping.java | 110 ++++++++++++++---- .../MvcUriComponentsBuilderTests.java | 39 ++++++- .../RequestMappingHandlerMappingTests.java | 41 ++++++- 5 files changed, 213 insertions(+), 56 deletions(-) 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 e3b7655be0..69c86f4fd6 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 @@ -57,8 +57,9 @@ import java.util.concurrent.Callable; * As a consequence, such an argument will never be {@code null}. * Note that session access may not be thread-safe, in particular in a * Servlet environment: Consider switching the - * {@link org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter#setSynchronizeOnSession "synchronizeOnSession"} - * flag to "true" if multiple requests are allowed to access a session concurrently. + * {@link org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter#setSynchronizeOnSession + * "synchronizeOnSession"} flag to "true" if multiple requests are allowed to + * access a session concurrently. *
  • {@link org.springframework.web.context.request.WebRequest} or * {@link org.springframework.web.context.request.NativeWebRequest}. * Allows for generic request parameter access as well as request/session @@ -297,20 +298,31 @@ public @interface RequestMapping { /** * The primary mapping expressed by this annotation. - *

    In a Servlet environment: the path mapping URIs (e.g. "/myPath.do"). - * Ant-style path patterns are also supported (e.g. "/myPath/*.do"). - * At the method level, relative paths (e.g. "edit.do") are supported - * within the primary mapping expressed at the type level. - * Path mapping URIs may contain placeholders (e.g. "/${connect}") - *

    In a Portlet environment: the mapped portlet modes + *

    In a Servlet environment this is an alias for {@link #path()}. + * For example {@code @RequestMapping("/foo")} is equivalent to + * {@code @RequestMapping(path="/foo")}. + *

    In a Portlet environment this is the mapped portlet modes * (i.e. "EDIT", "VIEW", "HELP" or any custom modes). *

    Supported at the type level as well as at the method level! * When used at the type level, all method-level mappings inherit * this primary mapping, narrowing it for a specific handler method. - * @see org.springframework.web.bind.annotation.ValueConstants#DEFAULT_NONE */ String[] value() default {}; + /** + * In a Servlet environment only: the path mapping URIs (e.g. "/myPath.do"). + * Ant-style path patterns are also supported (e.g. "/myPath/*.do"). + * At the method level, relative paths (e.g. "edit.do") are supported within + * the primary mapping expressed at the type level. Path mapping URIs may + * contain placeholders (e.g. "/${connect}") + *

    Supported at the type level as well as at the method level! + * When used at the type level, all method-level mappings inherit + * this primary mapping, narrowing it for a specific handler method. + * @see org.springframework.web.bind.annotation.ValueConstants#DEFAULT_NONE + * @since 4.2 + */ + String[] path() default {}; + /** * The HTTP request methods to map to, narrowing the primary mapping: * GET, POST, HEAD, OPTIONS, PUT, PATCH, DELETE, TRACE. diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/MvcUriComponentsBuilder.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/MvcUriComponentsBuilder.java index 81b142ffbe..e88ece401d 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/MvcUriComponentsBuilder.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/MvcUriComponentsBuilder.java @@ -40,7 +40,8 @@ import org.springframework.cglib.proxy.MethodProxy; import org.springframework.core.DefaultParameterNameDiscoverer; import org.springframework.core.MethodParameter; import org.springframework.core.ParameterNameDiscoverer; -import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.core.annotation.AnnotationAttributes; import org.springframework.objenesis.Objenesis; import org.springframework.objenesis.SpringObjenesis; import org.springframework.util.AntPathMatcher; @@ -377,16 +378,40 @@ public class MvcUriComponentsBuilder { private static String getTypeRequestMapping(Class controllerType) { Assert.notNull(controllerType, "'controllerType' must not be null"); - RequestMapping annot = AnnotationUtils.findAnnotation(controllerType, RequestMapping.class); - if (annot == null || ObjectUtils.isEmpty(annot.value()) || StringUtils.isEmpty(annot.value()[0])) { + String annotType = RequestMapping.class.getName(); + AnnotationAttributes attrs = AnnotatedElementUtils.getAnnotationAttributes(controllerType, annotType); + if (attrs == null) { return "/"; } - if (annot.value().length > 1 && logger.isWarnEnabled()) { + String[] paths = attrs.getStringArray("path"); + paths = ObjectUtils.isEmpty(paths) ? attrs.getStringArray("value") : paths; + if (ObjectUtils.isEmpty(paths) || StringUtils.isEmpty(paths[0])) { + return "/"; + } + if (paths.length > 1 && logger.isWarnEnabled()) { logger.warn("Multiple paths on controller " + controllerType.getName() + ", using first one"); } - return annot.value()[0]; + return paths[0]; + } + + private static String getMethodRequestMapping(Method method) { + String annotType = RequestMapping.class.getName(); + AnnotationAttributes attrs = AnnotatedElementUtils.getAnnotationAttributes(method, annotType); + if (attrs == null) { + throw new IllegalArgumentException("No @RequestMapping on: " + method.toGenericString()); + } + String[] paths = attrs.getStringArray("path"); + paths = ObjectUtils.isEmpty(paths) ? attrs.getStringArray("value") : paths; + if (ObjectUtils.isEmpty(paths) || StringUtils.isEmpty(paths[0])) { + return "/"; + } + if (paths.length > 1 && logger.isWarnEnabled()) { + logger.warn("Multiple paths on method " + method.toGenericString() + ", using first one"); + } + return paths[0]; } + private static Method getMethod(Class controllerType, String methodName, Object... args) { Method match = null; for (Method method : controllerType.getDeclaredMethods()) { @@ -405,20 +430,6 @@ public class MvcUriComponentsBuilder { return match; } - private static String getMethodRequestMapping(Method method) { - RequestMapping annot = AnnotationUtils.findAnnotation(method, RequestMapping.class); - if (annot == null) { - throw new IllegalArgumentException("No @RequestMapping on: " + method.toGenericString()); - } - if (ObjectUtils.isEmpty(annot.value()) || StringUtils.isEmpty(annot.value()[0])) { - return "/"; - } - if (annot.value().length > 1 && logger.isWarnEnabled()) { - logger.warn("Multiple paths on method " + method.toGenericString() + ", using first one"); - } - return annot.value()[0]; - } - private static UriComponents applyContributors(UriComponentsBuilder builder, Method method, Object... args) { CompositeUriComponentsContributor contributor = getConfiguredUriComponentsContributor(); if (contributor == null) { 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 bcfe8c88e0..3b318acdb7 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 @@ -16,15 +16,19 @@ package org.springframework.web.servlet.mvc.method.annotation; +import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.List; import org.springframework.context.EmbeddedValueResolverAware; +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.core.annotation.AnnotationAttributes; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.stereotype.Controller; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; +import org.springframework.util.ObjectUtils; import org.springframework.util.StringValueResolver; import org.springframework.web.accept.ContentNegotiationManager; import org.springframework.web.bind.annotation.CrossOrigin; @@ -190,15 +194,11 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi */ @Override protected RequestMappingInfo getMappingForMethod(Method method, Class handlerType) { - RequestMappingInfo info = null; - RequestMapping methodAnnotation = AnnotationUtils.findAnnotation(method, RequestMapping.class); - if (methodAnnotation != null) { - RequestCondition methodCondition = getCustomMethodCondition(method); - info = createRequestMappingInfo(methodAnnotation, methodCondition); - RequestMapping typeAnnotation = AnnotationUtils.findAnnotation(handlerType, RequestMapping.class); - if (typeAnnotation != null) { - RequestCondition typeCondition = getCustomTypeCondition(handlerType); - info = createRequestMappingInfo(typeAnnotation, typeCondition).combine(info); + RequestMappingInfo info = createRequestMappingInfo(method); + if (info != null) { + RequestMappingInfo typeInfo = createRequestMappingInfo(handlerType); + if (typeInfo != null) { + info = typeInfo.combine(info); } } return info; @@ -235,20 +235,83 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi } /** - * Created a RequestMappingInfo from a RequestMapping annotation. + * Transitional method used to invoke one of two createRequestMappingInfo + * variants one of which is deprecated. */ - protected RequestMappingInfo createRequestMappingInfo(RequestMapping annotation, RequestCondition customCondition) { - String[] patterns = resolveEmbeddedValuesInPatterns(annotation.value()); - return new RequestMappingInfo( - annotation.name(), - new PatternsRequestCondition(patterns, getUrlPathHelper(), getPathMatcher(), - this.useSuffixPatternMatch, this.useTrailingSlashMatch, this.fileExtensions), - new RequestMethodsRequestCondition(annotation.method()), - new ParamsRequestCondition(annotation.params()), - new HeadersRequestCondition(annotation.headers()), - new ConsumesRequestCondition(annotation.consumes(), annotation.headers()), - new ProducesRequestCondition(annotation.produces(), annotation.headers(), this.contentNegotiationManager), - customCondition); + private RequestMappingInfo createRequestMappingInfo(AnnotatedElement annotatedElement) { + RequestMapping annotation; + AnnotationAttributes attributes; + RequestCondition customCondition; + String annotationType = RequestMapping.class.getName(); + if (annotatedElement instanceof Class) { + Class type = (Class) annotatedElement; + annotation = AnnotationUtils.findAnnotation(type, RequestMapping.class); + attributes = AnnotatedElementUtils.getAnnotationAttributes(type, annotationType); + customCondition = getCustomTypeCondition(type); + } + else { + Method method = (Method) annotatedElement; + annotation = AnnotationUtils.findAnnotation(method, RequestMapping.class); + attributes = AnnotatedElementUtils.getAnnotationAttributes(method, annotationType); + customCondition = getCustomMethodCondition(method); + } + RequestMappingInfo info = null; + if (annotation != null) { + info = createRequestMappingInfo(annotation, customCondition); + if (info == null) { + info = createRequestMappingInfo(attributes, customCondition); + } + } + return info; + } + + /** + * Create a RequestMappingInfo from a RequestMapping annotation. + * @deprecated as of 4.2 after the introduction of support for + * {@code @RequestMapping} as meta-annotation. Please use + * {@link #createRequestMappingInfo(AnnotationAttributes, RequestCondition)}. + */ + @Deprecated + protected RequestMappingInfo createRequestMappingInfo(RequestMapping annotation, + RequestCondition customCondition) { + + return null; + } + + /** + * Create a RequestMappingInfo from the attributes of an + * {@code @RequestMapping} annotation or a meta-annotation, i.e. a custom + * annotation annotated with {@code @RequestMapping}. + * @since 4.2 + */ + protected RequestMappingInfo createRequestMappingInfo(AnnotationAttributes attributes, + RequestCondition customCondition) { + + String mappingName = attributes.getString("name"); + + String[] paths = attributes.getStringArray("path"); + paths = ObjectUtils.isEmpty(paths) ? attributes.getStringArray("value") : paths; + PatternsRequestCondition patternsCondition = new PatternsRequestCondition( + resolveEmbeddedValuesInPatterns(paths), getUrlPathHelper(), getPathMatcher(), + this.useSuffixPatternMatch, this.useTrailingSlashMatch, this.fileExtensions); + + RequestMethod[] methods = (RequestMethod[]) attributes.get("method"); + RequestMethodsRequestCondition methodsCondition = new RequestMethodsRequestCondition(methods); + + String[] params = attributes.getStringArray("params"); + ParamsRequestCondition paramsCondition = new ParamsRequestCondition(params); + + String[] headers = attributes.getStringArray("headers"); + String[] consumes = attributes.getStringArray("consumes"); + String[] produces = attributes.getStringArray("produces"); + + HeadersRequestCondition headersCondition = new HeadersRequestCondition(headers); + ConsumesRequestCondition consumesCondition = new ConsumesRequestCondition(consumes, headers); + ProducesRequestCondition producesCondition = new ProducesRequestCondition(produces, + headers, this.contentNegotiationManager); + + return new RequestMappingInfo(mappingName, patternsCondition, methodsCondition, paramsCondition, + headersCondition, consumesCondition, producesCondition, customCondition); } /** @@ -320,7 +383,8 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi config.setAllowCredentials(false); } else if (!annotation.allowCredentials().isEmpty()) { - throw new IllegalStateException("AllowCredentials value must be \"true\", \"false\" or \"\" (empty string), current value is " + annotation.allowCredentials()); + throw new IllegalStateException("AllowCredentials value must be \"true\", \"false\" " + + "or \"\" (empty string), current value is " + annotation.allowCredentials()); } if (annotation.maxAge() != -1 && config.getMaxAge() == null) { config.setMaxAge(annotation.maxAge()); diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/MvcUriComponentsBuilderTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/MvcUriComponentsBuilderTests.java index e2b7d3c373..7bb27000e1 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/MvcUriComponentsBuilderTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/MvcUriComponentsBuilderTests.java @@ -20,6 +20,11 @@ import static org.hamcrest.Matchers.*; import static org.junit.Assert.*; import static org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder.*; +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; import java.util.Arrays; import java.util.List; @@ -34,12 +39,15 @@ import org.springframework.context.annotation.Bean; import org.springframework.format.annotation.DateTimeFormat; import org.springframework.format.annotation.DateTimeFormat.ISO; import org.springframework.http.HttpEntity; +import org.springframework.http.MediaType; import org.springframework.mock.web.test.MockHttpServletRequest; import org.springframework.mock.web.test.MockServletContext; +import org.springframework.stereotype.Controller; import org.springframework.util.MultiValueMap; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; @@ -198,6 +206,12 @@ public class MvcUriComponentsBuilderTests { assertEquals("http://example.org:9090/base", builder.toUriString()); } + @Test + public void testFromMethodNameWithMetaAnnotation() throws Exception { + UriComponents uriComponents = fromMethodName(MetaAnnotationController.class, "handleInput").build(); + assertThat(uriComponents.toUriString(), is("http://localhost/input")); + } + @Test public void testFromMethodCall() { UriComponents uriComponents = fromMethodCall(on(ControllerWithMethods.class).myMethod(null)).build(); @@ -408,6 +422,30 @@ public class MvcUriComponentsBuilderTests { } } + @SuppressWarnings("unused") + @Controller + static class MetaAnnotationController { + + @RequestMapping + public void handle() { + } + + @PostJson(path="/input") + public void handleInput() { + } + + } + + @RequestMapping(method = RequestMethod.POST, + produces = MediaType.APPLICATION_JSON_VALUE, + consumes = MediaType.APPLICATION_JSON_VALUE) + @Target({ElementType.METHOD, ElementType.TYPE}) + @Retention(RetentionPolicy.RUNTIME) + @Documented + @interface PostJson { + String[] path() default {}; + } + @EnableWebMvc static class WebConfig extends WebMvcConfigurerAdapter { @@ -415,7 +453,6 @@ public class MvcUriComponentsBuilderTests { public PersonsAddressesController controller() { return new PersonsAddressesController(); } - } } 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 bd4385f494..892f62216e 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2015 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,11 @@ package org.springframework.web.servlet.mvc.method.annotation; +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; import java.lang.reflect.Method; import java.util.Arrays; import java.util.Collections; @@ -32,6 +37,7 @@ import org.springframework.util.StringValueResolver; import org.springframework.web.accept.ContentNegotiationManager; import org.springframework.web.accept.PathExtensionContentNegotiationStrategy; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.context.support.StaticWebApplicationContext; import org.springframework.web.servlet.mvc.method.RequestMappingInfo; @@ -46,11 +52,14 @@ public class RequestMappingHandlerMappingTests { private RequestMappingHandlerMapping handlerMapping; + private StaticWebApplicationContext applicationContext; + @Before public void setup() { this.handlerMapping = new RequestMappingHandlerMapping(); - this.handlerMapping.setApplicationContext(new StaticWebApplicationContext()); + this.applicationContext = new StaticWebApplicationContext(); + this.handlerMapping.setApplicationContext(applicationContext); } @@ -89,7 +98,7 @@ public class RequestMappingHandlerMappingTests { }; StaticWebApplicationContext wac = new StaticWebApplicationContext(); - wac.registerSingleton("testController", TestController.class); + wac.registerSingleton("testController", MetaAnnotationController.class); wac.refresh(); hm.setContentNegotiationManager(manager); @@ -131,13 +140,37 @@ public class RequestMappingHandlerMappingTests { assertArrayEquals(new String[] { "/foo", "/foo/bar" }, result); } + @Test + public void resolveRequestMappingViaMetaAnnotation() throws Exception { + Method method = MetaAnnotationController.class.getMethod("handleInput"); + RequestMappingInfo info = this.handlerMapping.getMappingForMethod(method, MetaAnnotationController.class); + + assertNotNull(info); + assertEquals(Collections.singleton("/input"), info.getPatternsCondition().getPatterns()); + } + @Controller - static class TestController { + static class MetaAnnotationController { @RequestMapping public void handle() { } + + @PostJson(path="/input") + public void handleInput() { + } + + } + + @RequestMapping(method = RequestMethod.POST, + produces = MediaType.APPLICATION_JSON_VALUE, + consumes = MediaType.APPLICATION_JSON_VALUE) + @Target({ElementType.METHOD, ElementType.TYPE}) + @Retention(RetentionPolicy.RUNTIME) + @Documented + @interface PostJson { + String[] path() default {}; } }