001/* 002 * Copyright 2002-2020 the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * https://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package org.springframework.web.servlet.mvc.method.annotation; 018 019import java.lang.reflect.Method; 020import java.util.Arrays; 021import java.util.HashMap; 022import java.util.List; 023import java.util.Map; 024import java.util.Set; 025 026import javax.servlet.http.HttpServletRequest; 027 028import org.aopalliance.intercept.MethodInterceptor; 029import org.apache.commons.logging.Log; 030import org.apache.commons.logging.LogFactory; 031 032import org.springframework.aop.framework.ProxyFactory; 033import org.springframework.aop.target.EmptyTargetSource; 034import org.springframework.beans.factory.NoSuchBeanDefinitionException; 035import org.springframework.cglib.core.SpringNamingPolicy; 036import org.springframework.cglib.proxy.Callback; 037import org.springframework.cglib.proxy.Enhancer; 038import org.springframework.cglib.proxy.Factory; 039import org.springframework.cglib.proxy.MethodProxy; 040import org.springframework.core.DefaultParameterNameDiscoverer; 041import org.springframework.core.MethodIntrospector; 042import org.springframework.core.MethodParameter; 043import org.springframework.core.ParameterNameDiscoverer; 044import org.springframework.core.annotation.AnnotatedElementUtils; 045import org.springframework.core.annotation.SynthesizingMethodParameter; 046import org.springframework.lang.Nullable; 047import org.springframework.objenesis.ObjenesisException; 048import org.springframework.objenesis.SpringObjenesis; 049import org.springframework.util.AntPathMatcher; 050import org.springframework.util.Assert; 051import org.springframework.util.ObjectUtils; 052import org.springframework.util.PathMatcher; 053import org.springframework.util.ReflectionUtils; 054import org.springframework.util.ReflectionUtils.MethodFilter; 055import org.springframework.util.StringUtils; 056import org.springframework.web.bind.annotation.RequestMapping; 057import org.springframework.web.context.WebApplicationContext; 058import org.springframework.web.context.request.RequestAttributes; 059import org.springframework.web.context.request.RequestContextHolder; 060import org.springframework.web.context.request.ServletRequestAttributes; 061import org.springframework.web.method.HandlerMethod; 062import org.springframework.web.method.annotation.RequestParamMethodArgumentResolver; 063import org.springframework.web.method.support.CompositeUriComponentsContributor; 064import org.springframework.web.servlet.DispatcherServlet; 065import org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping; 066import org.springframework.web.servlet.support.ServletUriComponentsBuilder; 067import org.springframework.web.util.UriComponentsBuilder; 068 069/** 070 * Creates instances of {@link org.springframework.web.util.UriComponentsBuilder} 071 * by pointing to {@code @RequestMapping} methods on Spring MVC controllers. 072 * 073 * <p>There are several groups of methods: 074 * <ul> 075 * <li>Static {@code fromXxx(...)} methods to prepare links using information 076 * from the current request as determined by a call to 077 * {@link org.springframework.web.servlet.support.ServletUriComponentsBuilder#fromCurrentServletMapping()}. 078 * <li>Static {@code fromXxx(UriComponentsBuilder,...)} methods can be given 079 * a baseUrl when operating outside the context of a request. 080 * <li>Instance-based {@code withXxx(...)} methods where an instance of 081 * MvcUriComponentsBuilder is created with a baseUrl via 082 * {@link #relativeTo(org.springframework.web.util.UriComponentsBuilder)}. 083 * </ul> 084 * 085 * <p><strong>Note:</strong> This class uses values from "Forwarded" 086 * (<a href="https://tools.ietf.org/html/rfc7239">RFC 7239</a>), 087 * "X-Forwarded-Host", "X-Forwarded-Port", and "X-Forwarded-Proto" headers, 088 * if present, in order to reflect the client-originated protocol and address. 089 * Consider using the {@code ForwardedHeaderFilter} in order to choose from a 090 * central place whether to extract and use, or to discard such headers. 091 * See the Spring Framework reference for more on this filter. 092 * 093 * @author Oliver Gierke 094 * @author Rossen Stoyanchev 095 * @author Sam Brannen 096 * @since 4.0 097 */ 098public class MvcUriComponentsBuilder { 099 100 /** 101 * Well-known name for the {@link CompositeUriComponentsContributor} object in the bean factory. 102 */ 103 public static final String MVC_URI_COMPONENTS_CONTRIBUTOR_BEAN_NAME = "mvcUriComponentsContributor"; 104 105 106 private static final Log logger = LogFactory.getLog(MvcUriComponentsBuilder.class); 107 108 private static final SpringObjenesis objenesis = new SpringObjenesis(); 109 110 private static final PathMatcher pathMatcher = new AntPathMatcher(); 111 112 private static final ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer(); 113 114 private static final CompositeUriComponentsContributor defaultUriComponentsContributor; 115 116 static { 117 defaultUriComponentsContributor = new CompositeUriComponentsContributor( 118 new PathVariableMethodArgumentResolver(), new RequestParamMethodArgumentResolver(false)); 119 } 120 121 private final UriComponentsBuilder baseUrl; 122 123 124 /** 125 * Default constructor. Protected to prevent direct instantiation. 126 * @see #fromController(Class) 127 * @see #fromMethodName(Class, String, Object...) 128 * @see #fromMethodCall(Object) 129 * @see #fromMappingName(String) 130 * @see #fromMethod(Class, Method, Object...) 131 */ 132 protected MvcUriComponentsBuilder(UriComponentsBuilder baseUrl) { 133 Assert.notNull(baseUrl, "'baseUrl' is required"); 134 this.baseUrl = baseUrl; 135 } 136 137 138 /** 139 * Create an instance of this class with a base URL. After that calls to one 140 * of the instance based {@code withXxx(...}} methods will create URLs relative 141 * to the given base URL. 142 */ 143 public static MvcUriComponentsBuilder relativeTo(UriComponentsBuilder baseUrl) { 144 return new MvcUriComponentsBuilder(baseUrl); 145 } 146 147 148 /** 149 * Create a {@link UriComponentsBuilder} from the mapping of a controller class 150 * and current request information including Servlet mapping. If the controller 151 * contains multiple mappings, only the first one is used. 152 * <p><strong>Note:</strong> This method extracts values from "Forwarded" 153 * and "X-Forwarded-*" headers if found. See class-level docs. 154 * @param controllerType the controller to build a URI for 155 * @return a UriComponentsBuilder instance (never {@code null}) 156 */ 157 public static UriComponentsBuilder fromController(Class<?> controllerType) { 158 return fromController(null, controllerType); 159 } 160 161 /** 162 * An alternative to {@link #fromController(Class)} that accepts a 163 * {@code UriComponentsBuilder} representing the base URL. This is useful 164 * when using MvcUriComponentsBuilder outside the context of processing a 165 * request or to apply a custom baseUrl not matching the current request. 166 * <p><strong>Note:</strong> This method extracts values from "Forwarded" 167 * and "X-Forwarded-*" headers if found. See class-level docs. 168 * @param builder the builder for the base URL; the builder will be cloned 169 * and therefore not modified and may be re-used for further calls. 170 * @param controllerType the controller to build a URI for 171 * @return a UriComponentsBuilder instance (never {@code null}) 172 */ 173 public static UriComponentsBuilder fromController(@Nullable UriComponentsBuilder builder, 174 Class<?> controllerType) { 175 176 builder = getBaseUrlToUse(builder); 177 178 // Externally configured prefix via PathConfigurer.. 179 String prefix = getPathPrefix(controllerType); 180 builder.path(prefix); 181 182 String mapping = getClassMapping(controllerType); 183 builder.path(mapping); 184 185 return builder; 186 } 187 188 /** 189 * Create a {@link UriComponentsBuilder} from the mapping of a controller 190 * method and an array of method argument values. This method delegates 191 * to {@link #fromMethod(Class, Method, Object...)}. 192 * <p><strong>Note:</strong> This method extracts values from "Forwarded" 193 * and "X-Forwarded-*" headers if found. See class-level docs. 194 * @param controllerType the controller 195 * @param methodName the method name 196 * @param args the argument values 197 * @return a UriComponentsBuilder instance, never {@code null} 198 * @throws IllegalArgumentException if there is no matching or 199 * if there is more than one matching method 200 */ 201 public static UriComponentsBuilder fromMethodName(Class<?> controllerType, 202 String methodName, Object... args) { 203 204 Method method = getMethod(controllerType, methodName, args); 205 return fromMethodInternal(null, controllerType, method, args); 206 } 207 208 /** 209 * An alternative to {@link #fromMethodName(Class, String, Object...)} that 210 * accepts a {@code UriComponentsBuilder} representing the base URL. This is 211 * useful when using MvcUriComponentsBuilder outside the context of processing 212 * a request or to apply a custom baseUrl not matching the current request. 213 * <p><strong>Note:</strong> This method extracts values from "Forwarded" 214 * and "X-Forwarded-*" headers if found. See class-level docs. 215 * @param builder the builder for the base URL; the builder will be cloned 216 * and therefore not modified and may be re-used for further calls. 217 * @param controllerType the controller 218 * @param methodName the method name 219 * @param args the argument values 220 * @return a UriComponentsBuilder instance, never {@code null} 221 * @throws IllegalArgumentException if there is no matching or 222 * if there is more than one matching method 223 */ 224 public static UriComponentsBuilder fromMethodName(UriComponentsBuilder builder, 225 Class<?> controllerType, String methodName, Object... args) { 226 227 Method method = getMethod(controllerType, methodName, args); 228 return fromMethodInternal(builder, controllerType, method, args); 229 } 230 231 /** 232 * Create a {@link UriComponentsBuilder} from the mapping of a controller method 233 * and an array of method argument values. The array of values must match the 234 * signature of the controller method. Values for {@code @RequestParam} and 235 * {@code @PathVariable} are used for building the URI (via implementations of 236 * {@link org.springframework.web.method.support.UriComponentsContributor 237 * UriComponentsContributor}) while remaining argument values are ignored and 238 * can be {@code null}. 239 * <p><strong>Note:</strong> This method extracts values from "Forwarded" 240 * and "X-Forwarded-*" headers if found. See class-level docs. 241 * @param controllerType the controller type 242 * @param method the controller method 243 * @param args argument values for the controller method 244 * @return a UriComponentsBuilder instance, never {@code null} 245 * @since 4.2 246 */ 247 public static UriComponentsBuilder fromMethod(Class<?> controllerType, Method method, Object... args) { 248 return fromMethodInternal(null, controllerType, method, args); 249 } 250 251 /** 252 * An alternative to {@link #fromMethod(Class, Method, Object...)} 253 * that accepts a {@code UriComponentsBuilder} representing the base URL. 254 * This is useful when using MvcUriComponentsBuilder outside the context of 255 * processing a request or to apply a custom baseUrl not matching the 256 * current request. 257 * <p><strong>Note:</strong> This method extracts values from "Forwarded" 258 * and "X-Forwarded-*" headers if found. See class-level docs. 259 * @param baseUrl the builder for the base URL; the builder will be cloned 260 * and therefore not modified and may be re-used for further calls. 261 * @param controllerType the controller type 262 * @param method the controller method 263 * @param args argument values for the controller method 264 * @return a UriComponentsBuilder instance (never {@code null}) 265 * @since 4.2 266 */ 267 public static UriComponentsBuilder fromMethod(UriComponentsBuilder baseUrl, 268 @Nullable Class<?> controllerType, Method method, Object... args) { 269 270 return fromMethodInternal(baseUrl, 271 (controllerType != null ? controllerType : method.getDeclaringClass()), method, args); 272 } 273 274 /** 275 * Create a {@link UriComponentsBuilder} by invoking a "mock" controller method. 276 * The controller method and the supplied argument values are then used to 277 * delegate to {@link #fromMethod(Class, Method, Object...)}. 278 * <p>For example, given this controller: 279 * <pre class="code"> 280 * @RequestMapping("/people/{id}/addresses") 281 * class AddressController { 282 * 283 * @RequestMapping("/{country}") 284 * public HttpEntity<Void> getAddressesForCountry(@PathVariable String country) { ... } 285 * 286 * @RequestMapping(value="/", method=RequestMethod.POST) 287 * public void addAddress(Address address) { ... } 288 * } 289 * </pre> 290 * A UriComponentsBuilder can be created: 291 * <pre class="code"> 292 * // Inline style with static import of "MvcUriComponentsBuilder.on" 293 * 294 * MvcUriComponentsBuilder.fromMethodCall( 295 * on(AddressController.class).getAddressesForCountry("US")).buildAndExpand(1); 296 * 297 * // Longer form useful for repeated invocation (and void controller methods) 298 * 299 * AddressController controller = MvcUriComponentsBuilder.on(AddressController.class); 300 * controller.addAddress(null); 301 * builder = MvcUriComponentsBuilder.fromMethodCall(controller); 302 * controller.getAddressesForCountry("US") 303 * builder = MvcUriComponentsBuilder.fromMethodCall(controller); 304 * </pre> 305 * <p><strong>Note:</strong> This method extracts values from "Forwarded" 306 * and "X-Forwarded-*" headers if found. See class-level docs. 307 * @param info either the value returned from a "mock" controller 308 * invocation or the "mock" controller itself after an invocation 309 * @return a UriComponents instance 310 * @see #on(Class) 311 * @see #controller(Class) 312 */ 313 public static UriComponentsBuilder fromMethodCall(Object info) { 314 Assert.isInstanceOf(MethodInvocationInfo.class, info, "MethodInvocationInfo required"); 315 MethodInvocationInfo invocationInfo = (MethodInvocationInfo) info; 316 Class<?> controllerType = invocationInfo.getControllerType(); 317 Method method = invocationInfo.getControllerMethod(); 318 Object[] arguments = invocationInfo.getArgumentValues(); 319 return fromMethodInternal(null, controllerType, method, arguments); 320 } 321 322 /** 323 * An alternative to {@link #fromMethodCall(Object)} that accepts a 324 * {@code UriComponentsBuilder} representing the base URL. This is useful 325 * when using MvcUriComponentsBuilder outside the context of processing a 326 * request or to apply a custom baseUrl not matching the current request. 327 * <p><strong>Note:</strong> This method extracts values from "Forwarded" 328 * and "X-Forwarded-*" headers if found. See class-level docs. 329 * @param builder the builder for the base URL; the builder will be cloned 330 * and therefore not modified and may be re-used for further calls. 331 * @param info either the value returned from a "mock" controller 332 * invocation or the "mock" controller itself after an invocation 333 * @return a UriComponents instance 334 */ 335 public static UriComponentsBuilder fromMethodCall(UriComponentsBuilder builder, Object info) { 336 Assert.isInstanceOf(MethodInvocationInfo.class, info, "MethodInvocationInfo required"); 337 MethodInvocationInfo invocationInfo = (MethodInvocationInfo) info; 338 Class<?> controllerType = invocationInfo.getControllerType(); 339 Method method = invocationInfo.getControllerMethod(); 340 Object[] arguments = invocationInfo.getArgumentValues(); 341 return fromMethodInternal(builder, controllerType, method, arguments); 342 } 343 344 /** 345 * Return a "mock" controller instance. When an {@code @RequestMapping} method 346 * on the controller is invoked, the supplied argument values are remembered 347 * and the result can then be used to create a {@code UriComponentsBuilder} 348 * via {@link #fromMethodCall(Object)}. 349 * <p>Note that this is a shorthand version of {@link #controller(Class)} intended 350 * for inline use (with a static import), for example: 351 * <pre class="code"> 352 * MvcUriComponentsBuilder.fromMethodCall(on(FooController.class).getFoo(1)).build(); 353 * </pre> 354 * <p><strong>Note:</strong> This method extracts values from "Forwarded" 355 * and "X-Forwarded-*" headers if found. See class-level docs. 356 * 357 * @param controllerType the target controller 358 */ 359 public static <T> T on(Class<T> controllerType) { 360 return controller(controllerType); 361 } 362 363 /** 364 * Return a "mock" controller instance. When an {@code @RequestMapping} method 365 * on the controller is invoked, the supplied argument values are remembered 366 * and the result can then be used to create {@code UriComponentsBuilder} via 367 * {@link #fromMethodCall(Object)}. 368 * <p>This is a longer version of {@link #on(Class)}. It is needed with controller 369 * methods returning void as well for repeated invocations. 370 * <pre class="code"> 371 * FooController fooController = controller(FooController.class); 372 * 373 * fooController.saveFoo(1, null); 374 * builder = MvcUriComponentsBuilder.fromMethodCall(fooController); 375 * 376 * fooController.saveFoo(2, null); 377 * builder = MvcUriComponentsBuilder.fromMethodCall(fooController); 378 * </pre> 379 * <p><strong>Note:</strong> This method extracts values from "Forwarded" 380 * and "X-Forwarded-*" headers if found. See class-level docs. 381 * @param controllerType the target controller 382 */ 383 public static <T> T controller(Class<T> controllerType) { 384 Assert.notNull(controllerType, "'controllerType' must not be null"); 385 return ControllerMethodInvocationInterceptor.initProxy(controllerType, null); 386 } 387 388 /** 389 * Create a URL from the name of a Spring MVC controller method's request mapping. 390 * <p>The configured 391 * {@link org.springframework.web.servlet.handler.HandlerMethodMappingNamingStrategy 392 * HandlerMethodMappingNamingStrategy} determines the names of controller 393 * method request mappings at startup. By default all mappings are assigned 394 * a name based on the capital letters of the class name, followed by "#" as 395 * separator, and then the method name. For example "PC#getPerson" 396 * for a class named PersonController with method getPerson. In case the 397 * naming convention does not produce unique results, an explicit name may 398 * be assigned through the name attribute of the {@code @RequestMapping} 399 * annotation. 400 * <p>This is aimed primarily for use in view rendering technologies and EL 401 * expressions. The Spring URL tag library registers this method as a function 402 * called "mvcUrl". 403 * <p>For example, given this controller: 404 * <pre class="code"> 405 * @RequestMapping("/people") 406 * class PersonController { 407 * 408 * @RequestMapping("/{id}") 409 * public HttpEntity<Void> getPerson(@PathVariable String id) { ... } 410 * 411 * } 412 * </pre> 413 * 414 * A JSP can prepare a URL to the controller method as follows: 415 * 416 * <pre class="code"> 417 * <%@ taglib uri="http://www.springframework.org/tags" prefix="s" %> 418 * 419 * <a href="${s:mvcUrl('PC#getPerson').arg(0,"123").build()}">Get Person</a> 420 * </pre> 421 * <p>Note that it's not necessary to specify all arguments. Only the ones 422 * required to prepare the URL, mainly {@code @RequestParam} and {@code @PathVariable}). 423 * 424 * <p><strong>Note:</strong> This method extracts values from "Forwarded" 425 * and "X-Forwarded-*" headers if found. See class-level docs. 426 * 427 * @param mappingName the mapping name 428 * @return a builder to prepare the URI String 429 * @throws IllegalArgumentException if the mapping name is not found or 430 * if there is no unique match 431 * @since 4.1 432 */ 433 public static MethodArgumentBuilder fromMappingName(String mappingName) { 434 return fromMappingName(null, mappingName); 435 } 436 437 /** 438 * An alternative to {@link #fromMappingName(String)} that accepts a 439 * {@code UriComponentsBuilder} representing the base URL. This is useful 440 * when using MvcUriComponentsBuilder outside the context of processing a 441 * request or to apply a custom baseUrl not matching the current request. 442 * <p><strong>Note:</strong> This method extracts values from "Forwarded" 443 * and "X-Forwarded-*" headers if found. See class-level docs. 444 * @param builder the builder for the base URL; the builder will be cloned 445 * and therefore not modified and may be re-used for further calls. 446 * @param name the mapping name 447 * @return a builder to prepare the URI String 448 * @throws IllegalArgumentException if the mapping name is not found or 449 * if there is no unique match 450 * @since 4.2 451 */ 452 public static MethodArgumentBuilder fromMappingName(@Nullable UriComponentsBuilder builder, String name) { 453 WebApplicationContext wac = getWebApplicationContext(); 454 Assert.notNull(wac, "No WebApplicationContext. "); 455 Map<String, RequestMappingInfoHandlerMapping> map = wac.getBeansOfType(RequestMappingInfoHandlerMapping.class); 456 List<HandlerMethod> handlerMethods = null; 457 for (RequestMappingInfoHandlerMapping mapping : map.values()) { 458 handlerMethods = mapping.getHandlerMethodsForMappingName(name); 459 if (handlerMethods != null) { 460 break; 461 } 462 } 463 if (handlerMethods == null) { 464 throw new IllegalArgumentException("Mapping not found: " + name); 465 } 466 else if (handlerMethods.size() != 1) { 467 throw new IllegalArgumentException("No unique match for mapping " + name + ": " + handlerMethods); 468 } 469 else { 470 HandlerMethod handlerMethod = handlerMethods.get(0); 471 Class<?> controllerType = handlerMethod.getBeanType(); 472 Method method = handlerMethod.getMethod(); 473 return new MethodArgumentBuilder(builder, controllerType, method); 474 } 475 } 476 477 478 // Instance methods, relative to a base UriComponentsBuilder... 479 480 /** 481 * An alternative to {@link #fromController(Class)} for use with an instance 482 * of this class created via a call to {@link #relativeTo}. 483 * <p><strong>Note:</strong> This method extracts values from "Forwarded" 484 * and "X-Forwarded-*" headers if found. See class-level docs. 485 * @since 4.2 486 */ 487 public UriComponentsBuilder withController(Class<?> controllerType) { 488 return fromController(this.baseUrl, controllerType); 489 } 490 491 /** 492 * An alternative to {@link #fromMethodName(Class, String, Object...)}} for 493 * use with an instance of this class created via {@link #relativeTo}. 494 * <p><strong>Note:</strong> This method extracts values from "Forwarded" 495 * and "X-Forwarded-*" headers if found. See class-level docs. 496 * @since 4.2 497 */ 498 public UriComponentsBuilder withMethodName(Class<?> controllerType, String methodName, Object... args) { 499 return fromMethodName(this.baseUrl, controllerType, methodName, args); 500 } 501 502 /** 503 * An alternative to {@link #fromMethodCall(Object)} for use with an instance 504 * of this class created via {@link #relativeTo}. 505 * <p><strong>Note:</strong> This method extracts values from "Forwarded" 506 * and "X-Forwarded-*" headers if found. See class-level docs. 507 * @since 4.2 508 */ 509 public UriComponentsBuilder withMethodCall(Object invocationInfo) { 510 return fromMethodCall(this.baseUrl, invocationInfo); 511 } 512 513 /** 514 * An alternative to {@link #fromMappingName(String)} for use with an instance 515 * of this class created via {@link #relativeTo}. 516 * <p><strong>Note:</strong> This method extracts values from "Forwarded" 517 * and "X-Forwarded-*" headers if found. See class-level docs. 518 * @since 4.2 519 */ 520 public MethodArgumentBuilder withMappingName(String mappingName) { 521 return fromMappingName(this.baseUrl, mappingName); 522 } 523 524 /** 525 * An alternative to {@link #fromMethod(Class, Method, Object...)} 526 * for use with an instance of this class created via {@link #relativeTo}. 527 * <p><strong>Note:</strong> This method extracts values from "Forwarded" 528 * and "X-Forwarded-*" headers if found. See class-level docs. 529 * @since 4.2 530 */ 531 public UriComponentsBuilder withMethod(Class<?> controllerType, Method method, Object... args) { 532 return fromMethod(this.baseUrl, controllerType, method, args); 533 } 534 535 536 private static UriComponentsBuilder fromMethodInternal(@Nullable UriComponentsBuilder builder, 537 Class<?> controllerType, Method method, Object... args) { 538 539 builder = getBaseUrlToUse(builder); 540 541 // Externally configured prefix via PathConfigurer.. 542 String prefix = getPathPrefix(controllerType); 543 builder.path(prefix); 544 545 String typePath = getClassMapping(controllerType); 546 String methodPath = getMethodMapping(method); 547 String path = pathMatcher.combine(typePath, methodPath); 548 if (StringUtils.hasLength(path) && !path.startsWith("/")) { 549 path = "/" + path; 550 } 551 builder.path(path); 552 553 return applyContributors(builder, method, args); 554 } 555 556 private static UriComponentsBuilder getBaseUrlToUse(@Nullable UriComponentsBuilder baseUrl) { 557 return baseUrl == null ? 558 ServletUriComponentsBuilder.fromCurrentServletMapping() : 559 baseUrl.cloneBuilder(); 560 } 561 562 private static String getPathPrefix(Class<?> controllerType) { 563 WebApplicationContext wac = getWebApplicationContext(); 564 if (wac != null) { 565 Map<String, RequestMappingHandlerMapping> map = wac.getBeansOfType(RequestMappingHandlerMapping.class); 566 for (RequestMappingHandlerMapping mapping : map.values()) { 567 if (mapping.isHandler(controllerType)) { 568 String prefix = mapping.getPathPrefix(controllerType); 569 if (prefix != null) { 570 return prefix; 571 } 572 } 573 } 574 } 575 return ""; 576 } 577 578 private static String getClassMapping(Class<?> controllerType) { 579 Assert.notNull(controllerType, "'controllerType' must not be null"); 580 RequestMapping mapping = AnnotatedElementUtils.findMergedAnnotation(controllerType, RequestMapping.class); 581 if (mapping == null) { 582 return "/"; 583 } 584 String[] paths = mapping.path(); 585 if (ObjectUtils.isEmpty(paths) || !StringUtils.hasLength(paths[0])) { 586 return "/"; 587 } 588 if (paths.length > 1 && logger.isTraceEnabled()) { 589 logger.trace("Using first of multiple paths on " + controllerType.getName()); 590 } 591 return paths[0]; 592 } 593 594 private static String getMethodMapping(Method method) { 595 Assert.notNull(method, "'method' must not be null"); 596 RequestMapping requestMapping = AnnotatedElementUtils.findMergedAnnotation(method, RequestMapping.class); 597 if (requestMapping == null) { 598 throw new IllegalArgumentException("No @RequestMapping on: " + method.toGenericString()); 599 } 600 String[] paths = requestMapping.path(); 601 if (ObjectUtils.isEmpty(paths) || !StringUtils.hasLength(paths[0])) { 602 return "/"; 603 } 604 if (paths.length > 1 && logger.isTraceEnabled()) { 605 logger.trace("Using first of multiple paths on " + method.toGenericString()); 606 } 607 return paths[0]; 608 } 609 610 private static Method getMethod(Class<?> controllerType, final String methodName, final Object... args) { 611 MethodFilter selector = method -> { 612 String name = method.getName(); 613 int argLength = method.getParameterCount(); 614 return (name.equals(methodName) && argLength == args.length); 615 }; 616 Set<Method> methods = MethodIntrospector.selectMethods(controllerType, selector); 617 if (methods.size() == 1) { 618 return methods.iterator().next(); 619 } 620 else if (methods.size() > 1) { 621 throw new IllegalArgumentException(String.format( 622 "Found two methods named '%s' accepting arguments %s in controller %s: [%s]", 623 methodName, Arrays.asList(args), controllerType.getName(), methods)); 624 } 625 else { 626 throw new IllegalArgumentException("No method named '" + methodName + "' with " + args.length + 627 " arguments found in controller " + controllerType.getName()); 628 } 629 } 630 631 private static UriComponentsBuilder applyContributors(UriComponentsBuilder builder, Method method, Object... args) { 632 CompositeUriComponentsContributor contributor = getUriComponentsContributor(); 633 634 int paramCount = method.getParameterCount(); 635 int argCount = args.length; 636 if (paramCount != argCount) { 637 throw new IllegalArgumentException("Number of method parameters " + paramCount + 638 " does not match number of argument values " + argCount); 639 } 640 641 final Map<String, Object> uriVars = new HashMap<>(); 642 for (int i = 0; i < paramCount; i++) { 643 MethodParameter param = new SynthesizingMethodParameter(method, i); 644 param.initParameterNameDiscovery(parameterNameDiscoverer); 645 contributor.contributeMethodArgument(param, args[i], builder, uriVars); 646 } 647 648 // This may not be all the URI variables, supply what we have so far.. 649 return builder.uriVariables(uriVars); 650 } 651 652 private static CompositeUriComponentsContributor getUriComponentsContributor() { 653 WebApplicationContext wac = getWebApplicationContext(); 654 if (wac != null) { 655 try { 656 return wac.getBean(MVC_URI_COMPONENTS_CONTRIBUTOR_BEAN_NAME, CompositeUriComponentsContributor.class); 657 } 658 catch (NoSuchBeanDefinitionException ex) { 659 // Ignore 660 } 661 } 662 return defaultUriComponentsContributor; 663 } 664 665 @Nullable 666 private static WebApplicationContext getWebApplicationContext() { 667 RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); 668 if (requestAttributes == null) { 669 return null; 670 } 671 HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest(); 672 String attributeName = DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE; 673 WebApplicationContext wac = (WebApplicationContext) request.getAttribute(attributeName); 674 if (wac == null) { 675 return null; 676 } 677 return wac; 678 } 679 680 681 682 /** 683 * Method invocation information. 684 */ 685 public interface MethodInvocationInfo { 686 687 /** 688 * Return the controller types. 689 */ 690 Class<?> getControllerType(); 691 692 /** 693 * Return the controller method. 694 */ 695 Method getControllerMethod(); 696 697 /** 698 * Return the argument values. 699 */ 700 Object[] getArgumentValues(); 701 } 702 703 704 private static class ControllerMethodInvocationInterceptor 705 implements org.springframework.cglib.proxy.MethodInterceptor, MethodInterceptor, MethodInvocationInfo { 706 707 private final Class<?> controllerType; 708 709 @Nullable 710 private Method controllerMethod; 711 712 @Nullable 713 private Object[] argumentValues; 714 715 ControllerMethodInvocationInterceptor(Class<?> controllerType) { 716 this.controllerType = controllerType; 717 } 718 719 @Override 720 @Nullable 721 public Object intercept(Object obj, Method method, Object[] args, @Nullable MethodProxy proxy) { 722 if (method.getName().equals("getControllerType")) { 723 return this.controllerType; 724 } 725 else if (method.getName().equals("getControllerMethod")) { 726 return this.controllerMethod; 727 } 728 else if (method.getName().equals("getArgumentValues")) { 729 return this.argumentValues; 730 } 731 else if (ReflectionUtils.isObjectMethod(method)) { 732 return ReflectionUtils.invokeMethod(method, obj, args); 733 } 734 else { 735 this.controllerMethod = method; 736 this.argumentValues = args; 737 Class<?> returnType = method.getReturnType(); 738 try { 739 return (returnType == void.class ? null : returnType.cast(initProxy(returnType, this))); 740 } 741 catch (Throwable ex) { 742 throw new IllegalStateException( 743 "Failed to create proxy for controller method return type: " + method, ex); 744 } 745 } 746 } 747 748 @Override 749 @Nullable 750 public Object invoke(org.aopalliance.intercept.MethodInvocation inv) throws Throwable { 751 return intercept(inv.getThis(), inv.getMethod(), inv.getArguments(), null); 752 } 753 754 @Override 755 public Class<?> getControllerType() { 756 return this.controllerType; 757 } 758 759 @Override 760 public Method getControllerMethod() { 761 Assert.state(this.controllerMethod != null, "Not initialized yet"); 762 return this.controllerMethod; 763 } 764 765 @Override 766 public Object[] getArgumentValues() { 767 Assert.state(this.argumentValues != null, "Not initialized yet"); 768 return this.argumentValues; 769 } 770 771 772 @SuppressWarnings("unchecked") 773 private static <T> T initProxy( 774 Class<?> controllerType, @Nullable ControllerMethodInvocationInterceptor interceptor) { 775 776 interceptor = interceptor != null ? 777 interceptor : new ControllerMethodInvocationInterceptor(controllerType); 778 779 if (controllerType == Object.class) { 780 return (T) interceptor; 781 } 782 783 else if (controllerType.isInterface()) { 784 ProxyFactory factory = new ProxyFactory(EmptyTargetSource.INSTANCE); 785 factory.addInterface(controllerType); 786 factory.addInterface(MethodInvocationInfo.class); 787 factory.addAdvice(interceptor); 788 return (T) factory.getProxy(); 789 } 790 791 else { 792 Enhancer enhancer = new Enhancer(); 793 enhancer.setSuperclass(controllerType); 794 enhancer.setInterfaces(new Class<?>[] {MethodInvocationInfo.class}); 795 enhancer.setNamingPolicy(SpringNamingPolicy.INSTANCE); 796 enhancer.setCallbackType(org.springframework.cglib.proxy.MethodInterceptor.class); 797 798 Class<?> proxyClass = enhancer.createClass(); 799 Object proxy = null; 800 801 if (objenesis.isWorthTrying()) { 802 try { 803 proxy = objenesis.newInstance(proxyClass, enhancer.getUseCache()); 804 } 805 catch (ObjenesisException ex) { 806 logger.debug("Failed to create controller proxy, falling back on default constructor", ex); 807 } 808 } 809 810 if (proxy == null) { 811 try { 812 proxy = ReflectionUtils.accessibleConstructor(proxyClass).newInstance(); 813 } 814 catch (Throwable ex) { 815 throw new IllegalStateException( 816 "Failed to create controller proxy or use default constructor", ex); 817 } 818 } 819 820 ((Factory) proxy).setCallbacks(new Callback[] {interceptor}); 821 return (T) proxy; 822 } 823 } 824 } 825 826 827 /** 828 * Builder class to create URLs for method arguments. 829 */ 830 public static class MethodArgumentBuilder { 831 832 private final Class<?> controllerType; 833 834 private final Method method; 835 836 private final Object[] argumentValues; 837 838 private final UriComponentsBuilder baseUrl; 839 840 /** 841 * Create a new {@link MethodArgumentBuilder} instance. 842 * @since 4.2 843 */ 844 public MethodArgumentBuilder(Class<?> controllerType, Method method) { 845 this(null, controllerType, method); 846 } 847 848 /** 849 * Create a new {@link MethodArgumentBuilder} instance. 850 * @since 4.2 851 */ 852 public MethodArgumentBuilder(@Nullable UriComponentsBuilder baseUrl, Class<?> controllerType, Method method) { 853 Assert.notNull(controllerType, "'controllerType' is required"); 854 Assert.notNull(method, "'method' is required"); 855 this.baseUrl = baseUrl != null ? baseUrl : UriComponentsBuilder.fromPath(getPath()); 856 this.controllerType = controllerType; 857 this.method = method; 858 this.argumentValues = new Object[method.getParameterCount()]; 859 } 860 861 private static String getPath() { 862 UriComponentsBuilder builder = ServletUriComponentsBuilder.fromCurrentServletMapping(); 863 String path = builder.build().getPath(); 864 return path != null ? path : ""; 865 } 866 867 public MethodArgumentBuilder arg(int index, Object value) { 868 this.argumentValues[index] = value; 869 return this; 870 } 871 872 /** 873 * Use this method only if you need to apply strong encoding to expanded 874 * URI variables by quoting all characters with reserved meaning. 875 * @since 5.0.8 876 */ 877 public MethodArgumentBuilder encode() { 878 this.baseUrl.encode(); 879 return this; 880 } 881 882 public String build() { 883 return fromMethodInternal(this.baseUrl, this.controllerType, this.method, this.argumentValues) 884 .build().encode().toUriString(); 885 } 886 887 public String buildAndExpand(Object... uriVars) { 888 return fromMethodInternal(this.baseUrl, this.controllerType, this.method, this.argumentValues) 889 .buildAndExpand(uriVars).encode().toString(); 890 } 891 } 892 893}