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 04044feca295..4939bb9d5def 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -30,6 +30,7 @@ import org.apache.commons.logging.LogFactory; import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.cglib.core.SpringNamingPolicy; import org.springframework.cglib.proxy.Callback; import org.springframework.cglib.proxy.Enhancer; @@ -52,6 +53,7 @@ import org.springframework.util.ReflectionUtils; import org.springframework.util.ReflectionUtils.MethodFilter; import org.springframework.util.StringUtils; +import org.springframework.util.SystemPropertyUtils; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.context.WebApplicationContext; import org.springframework.web.context.request.RequestAttributes; @@ -582,14 +584,7 @@ private static String getClassMapping(Class controllerType) { if (mapping == null) { return ""; } - String[] paths = mapping.path(); - if (ObjectUtils.isEmpty(paths) || !StringUtils.hasLength(paths[0])) { - return ""; - } - if (paths.length > 1 && logger.isTraceEnabled()) { - logger.trace("Using first of multiple paths on " + controllerType.getName()); - } - return paths[0]; + return getPathMapping(mapping, controllerType.getName()); } private static String getMethodMapping(Method method) { @@ -598,14 +593,18 @@ private static String getMethodMapping(Method method) { if (requestMapping == null) { throw new IllegalArgumentException("No @RequestMapping on: " + method.toGenericString()); } + return getPathMapping(requestMapping, method.toGenericString()); + } + + private static String getPathMapping(RequestMapping requestMapping, String source) { String[] paths = requestMapping.path(); if (ObjectUtils.isEmpty(paths) || !StringUtils.hasLength(paths[0])) { return ""; } if (paths.length > 1 && logger.isTraceEnabled()) { - logger.trace("Using first of multiple paths on " + method.toGenericString()); + logger.trace("Using first of multiple paths on " + source); } - return paths[0]; + return resolveEmbeddedValue(paths[0]); } private static Method getMethod(Class controllerType, final String methodName, final Object... args) { @@ -663,6 +662,18 @@ private static CompositeUriComponentsContributor getUriComponentsContributor() { return defaultUriComponentsContributor; } + private static String resolveEmbeddedValue(String value) { + if (!value.contains(SystemPropertyUtils.PLACEHOLDER_PREFIX)) { + return value; + } + WebApplicationContext webApplicationContext = getWebApplicationContext(); + if (webApplicationContext != null + && webApplicationContext.getAutowireCapableBeanFactory() instanceof ConfigurableBeanFactory cbf) { + return cbf.resolveEmbeddedValue(value); + } + return value; + } + @Nullable private static WebApplicationContext getWebApplicationContext() { RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/MvcUriComponentsBuilderTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/MvcUriComponentsBuilderTests.java index f4206144ef5b..9cde7b75dffa 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/MvcUriComponentsBuilderTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/MvcUriComponentsBuilderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -25,6 +25,7 @@ import java.util.Arrays; import java.util.Date; import java.util.List; +import java.util.Map; import java.util.Optional; import jakarta.servlet.http.HttpServletRequest; @@ -34,10 +35,14 @@ import org.springframework.context.annotation.Bean; import org.springframework.core.annotation.AliasFor; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.MapPropertySource; +import org.springframework.core.env.StandardEnvironment; import org.springframework.format.annotation.DateTimeFormat; import org.springframework.format.annotation.DateTimeFormat.ISO; import org.springframework.http.HttpEntity; import org.springframework.http.MediaType; +import org.springframework.lang.Nullable; import org.springframework.stereotype.Controller; import org.springframework.util.MultiValueMap; import org.springframework.web.bind.annotation.GetMapping; @@ -146,6 +151,16 @@ public void fromControllerWithCustomBaseUrlViaInstance() { assertThat(builder.toUriString()).isEqualTo("https://example.org:9090/base"); } + @Test + public void fromControllerWithPlaceholder() { + StandardEnvironment environment = new StandardEnvironment(); + environment.getPropertySources().addFirst(new MapPropertySource("test", + Map.of("context.test.mapping", "people"))); + initWebApplicationContext(WebConfig.class, environment); + UriComponents uriComponents = fromController(ConfigurablePersonController.class).build(); + assertThat(uriComponents.toUriString()).endsWith("/people"); + } + @Test public void usesForwardedHostAsHostIfHeaderIsSet() throws Exception { this.request.setScheme("https"); @@ -293,6 +308,17 @@ public void fromMethodNameWithMetaAnnotation() { assertThat(uriComponents.toUriString()).isEqualTo("http://localhost/input"); } + @Test + public void fromMethodNameConfigurablePath() { + StandardEnvironment environment = new StandardEnvironment(); + environment.getPropertySources().addFirst(new MapPropertySource("test", + Map.of("method.test.mapping", "custom"))); + initWebApplicationContext(WebConfig.class, environment); + UriComponents uriComponents = fromMethodName(ControllerWithMethods.class, + "methodWithConfigurableMapping", "1").build(); + assertThat(uriComponents.toUriString()).isEqualTo("http://localhost/something/custom/1/foo"); + } + @Test public void fromMethodCallOnSubclass() { UriComponents uriComponents = fromMethodCall(on(ExtendedController.class).myMethod(null)).build(); @@ -522,7 +548,14 @@ public void fromMethodWithPrefix() { } private void initWebApplicationContext(Class configClass) { + initWebApplicationContext(configClass, null); + } + + private void initWebApplicationContext(Class configClass, @Nullable ConfigurableEnvironment environment) { AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext(); + if (environment != null) { + context.setEnvironment(environment); + } context.setServletContext(new MockServletContext()); context.register(configClass); context.refresh(); @@ -574,6 +607,11 @@ private class InvalidController { } + @RequestMapping("/${context.test.mapping}") + interface ConfigurablePersonController { + } + + private class UnmappedController { @RequestMapping @@ -636,6 +674,11 @@ HttpEntity methodWithOptionalParam(@RequestParam(defaultValue = "") String HttpEntity methodWithOptionalNamedParam(@RequestParam("search") Optional q) { return null; } + + @RequestMapping("/${method.test.mapping}/{id}/foo") + HttpEntity methodWithConfigurableMapping(@PathVariable String id) { + return null; + } }