Add naming strategy for @MVC request mappings.

This change adds a strategy for assigning a default name to an
@RequestMapping controller method. The @RequestMapping annotation
itself now has a name attribute allowing the explicit assignment
of a mapping name.

This is mainly intended for use in EL expressions in views. The
RequestContext class now provides a getMvcUrl method that internally
delegates to MvcUriComponents to look up the handler method.

See the Javadoc of MvcUriComponents.fromMappingName.

Issue: SPR-5779
This commit is contained in:
Rossen Stoyanchev
2014-05-06 15:18:31 -04:00
parent 381ccde48d
commit 9d479feadd
14 changed files with 500 additions and 52 deletions

View File

@@ -71,10 +71,15 @@ public abstract class AbstractHandlerMethodMapping<T> extends AbstractHandlerMap
private boolean detectHandlerMethodsInAncestorContexts = false;
private HandlerMethodMappingNamingStrategy<T> namingStrategy;
private final Map<T, HandlerMethod> handlerMethods = new LinkedHashMap<T, HandlerMethod>();
private final MultiValueMap<String, T> urlMap = new LinkedMultiValueMap<String, T>();
private final MultiValueMap<String, HandlerMethod> nameMap = new LinkedMultiValueMap<String, HandlerMethod>();
/**
* Whether to detect handler methods in beans in ancestor ApplicationContexts.
@@ -88,6 +93,16 @@ public abstract class AbstractHandlerMethodMapping<T> extends AbstractHandlerMap
this.detectHandlerMethodsInAncestorContexts = detectHandlerMethodsInAncestorContexts;
}
/**
* Configure the naming strategy to use for assigning a default name to every
* mapped handler method.
*
* @param namingStrategy strategy to use.
*/
public void setHandlerMethodMappingNamingStrategy(HandlerMethodMappingNamingStrategy<T> namingStrategy) {
this.namingStrategy = namingStrategy;
}
/**
* Return a map with all handler methods and their mappings.
*/
@@ -95,6 +110,14 @@ public abstract class AbstractHandlerMethodMapping<T> extends AbstractHandlerMap
return Collections.unmodifiableMap(this.handlerMethods);
}
/**
* Return the handler methods mapped to the mapping with the given name.
* @param mappingName the mapping name
*/
public List<HandlerMethod> getHandlerMethodsForMappingName(String mappingName) {
return this.nameMap.get(mappingName);
}
/**
* Detects handler methods at initialization.
*/
@@ -203,6 +226,34 @@ public abstract class AbstractHandlerMethodMapping<T> extends AbstractHandlerMap
this.urlMap.add(pattern, mapping);
}
}
if (this.namingStrategy != null) {
String name = this.namingStrategy.getName(newHandlerMethod, mapping);
updateNameMap(name, newHandlerMethod);
}
}
private void updateNameMap(String name, HandlerMethod newHandlerMethod) {
List<HandlerMethod> handlerMethods = this.nameMap.get(name);
if (handlerMethods != null) {
for (HandlerMethod handlerMethod : handlerMethods) {
if (handlerMethod.getMethod().equals(newHandlerMethod.getMethod())) {
logger.trace("Mapping name already registered. Multiple controller instances perhaps?");
return;
}
}
}
logger.trace("Mapping name=" + name);
this.nameMap.add(name, newHandlerMethod);
if (this.nameMap.get(name).size() > 1) {
if (logger.isDebugEnabled()) {
logger.debug("Mapping name clash for handlerMethods=" + this.nameMap.get(name) +
". Consider assigning explicit names.");
}
}
}
/**

View File

@@ -0,0 +1,41 @@
/*
* Copyright 2002-2014 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.servlet.handler;
import org.springframework.web.method.HandlerMethod;
import java.lang.reflect.Method;
/**
* A strategy for assigning a name to a controller method mapping.
*
* @author Rossen Stoyanchev
* @since 4.1
*/
public interface HandlerMethodMappingNamingStrategy<T> {
/**
* Determine the name for the given HandlerMethod and mapping.
*
* @param handlerMethod the handler method
* @param mapping the mapping
*
* @return the name
*/
String getName(HandlerMethod handlerMethod, T mapping);
}

View File

@@ -18,6 +18,7 @@ package org.springframework.web.servlet.mvc.method;
import javax.servlet.http.HttpServletRequest;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.mvc.condition.ConsumesRequestCondition;
import org.springframework.web.servlet.mvc.condition.HeadersRequestCondition;
import org.springframework.web.servlet.mvc.condition.ParamsRequestCondition;
@@ -45,6 +46,8 @@ import org.springframework.web.servlet.mvc.condition.RequestMethodsRequestCondit
*/
public final class RequestMappingInfo implements RequestCondition<RequestMappingInfo> {
private final String name;
private final PatternsRequestCondition patternsCondition;
private final RequestMethodsRequestCondition methodsCondition;
@@ -60,13 +63,11 @@ public final class RequestMappingInfo implements RequestCondition<RequestMapping
private final RequestConditionHolder customConditionHolder;
/**
* Creates a new instance with the given request conditions.
*/
public RequestMappingInfo(PatternsRequestCondition patterns, RequestMethodsRequestCondition methods,
public RequestMappingInfo(String name, PatternsRequestCondition patterns, RequestMethodsRequestCondition methods,
ParamsRequestCondition params, HeadersRequestCondition headers, ConsumesRequestCondition consumes,
ProducesRequestCondition produces, RequestCondition<?> custom) {
this.name = (StringUtils.hasText(name) ? name : null);
this.patternsCondition = (patterns != null ? patterns : new PatternsRequestCondition());
this.methodsCondition = (methods != null ? methods : new RequestMethodsRequestCondition());
this.paramsCondition = (params != null ? params : new ParamsRequestCondition());
@@ -76,15 +77,32 @@ public final class RequestMappingInfo implements RequestCondition<RequestMapping
this.customConditionHolder = new RequestConditionHolder(custom);
}
/**
* Creates a new instance with the given request conditions.
*/
public RequestMappingInfo(PatternsRequestCondition patterns, RequestMethodsRequestCondition methods,
ParamsRequestCondition params, HeadersRequestCondition headers, ConsumesRequestCondition consumes,
ProducesRequestCondition produces, RequestCondition<?> custom) {
this(null, patterns, methods, params, headers, consumes, produces, custom);
}
/**
* Re-create a RequestMappingInfo with the given custom request condition.
*/
public RequestMappingInfo(RequestMappingInfo info, RequestCondition<?> customRequestCondition) {
this(info.patternsCondition, info.methodsCondition, info.paramsCondition, info.headersCondition,
this(info.name, info.patternsCondition, info.methodsCondition, info.paramsCondition, info.headersCondition,
info.consumesCondition, info.producesCondition, customRequestCondition);
}
/**
* Return the name for this mapping, or {@code null}.
*/
public String getName() {
return this.name;
}
/**
* Returns the URL patterns of this {@link RequestMappingInfo};
* or instance with 0 patterns, never {@code null}.
@@ -148,6 +166,7 @@ public final class RequestMappingInfo implements RequestCondition<RequestMapping
*/
@Override
public RequestMappingInfo combine(RequestMappingInfo other) {
String name = combineNames(other);
PatternsRequestCondition patterns = this.patternsCondition.combine(other.patternsCondition);
RequestMethodsRequestCondition methods = this.methodsCondition.combine(other.methodsCondition);
ParamsRequestCondition params = this.paramsCondition.combine(other.paramsCondition);
@@ -156,7 +175,21 @@ public final class RequestMappingInfo implements RequestCondition<RequestMapping
ProducesRequestCondition produces = this.producesCondition.combine(other.producesCondition);
RequestConditionHolder custom = this.customConditionHolder.combine(other.customConditionHolder);
return new RequestMappingInfo(patterns, methods, params, headers, consumes, produces, custom.getCondition());
return new RequestMappingInfo(name, patterns,
methods, params, headers, consumes, produces, custom.getCondition());
}
private String combineNames(RequestMappingInfo other) {
if (this.name != null && other.name != null) {
String separator = RequestMappingInfoHandlerMethodMappingNamingStrategy.SEPARATOR;
return this.name + separator + other.name;
}
else if (this.name != null) {
return this.name;
}
else {
return (other.name != null ? other.name : null);
}
}
/**
@@ -188,7 +221,8 @@ public final class RequestMappingInfo implements RequestCondition<RequestMapping
return null;
}
return new RequestMappingInfo(patterns, methods, params, headers, consumes, produces, custom.getCondition());
return new RequestMappingInfo(this.name, patterns,
methods, params, headers, consumes, produces, custom.getCondition());
}

View File

@@ -55,6 +55,11 @@ import org.springframework.web.util.WebUtils;
*/
public abstract class RequestMappingInfoHandlerMapping extends AbstractHandlerMethodMapping<RequestMappingInfo> {
protected RequestMappingInfoHandlerMapping() {
setHandlerMethodMappingNamingStrategy(new RequestMappingInfoHandlerMethodMappingNamingStrategy());
}
/**
* Get the URL path patterns associated with this {@link RequestMappingInfo}.
*/

View File

@@ -0,0 +1,60 @@
/*
* Copyright 2002-2014 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.servlet.mvc.method;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.handler.HandlerMethodMappingNamingStrategy;
import java.lang.reflect.Method;
/**
* A {@link org.springframework.web.servlet.handler.HandlerMethodMappingNamingStrategy
* HandlerMethodMappingNamingStrategy} for {@code RequestMappingInfo}-based handler
* method mappings.
*
* If the {@code RequestMappingInfo} name attribute is set, its value is used.
* Otherwise the name is based on the capital letters of the class name,
* followed by "#" as a separator, and the method name. For example "TC#getFoo"
* for a class named TestController with method getFoo.
*
* @author Rossen Stoyanchev
* @since 4.1
*/
public class RequestMappingInfoHandlerMethodMappingNamingStrategy
implements HandlerMethodMappingNamingStrategy<RequestMappingInfo> {
/** Separator between the type and method-level parts of a HandlerMethod mapping name */
public static final String SEPARATOR = "#";
@Override
public String getName(HandlerMethod handlerMethod, RequestMappingInfo mapping) {
if (mapping.getName() != null) {
return mapping.getName();
}
StringBuilder sb = new StringBuilder();
String simpleTypeName = handlerMethod.getBeanType().getSimpleName();
for (int i = 0 ; i < simpleTypeName.length(); i++) {
if (Character.isUpperCase(simpleTypeName.charAt(i))) {
sb.append(simpleTypeName.charAt(i));
}
}
sb.append(SEPARATOR).append(handlerMethod.getMethod().getName());
return sb.toString();
}
}

View File

@@ -19,6 +19,7 @@ package org.springframework.web.servlet.mvc.method.annotation;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
@@ -29,6 +30,7 @@ import org.apache.commons.logging.LogFactory;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.target.EmptyTargetSource;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.NoUniqueBeanDefinitionException;
import org.springframework.cglib.core.SpringNamingPolicy;
import org.springframework.cglib.proxy.Callback;
import org.springframework.cglib.proxy.Enhancer;
@@ -50,9 +52,11 @@ import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.method.annotation.RequestParamMethodArgumentResolver;
import org.springframework.web.method.support.CompositeUriComponentsContributor;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
@@ -195,6 +199,43 @@ public class MvcUriComponentsBuilder extends UriComponentsBuilder {
return fromMethod(info.getControllerMethod(), info.getArgumentValues());
}
/**
* Create a {@link UriComponentsBuilder} from a request mapping identified
* by name. The configured
* {@link org.springframework.web.servlet.handler.HandlerMethodMappingNamingStrategy
* HandlerMethodMappingNamingStrategy} assigns a default name to every
* {@code @RequestMapping} method but an explicit name may also be assigned
* through the {@code @RequestMapping} name attribute.
*
* <p>This is intended for use in EL expressions, typically in JSPs or other
* view templates, which can use the convenience method:
* {@link org.springframework.web.servlet.support.RequestContext#getMvcUrl(String, Object...)
* RequestContext.getMvcUrl(String, Object...)}).
*
* <p>The default naming convention for mappings is based on the capital
* letters of the class name, followed by "#" as a separator, and the method
* name. For example "TC#getFoo" for a class named TestController with method
* getFoo. Use explicit names where the naming convention does not produce
* unique results.
*
* @param name the mapping name
* @param argumentValues argument values for the controller method; those values
* are important for {@code @RequestParam} and {@code @PathVariable} arguments
* but may be passed as {@code null} otherwise.
*
* @return the UriComponentsBuilder
*
* @throws java.lang.IllegalStateException if the mapping name is not found
* or there is no unique match
*/
public static UriComponentsBuilder fromMappingName(String name, Object... argumentValues) {
RequestMappingInfoHandlerMapping hm = getRequestMappingInfoHandlerMapping();
List<HandlerMethod> handlerMethods = hm.getHandlerMethodsForMappingName(name);
Assert.state(handlerMethods != null, "Mapping name not found: " + name);
Assert.state(handlerMethods.size() == 1, "No unique match for mapping name " + name + ": " + handlerMethods);
return fromMethod(handlerMethods.get(0).getMethod(), argumentValues);
}
/**
* Create a {@link UriComponentsBuilder} from the mapping of a controller method
* and an array of method argument values. The array of values must match the
@@ -263,6 +304,37 @@ public class MvcUriComponentsBuilder extends UriComponentsBuilder {
}
protected static CompositeUriComponentsContributor getConfiguredUriComponentsContributor() {
WebApplicationContext wac = getWebApplicationContext();
if (wac == null) {
return null;
}
try {
return wac.getBean(MVC_URI_COMPONENTS_CONTRIBUTOR_BEAN_NAME, CompositeUriComponentsContributor.class);
}
catch (NoSuchBeanDefinitionException ex) {
if (logger.isDebugEnabled()) {
logger.debug("No CompositeUriComponentsContributor bean with name '" +
MVC_URI_COMPONENTS_CONTRIBUTOR_BEAN_NAME + "'");
}
return null;
}
}
protected static RequestMappingInfoHandlerMapping getRequestMappingInfoHandlerMapping() {
WebApplicationContext wac = getWebApplicationContext();
Assert.notNull(wac, "Cannot lookup handler method mappings without WebApplicationContext");
try {
return wac.getBean(RequestMappingInfoHandlerMapping.class);
}
catch (NoUniqueBeanDefinitionException ex) {
throw new IllegalStateException("More than one RequestMappingInfoHandlerMapping beans found", ex);
}
catch (NoSuchBeanDefinitionException ex) {
throw new IllegalStateException("No RequestMappingInfoHandlerMapping bean", ex);
}
}
private static WebApplicationContext getWebApplicationContext() {
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
if (requestAttributes == null) {
logger.debug("No request bound to the current thread: is DispatcherSerlvet used?");
@@ -281,17 +353,7 @@ public class MvcUriComponentsBuilder extends UriComponentsBuilder {
logger.debug("No WebApplicationContext found: not in a DispatcherServlet request?");
return null;
}
try {
return wac.getBean(MVC_URI_COMPONENTS_CONTRIBUTOR_BEAN_NAME, CompositeUriComponentsContributor.class);
}
catch (NoSuchBeanDefinitionException ex) {
if (logger.isDebugEnabled()) {
logger.debug("No CompositeUriComponentsContributor bean with name '" +
MVC_URI_COMPONENTS_CONTRIBUTOR_BEAN_NAME + "'");
}
return null;
}
return wac;
}
/**

View File

@@ -123,23 +123,34 @@ public class PathVariableMethodArgumentResolver extends AbstractNamedValueMethod
}
@Override
public void contributeMethodArgument(MethodParameter parameter, Object value,
UriComponentsBuilder builder, Map<String, Object> uriVariables, ConversionService conversionService) {
public void contributeMethodArgument(MethodParameter param, Object value,
UriComponentsBuilder builder, Map<String, Object> uriVariables, ConversionService cs) {
if (Map.class.isAssignableFrom(parameter.getParameterType())) {
if (Map.class.isAssignableFrom(param.getParameterType())) {
return;
}
PathVariable annot = parameter.getParameterAnnotation(PathVariable.class);
String name = StringUtils.isEmpty(annot.value()) ? parameter.getParameterName() : annot.value();
if (conversionService != null) {
value = conversionService.convert(value, new TypeDescriptor(parameter), STRING_TYPE_DESCRIPTOR);
}
PathVariable annot = param.getParameterAnnotation(PathVariable.class);
String name = (StringUtils.isEmpty(annot.value()) ? param.getParameterName() : annot.value());
value = formatUriValue(cs, new TypeDescriptor(param), value);
uriVariables.put(name, value);
}
protected String formatUriValue(ConversionService cs, TypeDescriptor sourceType, Object value) {
if (value == null) {
return null;
}
else if (value instanceof String) {
return (String) value;
}
else if (cs != null) {
return (String) cs.convert(value, sourceType, STRING_TYPE_DESCRIPTOR);
}
else {
return value.toString();
}
}
private static class PathVariableNamedValueInfo extends NamedValueInfo {

View File

@@ -234,6 +234,7 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi
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()),

View File

@@ -47,6 +47,7 @@ import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.servlet.LocaleContextResolver;
import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.ThemeResolver;
import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder;
import org.springframework.web.util.HtmlUtils;
import org.springframework.web.util.UriTemplate;
import org.springframework.web.util.UrlPathHelper;
@@ -596,6 +597,18 @@ public class RequestContext {
return this.urlPathHelper.getOriginatingQueryString(this.request);
}
/**
* Return a URL derived from a controller method's {@code @RequestMapping}.
* This method internally uses
* {@link org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder#fromMappingName(String, Object...)
* MvcUriComponentsBuilder#fromMappingName(String, Object...)}. See its
* Javadoc for more details.
*/
public String getMvcUrl(String mappingName, Object... handlerMethodArguments) {
return MvcUriComponentsBuilder.fromMappingName(
mappingName, handlerMethodArguments).build().encode().toUriString();
}
/**
* Retrieve the message for the given code, using the "defaultHtmlEscape" setting.
* @param code code of the message