Browse Source

Support @RequestMapping as meta-annotation

Issue: SPR-12296
pull/903/head
Rossen Stoyanchev 10 years ago
parent
commit
8376e1eca1
  1. 30
      spring-web/src/main/java/org/springframework/web/bind/annotation/RequestMapping.java
  2. 49
      spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/MvcUriComponentsBuilder.java
  3. 110
      spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java
  4. 39
      spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/MvcUriComponentsBuilderTests.java
  5. 41
      spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMappingTests.java

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

@ -57,8 +57,9 @@ import java.util.concurrent.Callable; @@ -57,8 +57,9 @@ import java.util.concurrent.Callable;
* As a consequence, such an argument will never be {@code null}.
* <i>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.</i>
* {@link org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter#setSynchronizeOnSession
* "synchronizeOnSession"} flag to "true" if multiple requests are allowed to
* access a session concurrently.</i>
* <li>{@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 { @@ -297,20 +298,31 @@ public @interface RequestMapping {
/**
* The primary mapping expressed by this annotation.
* <p>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}")
* <p>In a Portlet environment: the mapped portlet modes
* <p>In a Servlet environment this is an alias for {@link #path()}.
* For example {@code @RequestMapping("/foo")} is equivalent to
* {@code @RequestMapping(path="/foo")}.
* <p>In a Portlet environment this is the mapped portlet modes
* (i.e. "EDIT", "VIEW", "HELP" or any custom modes).
* <p><b>Supported at the type level as well as at the method level!</b>
* 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}")
* <p><b>Supported at the type level as well as at the method level!</b>
* 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.

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

@ -40,7 +40,8 @@ import org.springframework.cglib.proxy.MethodProxy; @@ -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 { @@ -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 { @@ -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) {

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

@ -16,15 +16,19 @@ @@ -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 @@ -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 @@ -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 @@ -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());

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

@ -20,6 +20,11 @@ import static org.hamcrest.Matchers.*; @@ -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; @@ -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 { @@ -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 { @@ -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 { @@ -415,7 +453,6 @@ public class MvcUriComponentsBuilderTests {
public PersonsAddressesController controller() {
return new PersonsAddressesController();
}
}
}

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

@ -1,5 +1,5 @@ @@ -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 @@ @@ -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; @@ -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 { @@ -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 { @@ -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 { @@ -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 {};
}
}

Loading…
Cancel
Save