Browse Source

Polish MvcUriComponentsBuilder

pull/1858/merge
Rossen Stoyanchev 8 years ago
parent
commit
dad9ed83b7
  1. 449
      spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/MvcUriComponentsBuilder.java

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

@ -31,7 +31,6 @@ import org.apache.commons.logging.LogFactory; @@ -31,7 +31,6 @@ 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;
@ -175,7 +174,7 @@ public class MvcUriComponentsBuilder { @@ -175,7 +174,7 @@ public class MvcUriComponentsBuilder {
Class<?> controllerType) {
builder = getBaseUrlToUse(builder);
String mapping = getTypeRequestMapping(controllerType);
String mapping = getClassMapping(controllerType);
return builder.path(mapping);
}
@ -222,6 +221,49 @@ public class MvcUriComponentsBuilder { @@ -222,6 +221,49 @@ public class MvcUriComponentsBuilder {
return fromMethodInternal(builder, controllerType, method, args);
}
/**
* 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
* signature of the controller method. Values for {@code @RequestParam} and
* {@code @PathVariable} are used for building the URI (via implementations of
* {@link org.springframework.web.method.support.UriComponentsContributor
* UriComponentsContributor}) while remaining argument values are ignored and
* can be {@code null}.
* <p><strong>Note:</strong> This method extracts values from "Forwarded"
* and "X-Forwarded-*" headers if found. See class-level docs.
* @param controllerType the controller type
* @param method the controller method
* @param args argument values for the controller method
* @return a UriComponentsBuilder instance, never {@code null}
* @since 4.2
*/
public static UriComponentsBuilder fromMethod(Class<?> controllerType, Method method, Object... args) {
return fromMethodInternal(null, controllerType, method, args);
}
/**
* An alternative to {@link #fromMethod(Class, Method, Object...)}
* that accepts a {@code UriComponentsBuilder} representing the base URL.
* This is useful when using MvcUriComponentsBuilder outside the context of
* processing a request or to apply a custom baseUrl not matching the
* current request.
* <p><strong>Note:</strong> This method extracts values from "Forwarded"
* and "X-Forwarded-*" headers if found. See class-level docs.
* @param baseUrl the builder for the base URL; the builder will be cloned
* and therefore not modified and may be re-used for further calls.
* @param controllerType the controller type
* @param method the controller method
* @param args argument values for the controller method
* @return a UriComponentsBuilder instance (never {@code null})
* @since 4.2
*/
public static UriComponentsBuilder fromMethod(UriComponentsBuilder baseUrl,
@Nullable Class<?> controllerType, Method method, Object... args) {
return fromMethodInternal(baseUrl,
(controllerType != null ? controllerType : method.getDeclaringClass()), method, args);
}
/**
* Create a {@link UriComponentsBuilder} by invoking a "mock" controller method.
* The controller method and the supplied argument values are then used to
@ -258,6 +300,8 @@ public class MvcUriComponentsBuilder { @@ -258,6 +300,8 @@ public class MvcUriComponentsBuilder {
* @param info either the value returned from a "mock" controller
* invocation or the "mock" controller itself after an invocation
* @return a UriComponents instance
* @see #on(Class)
* @see #controller(Class)
*/
public static UriComponentsBuilder fromMethodCall(Object info) {
Assert.isInstanceOf(MethodInvocationInfo.class, info, "MethodInvocationInfo required");
@ -290,6 +334,50 @@ public class MvcUriComponentsBuilder { @@ -290,6 +334,50 @@ public class MvcUriComponentsBuilder {
return fromMethodInternal(builder, controllerType, method, arguments);
}
/**
* Return a "mock" controller instance. When an {@code @RequestMapping} method
* on the controller is invoked, the supplied argument values are remembered
* and the result can then be used to create a {@code UriComponentsBuilder}
* via {@link #fromMethodCall(Object)}.
* <p>Note that this is a shorthand version of {@link #controller(Class)} intended
* for inline use (with a static import), for example:
* <pre class="code">
* MvcUriComponentsBuilder.fromMethodCall(on(FooController.class).getFoo(1)).build();
* </pre>
* <p><strong>Note:</strong> This method extracts values from "Forwarded"
* and "X-Forwarded-*" headers if found. See class-level docs.
*
* @param controllerType the target controller
*/
public static <T> T on(Class<T> controllerType) {
return controller(controllerType);
}
/**
* Return a "mock" controller instance. When an {@code @RequestMapping} method
* on the controller is invoked, the supplied argument values are remembered
* and the result can then be used to create {@code UriComponentsBuilder} via
* {@link #fromMethodCall(Object)}.
* <p>This is a longer version of {@link #on(Class)}. It is needed with controller
* methods returning void as well for repeated invocations.
* <pre class="code">
* FooController fooController = controller(FooController.class);
*
* fooController.saveFoo(1, null);
* builder = MvcUriComponentsBuilder.fromMethodCall(fooController);
*
* fooController.saveFoo(2, null);
* builder = MvcUriComponentsBuilder.fromMethodCall(fooController);
* </pre>
* <p><strong>Note:</strong> This method extracts values from "Forwarded"
* and "X-Forwarded-*" headers if found. See class-level docs.
* @param controllerType the target controller
*/
public static <T> T controller(Class<T> controllerType) {
Assert.notNull(controllerType, "'controllerType' must not be null");
return ControllerMethodInvocationInterceptor.initProxy(controllerType, null);
}
/**
* Create a URL from the name of a Spring MVC controller method's request mapping.
* <p>The configured
@ -355,70 +443,89 @@ public class MvcUriComponentsBuilder { @@ -355,70 +443,89 @@ public class MvcUriComponentsBuilder {
* @since 4.2
*/
public static MethodArgumentBuilder fromMappingName(@Nullable UriComponentsBuilder builder, String name) {
RequestMappingInfoHandlerMapping handlerMapping = getRequestMappingInfoHandlerMapping();
List<HandlerMethod> handlerMethods = handlerMapping.getHandlerMethodsForMappingName(name);
WebApplicationContext wac = getWebApplicationContext();
Assert.notNull(wac, "Cannot lookup handler method mappings without WebApplicationContext");
RequestMappingInfoHandlerMapping mapping = wac.getBean(RequestMappingInfoHandlerMapping.class);
List<HandlerMethod> handlerMethods = mapping.getHandlerMethodsForMappingName(name);
if (handlerMethods == null) {
throw new IllegalArgumentException("Mapping mappingName not found: " + name);
throw new IllegalArgumentException("Mapping not found: " + name);
}
if (handlerMethods.size() != 1) {
throw new IllegalArgumentException("No unique match for mapping mappingName " +
name + ": " + handlerMethods);
else if (handlerMethods.size() != 1) {
throw new IllegalArgumentException("No unique match for mapping " + name + ": " + handlerMethods);
}
else {
HandlerMethod handlerMethod = handlerMethods.get(0);
Class<?> controllerType = handlerMethod.getBeanType();
Method method = handlerMethod.getMethod();
return new MethodArgumentBuilder(builder, controllerType, method);
}
HandlerMethod handlerMethod = handlerMethods.get(0);
Class<?> controllerType = handlerMethod.getBeanType();
Method method = handlerMethod.getMethod();
return new MethodArgumentBuilder(builder, controllerType, method);
}
// Instance methods, relative to a base UriComponentsBuilder...
/**
* 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
* signature of the controller method. Values for {@code @RequestParam} and
* {@code @PathVariable} are used for building the URI (via implementations of
* {@link org.springframework.web.method.support.UriComponentsContributor
* UriComponentsContributor}) while remaining argument values are ignored and
* can be {@code null}.
* An alternative to {@link #fromController(Class)} for use with an instance
* of this class created via a call to {@link #relativeTo}.
* <p><strong>Note:</strong> This method extracts values from "Forwarded"
* and "X-Forwarded-*" headers if found. See class-level docs.
* @param controllerType the controller type
* @param method the controller method
* @param args argument values for the controller method
* @return a UriComponentsBuilder instance, never {@code null}
* @since 4.2
*/
public static UriComponentsBuilder fromMethod(Class<?> controllerType, Method method, Object... args) {
return fromMethodInternal(null, controllerType, method, args);
public UriComponentsBuilder withController(Class<?> controllerType) {
return fromController(this.baseUrl, controllerType);
}
/**
* An alternative to {@link #fromMethod(Class, Method, Object...)}
* that accepts a {@code UriComponentsBuilder} representing the base URL.
* This is useful when using MvcUriComponentsBuilder outside the context of
* processing a request or to apply a custom baseUrl not matching the
* current request.
* An alternative to {@link #fromMethodName(Class, String, Object...)}} for
* use with an instance of this class created via {@link #relativeTo}.
* <p><strong>Note:</strong> This method extracts values from "Forwarded"
* and "X-Forwarded-*" headers if found. See class-level docs.
* @param baseUrl the builder for the base URL; the builder will be cloned
* and therefore not modified and may be re-used for further calls.
* @param controllerType the controller type
* @param method the controller method
* @param args argument values for the controller method
* @return a UriComponentsBuilder instance (never {@code null})
* @since 4.2
*/
public static UriComponentsBuilder fromMethod(UriComponentsBuilder baseUrl,
@Nullable Class<?> controllerType, Method method, Object... args) {
public UriComponentsBuilder withMethodName(Class<?> controllerType, String methodName, Object... args) {
return fromMethodName(this.baseUrl, controllerType, methodName, args);
}
return fromMethodInternal(baseUrl,
(controllerType != null ? controllerType : method.getDeclaringClass()), method, args);
/**
* An alternative to {@link #fromMethodCall(Object)} for use with an instance
* of this class created via {@link #relativeTo}.
* <p><strong>Note:</strong> This method extracts values from "Forwarded"
* and "X-Forwarded-*" headers if found. See class-level docs.
* @since 4.2
*/
public UriComponentsBuilder withMethodCall(Object invocationInfo) {
return fromMethodCall(this.baseUrl, invocationInfo);
}
/**
* An alternative to {@link #fromMappingName(String)} for use with an instance
* of this class created via {@link #relativeTo}.
* <p><strong>Note:</strong> This method extracts values from "Forwarded"
* and "X-Forwarded-*" headers if found. See class-level docs.
* @since 4.2
*/
public MethodArgumentBuilder withMappingName(String mappingName) {
return fromMappingName(this.baseUrl, mappingName);
}
/**
* An alternative to {@link #fromMethod(Class, Method, Object...)}
* for use with an instance of this class created via {@link #relativeTo}.
* <p><strong>Note:</strong> This method extracts values from "Forwarded"
* and "X-Forwarded-*" headers if found. See class-level docs.
* @since 4.2
*/
public UriComponentsBuilder withMethod(Class<?> controllerType, Method method, Object... args) {
return fromMethod(this.baseUrl, controllerType, method, args);
}
private static UriComponentsBuilder fromMethodInternal(@Nullable UriComponentsBuilder baseUrl,
Class<?> controllerType, Method method, Object... args) {
baseUrl = getBaseUrlToUse(baseUrl);
String typePath = getTypeRequestMapping(controllerType);
String methodPath = getMethodRequestMapping(method);
String typePath = getClassMapping(controllerType);
String methodPath = getMethodMapping(method);
String path = pathMatcher.combine(typePath, methodPath);
baseUrl.path(path);
UriComponents uriComponents = applyContributors(baseUrl, method, args);
@ -426,21 +533,18 @@ public class MvcUriComponentsBuilder { @@ -426,21 +533,18 @@ public class MvcUriComponentsBuilder {
}
private static UriComponentsBuilder getBaseUrlToUse(@Nullable UriComponentsBuilder baseUrl) {
if (baseUrl != null) {
return baseUrl.cloneBuilder();
}
else {
return ServletUriComponentsBuilder.fromCurrentServletMapping();
}
return baseUrl == null ?
ServletUriComponentsBuilder.fromCurrentServletMapping() :
baseUrl.cloneBuilder();
}
private static String getTypeRequestMapping(Class<?> controllerType) {
private static String getClassMapping(Class<?> controllerType) {
Assert.notNull(controllerType, "'controllerType' must not be null");
RequestMapping requestMapping = AnnotatedElementUtils.findMergedAnnotation(controllerType, RequestMapping.class);
if (requestMapping == null) {
RequestMapping mapping = AnnotatedElementUtils.findMergedAnnotation(controllerType, RequestMapping.class);
if (mapping == null) {
return "/";
}
String[] paths = requestMapping.path();
String[] paths = mapping.path();
if (ObjectUtils.isEmpty(paths) || StringUtils.isEmpty(paths[0])) {
return "/";
}
@ -450,7 +554,7 @@ public class MvcUriComponentsBuilder { @@ -450,7 +554,7 @@ public class MvcUriComponentsBuilder {
return paths[0];
}
private static String getMethodRequestMapping(Method method) {
private static String getMethodMapping(Method method) {
Assert.notNull(method, "'method' must not be null");
RequestMapping requestMapping = AnnotatedElementUtils.findMergedAnnotation(method, RequestMapping.class);
if (requestMapping == null) {
@ -488,9 +592,9 @@ public class MvcUriComponentsBuilder { @@ -488,9 +592,9 @@ public class MvcUriComponentsBuilder {
}
private static UriComponents applyContributors(UriComponentsBuilder builder, Method method, Object... args) {
CompositeUriComponentsContributor contributor = getConfiguredUriComponentsContributor();
CompositeUriComponentsContributor contributor = getUriComponentsContributor();
if (contributor == null) {
logger.debug("Using default CompositeUriComponentsContributor");
logger.trace("Using default CompositeUriComponentsContributor");
contributor = defaultUriComponentsContributor;
}
@ -509,11 +613,12 @@ public class MvcUriComponentsBuilder { @@ -509,11 +613,12 @@ public class MvcUriComponentsBuilder {
}
// We may not have all URI var values, expand only what we have
return builder.build().expand(name -> uriVars.getOrDefault(name, UriComponents.UriTemplateVariables.SKIP_VALUE));
return builder.build().expand(name ->
uriVars.getOrDefault(name, UriComponents.UriTemplateVariables.SKIP_VALUE));
}
@Nullable
private static CompositeUriComponentsContributor getConfiguredUriComponentsContributor() {
private static CompositeUriComponentsContributor getUriComponentsContributor() {
WebApplicationContext wac = getWebApplicationContext();
if (wac == null) {
return null;
@ -522,193 +627,30 @@ public class MvcUriComponentsBuilder { @@ -522,193 +627,30 @@ public class MvcUriComponentsBuilder {
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 + "'");
if (logger.isTraceEnabled()) {
logger.trace("No CompositeUriComponentsContributor");
}
return null;
}
}
private 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);
}
}
@Nullable
private static WebApplicationContext getWebApplicationContext() {
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
if (requestAttributes == null) {
logger.debug("No request bound to the current thread: not in a DispatcherServlet request?");
logger.trace("No request bound to the current thread: not in a DispatcherServlet request?");
return null;
}
HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest();
WebApplicationContext wac = (WebApplicationContext)
request.getAttribute(DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE);
String attributeName = DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE;
WebApplicationContext wac = (WebApplicationContext) request.getAttribute(attributeName);
if (wac == null) {
logger.debug("No WebApplicationContext found: not in a DispatcherServlet request?");
logger.trace("No WebApplicationContext found: not in a DispatcherServlet request?");
return null;
}
return wac;
}
/**
* Return a "mock" controller instance. When an {@code @RequestMapping} method
* on the controller is invoked, the supplied argument values are remembered
* and the result can then be used to create a {@code UriComponentsBuilder}
* via {@link #fromMethodCall(Object)}.
* <p>Note that this is a shorthand version of {@link #controller(Class)} intended
* for inline use (with a static import), for example:
* <pre class="code">
* MvcUriComponentsBuilder.fromMethodCall(on(FooController.class).getFoo(1)).build();
* </pre>
* <p><strong>Note:</strong> This method extracts values from "Forwarded"
* and "X-Forwarded-*" headers if found. See class-level docs.
*
* @param controllerType the target controller
*/
public static <T> T on(Class<T> controllerType) {
return controller(controllerType);
}
/**
* Return a "mock" controller instance. When an {@code @RequestMapping} method
* on the controller is invoked, the supplied argument values are remembered
* and the result can then be used to create {@code UriComponentsBuilder} via
* {@link #fromMethodCall(Object)}.
* <p>This is a longer version of {@link #on(Class)}. It is needed with controller
* methods returning void as well for repeated invocations.
* <pre class="code">
* FooController fooController = controller(FooController.class);
*
* fooController.saveFoo(1, null);
* builder = MvcUriComponentsBuilder.fromMethodCall(fooController);
*
* fooController.saveFoo(2, null);
* builder = MvcUriComponentsBuilder.fromMethodCall(fooController);
* </pre>
* <p><strong>Note:</strong> This method extracts values from "Forwarded"
* and "X-Forwarded-*" headers if found. See class-level docs.
* @param controllerType the target controller
*/
public static <T> T controller(Class<T> controllerType) {
Assert.notNull(controllerType, "'controllerType' must not be null");
return initProxy(controllerType, new ControllerMethodInvocationInterceptor(controllerType));
}
@SuppressWarnings("unchecked")
private static <T> T initProxy(Class<?> type, ControllerMethodInvocationInterceptor interceptor) {
if (type == Object.class) {
return (T) interceptor;
}
else if (type.isInterface()) {
ProxyFactory factory = new ProxyFactory(EmptyTargetSource.INSTANCE);
factory.addInterface(type);
factory.addInterface(MethodInvocationInfo.class);
factory.addAdvice(interceptor);
return (T) factory.getProxy();
}
else {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(type);
enhancer.setInterfaces(new Class<?>[] {MethodInvocationInfo.class});
enhancer.setNamingPolicy(SpringNamingPolicy.INSTANCE);
enhancer.setCallbackType(org.springframework.cglib.proxy.MethodInterceptor.class);
Class<?> proxyClass = enhancer.createClass();
Object proxy = null;
if (objenesis.isWorthTrying()) {
try {
proxy = objenesis.newInstance(proxyClass, enhancer.getUseCache());
}
catch (ObjenesisException ex) {
logger.debug("Unable to instantiate controller proxy using Objenesis, " +
"falling back to regular construction", ex);
}
}
if (proxy == null) {
try {
proxy = ReflectionUtils.accessibleConstructor(proxyClass).newInstance();
}
catch (Throwable ex) {
throw new IllegalStateException("Unable to instantiate controller proxy using Objenesis, " +
"and regular controller instantiation via default constructor fails as well", ex);
}
}
((Factory) proxy).setCallbacks(new Callback[] {interceptor});
return (T) proxy;
}
}
/**
* An alternative to {@link #fromController(Class)} for use with an instance
* of this class created via a call to {@link #relativeTo}.
* <p><strong>Note:</strong> This method extracts values from "Forwarded"
* and "X-Forwarded-*" headers if found. See class-level docs.
* @since 4.2
*/
public UriComponentsBuilder withController(Class<?> controllerType) {
return fromController(this.baseUrl, controllerType);
}
/**
* An alternative to {@link #fromMethodName(Class, String, Object...)}} for
* use with an instance of this class created via {@link #relativeTo}.
* <p><strong>Note:</strong> This method extracts values from "Forwarded"
* and "X-Forwarded-*" headers if found. See class-level docs.
* @since 4.2
*/
public UriComponentsBuilder withMethodName(Class<?> controllerType, String methodName, Object... args) {
return fromMethodName(this.baseUrl, controllerType, methodName, args);
}
/**
* An alternative to {@link #fromMethodCall(Object)} for use with an instance
* of this class created via {@link #relativeTo}.
* <p><strong>Note:</strong> This method extracts values from "Forwarded"
* and "X-Forwarded-*" headers if found. See class-level docs.
* @since 4.2
*/
public UriComponentsBuilder withMethodCall(Object invocationInfo) {
return fromMethodCall(this.baseUrl, invocationInfo);
}
/**
* An alternative to {@link #fromMappingName(String)} for use with an instance
* of this class created via {@link #relativeTo}.
* <p><strong>Note:</strong> This method extracts values from "Forwarded"
* and "X-Forwarded-*" headers if found. See class-level docs.
* @since 4.2
*/
public MethodArgumentBuilder withMappingName(String mappingName) {
return fromMappingName(this.baseUrl, mappingName);
}
/**
* An alternative to {@link #fromMethod(Class, Method, Object...)}
* for use with an instance of this class created via {@link #relativeTo}.
* <p><strong>Note:</strong> This method extracts values from "Forwarded"
* and "X-Forwarded-*" headers if found. See class-level docs.
* @since 4.2
*/
public UriComponentsBuilder withMethod(Class<?> controllerType, Method method, Object... args) {
return fromMethod(this.baseUrl, controllerType, method, args);
}
public interface MethodInvocationInfo {
@ -787,6 +729,61 @@ public class MvcUriComponentsBuilder { @@ -787,6 +729,61 @@ public class MvcUriComponentsBuilder {
Assert.state(this.argumentValues != null, "Not initialized yet");
return this.argumentValues;
}
@SuppressWarnings("unchecked")
private static <T> T initProxy(
Class<?> controllerType, @Nullable ControllerMethodInvocationInterceptor interceptor) {
interceptor = interceptor != null ?
interceptor : new ControllerMethodInvocationInterceptor(controllerType);
if (controllerType == Object.class) {
return (T) interceptor;
}
else if (controllerType.isInterface()) {
ProxyFactory factory = new ProxyFactory(EmptyTargetSource.INSTANCE);
factory.addInterface(controllerType);
factory.addInterface(MethodInvocationInfo.class);
factory.addAdvice(interceptor);
return (T) factory.getProxy();
}
else {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(controllerType);
enhancer.setInterfaces(new Class<?>[] {MethodInvocationInfo.class});
enhancer.setNamingPolicy(SpringNamingPolicy.INSTANCE);
enhancer.setCallbackType(org.springframework.cglib.proxy.MethodInterceptor.class);
Class<?> proxyClass = enhancer.createClass();
Object proxy = null;
if (objenesis.isWorthTrying()) {
try {
proxy = objenesis.newInstance(proxyClass, enhancer.getUseCache());
}
catch (ObjenesisException ex) {
logger.debug("Unable to instantiate controller proxy using Objenesis, " +
"falling back to regular construction", ex);
}
}
if (proxy == null) {
try {
proxy = ReflectionUtils.accessibleConstructor(proxyClass).newInstance();
}
catch (Throwable ex) {
throw new IllegalStateException("Unable to instantiate controller proxy using Objenesis, " +
"and regular controller instantiation via default constructor fails as well", ex);
}
}
((Factory) proxy).setCallbacks(new Callback[] {interceptor});
return (T) proxy;
}
}
}

Loading…
Cancel
Save