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 fbf4bbcb1d9..a2ea58bf5a3 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 @@ -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 { Class controllerType) { builder = getBaseUrlToUse(builder); - String mapping = getTypeRequestMapping(controllerType); + String mapping = getClassMapping(controllerType); return builder.path(mapping); } @@ -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}. + *

Note: 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. + *

Note: 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 { * @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 { 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)}. + *

Note that this is a shorthand version of {@link #controller(Class)} intended + * for inline use (with a static import), for example: + *

+	 * MvcUriComponentsBuilder.fromMethodCall(on(FooController.class).getFoo(1)).build();
+	 * 
+ *

Note: This method extracts values from "Forwarded" + * and "X-Forwarded-*" headers if found. See class-level docs. + * + * @param controllerType the target controller + */ + public static T on(Class 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)}. + *

This is a longer version of {@link #on(Class)}. It is needed with controller + * methods returning void as well for repeated invocations. + *

+	 * FooController fooController = controller(FooController.class);
+	 *
+	 * fooController.saveFoo(1, null);
+	 * builder = MvcUriComponentsBuilder.fromMethodCall(fooController);
+	 *
+	 * fooController.saveFoo(2, null);
+	 * builder = MvcUriComponentsBuilder.fromMethodCall(fooController);
+	 * 
+ *

Note: This method extracts values from "Forwarded" + * and "X-Forwarded-*" headers if found. See class-level docs. + * @param controllerType the target controller + */ + public static T controller(Class 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. *

The configured @@ -355,70 +443,89 @@ public class MvcUriComponentsBuilder { * @since 4.2 */ public static MethodArgumentBuilder fromMappingName(@Nullable UriComponentsBuilder builder, String name) { - RequestMappingInfoHandlerMapping handlerMapping = getRequestMappingInfoHandlerMapping(); - List handlerMethods = handlerMapping.getHandlerMethodsForMappingName(name); + WebApplicationContext wac = getWebApplicationContext(); + Assert.notNull(wac, "Cannot lookup handler method mappings without WebApplicationContext"); + RequestMappingInfoHandlerMapping mapping = wac.getBean(RequestMappingInfoHandlerMapping.class); + List 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}. *

Note: 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}. *

Note: 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}. + *

Note: 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}. + *

Note: 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}. + *

Note: 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 { } 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 { 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 { } 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 { } // 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 { 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)}. - *

Note that this is a shorthand version of {@link #controller(Class)} intended - * for inline use (with a static import), for example: - *

-	 * MvcUriComponentsBuilder.fromMethodCall(on(FooController.class).getFoo(1)).build();
-	 * 
- *

Note: This method extracts values from "Forwarded" - * and "X-Forwarded-*" headers if found. See class-level docs. - * - * @param controllerType the target controller - */ - public static T on(Class 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)}. - *

This is a longer version of {@link #on(Class)}. It is needed with controller - * methods returning void as well for repeated invocations. - *

-	 * FooController fooController = controller(FooController.class);
-	 *
-	 * fooController.saveFoo(1, null);
-	 * builder = MvcUriComponentsBuilder.fromMethodCall(fooController);
-	 *
-	 * fooController.saveFoo(2, null);
-	 * builder = MvcUriComponentsBuilder.fromMethodCall(fooController);
-	 * 
- *

Note: This method extracts values from "Forwarded" - * and "X-Forwarded-*" headers if found. See class-level docs. - * @param controllerType the target controller - */ - public static T controller(Class controllerType) { - Assert.notNull(controllerType, "'controllerType' must not be null"); - return initProxy(controllerType, new ControllerMethodInvocationInterceptor(controllerType)); - } - - @SuppressWarnings("unchecked") - private static 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}. - *

Note: 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}. - *

Note: 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}. - *

Note: 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}. - *

Note: 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}. - *

Note: 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 { Assert.state(this.argumentValues != null, "Not initialized yet"); return this.argumentValues; } + + + @SuppressWarnings("unchecked") + private static 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; + } + } }