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
This commit is contained in:
Stéphane Nicoll 2024-01-05 16:04:14 +01:00
parent 580d9f81e2
commit a8273a3009
2 changed files with 83 additions and 12 deletions

View File

@ -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.Log;
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.PathMatcher;
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 @@ public class MvcUriComponentsBuilder {
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 @@ public class MvcUriComponentsBuilder {
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,20 @@ public class MvcUriComponentsBuilder {
return defaultUriComponentsContributor;
}
private static String resolveEmbeddedValue(String value) {
if (value.contains(SystemPropertyUtils.PLACEHOLDER_PREFIX)) {
WebApplicationContext webApplicationContext = getWebApplicationContext();
if (webApplicationContext != null
&& webApplicationContext.getAutowireCapableBeanFactory() instanceof ConfigurableBeanFactory cbf) {
String resolvedEmbeddedValue = cbf.resolveEmbeddedValue(value);
if (resolvedEmbeddedValue != null) {
return resolvedEmbeddedValue;
}
}
}
return value;
}
@Nullable
private static WebApplicationContext getWebApplicationContext() {
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();

View File

@ -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.sql.Savepoint;
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.junit.jupiter.api.Test;
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,31 @@ public class MvcUriComponentsBuilderTests {
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 fromControllerWithPlaceholderAndMissingValue() {
StandardEnvironment environment = new StandardEnvironment();
assertThat(environment.containsProperty("context.test.mapping")).isFalse();
initWebApplicationContext(WebConfig.class, environment);
UriComponents uriComponents = fromController(ConfigurablePersonController.class).build();
assertThat(uriComponents.toUriString()).endsWith("/${context.test.mapping}");
}
@Test
public void fromControllerWithPlaceholderAndNoValueResolver() {
UriComponents uriComponents = fromController(ConfigurablePersonController.class).build();
assertThat(uriComponents.toUriString()).endsWith("/${context.test.mapping}");
}
@Test
public void usesForwardedHostAsHostIfHeaderIsSet() throws Exception {
this.request.setScheme("https");
@ -293,6 +323,17 @@ public class MvcUriComponentsBuilderTests {
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 +563,14 @@ public class MvcUriComponentsBuilderTests {
}
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 +622,11 @@ public class MvcUriComponentsBuilderTests {
}
@RequestMapping("/${context.test.mapping}")
interface ConfigurablePersonController {
}
private class UnmappedController {
@RequestMapping
@ -636,6 +689,11 @@ public class MvcUriComponentsBuilderTests {
HttpEntity<Void> methodWithOptionalNamedParam(@RequestParam("search") Optional<String> q) {
return null;
}
@RequestMapping("/${method.test.mapping}/{id}/foo")
HttpEntity<Void> methodWithConfigurableMapping(@PathVariable String id) {
return null;
}
}