Skip to content

Commit a8273a3

Browse files
committed
Resolve property placeholder in RequestMapping if necessary
This commit makes sure to resolve placeholders in request mappings using the EmbeddedValueResolver of the current WebApplicationContext. To avoid retrieving the context too often, we check for the presence of the standard placeholder prefix. Closes gh-26795
1 parent 580d9f8 commit a8273a3

File tree

2 files changed

+83
-12
lines changed

2 files changed

+83
-12
lines changed

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

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2023 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -30,6 +30,7 @@
3030
import org.apache.commons.logging.LogFactory;
3131

3232
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
33+
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
3334
import org.springframework.cglib.core.SpringNamingPolicy;
3435
import org.springframework.cglib.proxy.Callback;
3536
import org.springframework.cglib.proxy.Enhancer;
@@ -52,6 +53,7 @@
5253
import org.springframework.util.ReflectionUtils;
5354
import org.springframework.util.ReflectionUtils.MethodFilter;
5455
import org.springframework.util.StringUtils;
56+
import org.springframework.util.SystemPropertyUtils;
5557
import org.springframework.web.bind.annotation.RequestMapping;
5658
import org.springframework.web.context.WebApplicationContext;
5759
import org.springframework.web.context.request.RequestAttributes;
@@ -582,14 +584,7 @@ private static String getClassMapping(Class<?> controllerType) {
582584
if (mapping == null) {
583585
return "";
584586
}
585-
String[] paths = mapping.path();
586-
if (ObjectUtils.isEmpty(paths) || !StringUtils.hasLength(paths[0])) {
587-
return "";
588-
}
589-
if (paths.length > 1 && logger.isTraceEnabled()) {
590-
logger.trace("Using first of multiple paths on " + controllerType.getName());
591-
}
592-
return paths[0];
587+
return getPathMapping(mapping, controllerType.getName());
593588
}
594589

595590
private static String getMethodMapping(Method method) {
@@ -598,14 +593,18 @@ private static String getMethodMapping(Method method) {
598593
if (requestMapping == null) {
599594
throw new IllegalArgumentException("No @RequestMapping on: " + method.toGenericString());
600595
}
596+
return getPathMapping(requestMapping, method.toGenericString());
597+
}
598+
599+
private static String getPathMapping(RequestMapping requestMapping, String source) {
601600
String[] paths = requestMapping.path();
602601
if (ObjectUtils.isEmpty(paths) || !StringUtils.hasLength(paths[0])) {
603602
return "";
604603
}
605604
if (paths.length > 1 && logger.isTraceEnabled()) {
606-
logger.trace("Using first of multiple paths on " + method.toGenericString());
605+
logger.trace("Using first of multiple paths on " + source);
607606
}
608-
return paths[0];
607+
return resolveEmbeddedValue(paths[0]);
609608
}
610609

611610
private static Method getMethod(Class<?> controllerType, final String methodName, final Object... args) {
@@ -663,6 +662,20 @@ private static CompositeUriComponentsContributor getUriComponentsContributor() {
663662
return defaultUriComponentsContributor;
664663
}
665664

665+
private static String resolveEmbeddedValue(String value) {
666+
if (value.contains(SystemPropertyUtils.PLACEHOLDER_PREFIX)) {
667+
WebApplicationContext webApplicationContext = getWebApplicationContext();
668+
if (webApplicationContext != null
669+
&& webApplicationContext.getAutowireCapableBeanFactory() instanceof ConfigurableBeanFactory cbf) {
670+
String resolvedEmbeddedValue = cbf.resolveEmbeddedValue(value);
671+
if (resolvedEmbeddedValue != null) {
672+
return resolvedEmbeddedValue;
673+
}
674+
}
675+
}
676+
return value;
677+
}
678+
666679
@Nullable
667680
private static WebApplicationContext getWebApplicationContext() {
668681
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();

spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/MvcUriComponentsBuilderTests.java

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2023 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -25,6 +25,7 @@
2525
import java.util.Arrays;
2626
import java.util.Date;
2727
import java.util.List;
28+
import java.util.Map;
2829
import java.util.Optional;
2930

3031
import jakarta.servlet.http.HttpServletRequest;
@@ -34,10 +35,14 @@
3435

3536
import org.springframework.context.annotation.Bean;
3637
import org.springframework.core.annotation.AliasFor;
38+
import org.springframework.core.env.ConfigurableEnvironment;
39+
import org.springframework.core.env.MapPropertySource;
40+
import org.springframework.core.env.StandardEnvironment;
3741
import org.springframework.format.annotation.DateTimeFormat;
3842
import org.springframework.format.annotation.DateTimeFormat.ISO;
3943
import org.springframework.http.HttpEntity;
4044
import org.springframework.http.MediaType;
45+
import org.springframework.lang.Nullable;
4146
import org.springframework.stereotype.Controller;
4247
import org.springframework.util.MultiValueMap;
4348
import org.springframework.web.bind.annotation.GetMapping;
@@ -146,6 +151,31 @@ public void fromControllerWithCustomBaseUrlViaInstance() {
146151
assertThat(builder.toUriString()).isEqualTo("https://example.org:9090/base");
147152
}
148153

154+
@Test
155+
public void fromControllerWithPlaceholder() {
156+
StandardEnvironment environment = new StandardEnvironment();
157+
environment.getPropertySources().addFirst(new MapPropertySource("test",
158+
Map.of("context.test.mapping", "people")));
159+
initWebApplicationContext(WebConfig.class, environment);
160+
UriComponents uriComponents = fromController(ConfigurablePersonController.class).build();
161+
assertThat(uriComponents.toUriString()).endsWith("/people");
162+
}
163+
164+
@Test
165+
public void fromControllerWithPlaceholderAndMissingValue() {
166+
StandardEnvironment environment = new StandardEnvironment();
167+
assertThat(environment.containsProperty("context.test.mapping")).isFalse();
168+
initWebApplicationContext(WebConfig.class, environment);
169+
UriComponents uriComponents = fromController(ConfigurablePersonController.class).build();
170+
assertThat(uriComponents.toUriString()).endsWith("/${context.test.mapping}");
171+
}
172+
173+
@Test
174+
public void fromControllerWithPlaceholderAndNoValueResolver() {
175+
UriComponents uriComponents = fromController(ConfigurablePersonController.class).build();
176+
assertThat(uriComponents.toUriString()).endsWith("/${context.test.mapping}");
177+
}
178+
149179
@Test
150180
public void usesForwardedHostAsHostIfHeaderIsSet() throws Exception {
151181
this.request.setScheme("https");
@@ -293,6 +323,17 @@ public void fromMethodNameWithMetaAnnotation() {
293323
assertThat(uriComponents.toUriString()).isEqualTo("http://localhost/input");
294324
}
295325

326+
@Test
327+
public void fromMethodNameConfigurablePath() {
328+
StandardEnvironment environment = new StandardEnvironment();
329+
environment.getPropertySources().addFirst(new MapPropertySource("test",
330+
Map.of("method.test.mapping", "custom")));
331+
initWebApplicationContext(WebConfig.class, environment);
332+
UriComponents uriComponents = fromMethodName(ControllerWithMethods.class,
333+
"methodWithConfigurableMapping", "1").build();
334+
assertThat(uriComponents.toUriString()).isEqualTo("http://localhost/something/custom/1/foo");
335+
}
336+
296337
@Test
297338
public void fromMethodCallOnSubclass() {
298339
UriComponents uriComponents = fromMethodCall(on(ExtendedController.class).myMethod(null)).build();
@@ -522,7 +563,14 @@ public void fromMethodWithPrefix() {
522563
}
523564

524565
private void initWebApplicationContext(Class<?> configClass) {
566+
initWebApplicationContext(configClass, null);
567+
}
568+
569+
private void initWebApplicationContext(Class<?> configClass, @Nullable ConfigurableEnvironment environment) {
525570
AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
571+
if (environment != null) {
572+
context.setEnvironment(environment);
573+
}
526574
context.setServletContext(new MockServletContext());
527575
context.register(configClass);
528576
context.refresh();
@@ -574,6 +622,11 @@ private class InvalidController {
574622
}
575623

576624

625+
@RequestMapping("/${context.test.mapping}")
626+
interface ConfigurablePersonController {
627+
}
628+
629+
577630
private class UnmappedController {
578631

579632
@RequestMapping
@@ -636,6 +689,11 @@ HttpEntity<Void> methodWithOptionalParam(@RequestParam(defaultValue = "") String
636689
HttpEntity<Void> methodWithOptionalNamedParam(@RequestParam("search") Optional<String> q) {
637690
return null;
638691
}
692+
693+
@RequestMapping("/${method.test.mapping}/{id}/foo")
694+
HttpEntity<Void> methodWithConfigurableMapping(@PathVariable String id) {
695+
return null;
696+
}
639697
}
640698

641699

0 commit comments

Comments
 (0)