Support @RequestMapping as meta-annotation
Issue: SPR-12296
This commit is contained in:
parent
60b19c784d
commit
8376e1eca1
|
|
@ -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 {
|
|||
|
||||
/**
|
||||
* 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.
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {};
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue