From 57f1c947d1785d25cbf2aa962cda1edb5f68e85f Mon Sep 17 00:00:00 2001
From: Wan bin yu <3431359639@qq.com>
Date: Fri, 11 Sep 2026 09:09:26 +0800
Subject: [PATCH] Resolve property placeholders in constructor binding default
values
Resolve each annotated default using the binder placeholder resolver
before conversion.
Signed-off-by: Wan bin yu <3431359639@qq.com>
See gh-51661
---
.../context/properties/bind/DefaultValue.java | 5 +-
.../properties/bind/ValueObjectBinder.java | 13 +++-
.../ConfigurationPropertiesTests.java | 62 +++++++++++++++++++
.../bind/ValueObjectBinderTests.java | 26 ++++++++
4 files changed, 103 insertions(+), 3 deletions(-)
diff --git a/core/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/DefaultValue.java b/core/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/DefaultValue.java
index 238406cddf8..49f0618324b 100644
--- a/core/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/DefaultValue.java
+++ b/core/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/DefaultValue.java
@@ -33,11 +33,12 @@ import java.lang.annotation.Target;
* the default value for the property will not be used even if the property value is
* empty.
*
- * NOTE: This annotation does not support property placeholder resolution and the value
- * must be constant.
+ * Property placeholders in the default value are resolved using the {@link Binder}'s
+ * {@link PlaceholdersResolver} before the value is converted to the property's type.
*
* @author Madhura Bhave
* @author Pavel Anisimov
+ * @author Wan bin yu
* @since 2.2.0
*/
@Retention(RetentionPolicy.RUNTIME)
diff --git a/core/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/ValueObjectBinder.java b/core/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/ValueObjectBinder.java
index 9bdab548996..a1e4c92694a 100644
--- a/core/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/ValueObjectBinder.java
+++ b/core/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/ValueObjectBinder.java
@@ -62,6 +62,7 @@ import org.springframework.util.Assert;
* @author Phillip Webb
* @author Scott Frederick
* @author Ondřej Světlík
+ * @author Wan bin yu
*/
class ValueObjectBinder implements DataObjectBinder {
@@ -135,12 +136,22 @@ class ValueObjectBinder implements DataObjectBinder {
if (defaultValue.length == 0) {
return getNewDefaultValueInstanceIfPossible(context, type);
}
- return convertDefaultValue(context.getConverter(), defaultValue, type, annotations);
+ return convertDefaultValue(context.getConverter(),
+ resolveDefaultValue(context.getPlaceholdersResolver(), defaultValue), type, annotations);
}
}
return context.getConverter().convert(null, type);
}
+ private String[] resolveDefaultValue(PlaceholdersResolver resolver, String[] defaultValue) {
+ String[] resolved = new String[defaultValue.length];
+ for (int i = 0; i < defaultValue.length; i++) {
+ Object value = resolver.resolvePlaceholders(defaultValue[i]);
+ resolved[i] = String.valueOf(value);
+ }
+ return resolved;
+ }
+
private @Nullable T convertDefaultValue(BindConverter converter, String[] defaultValue, ResolvableType type,
Annotation[] annotations) {
try {
diff --git a/core/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesTests.java b/core/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesTests.java
index 9dfb957b7cf..1d8b533ff63 100644
--- a/core/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesTests.java
+++ b/core/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesTests.java
@@ -132,6 +132,7 @@ import static org.mockito.Mockito.mock;
* @author Stephane Nicoll
* @author Madhura Bhave
* @author Vladislav Kisel
+ * @author Wan bin yu
*/
@ExtendWith(OutputCaptureExtension.class)
class ConfigurationPropertiesTests {
@@ -1016,6 +1017,38 @@ class ConfigurationPropertiesTests {
assertThat(bean.getOptional()).isEmpty();
}
+ @Test
+ void loadWhenDefaultValuesContainPlaceholdersShouldResolveAndConvert() {
+ load(PlaceholderDefaultsConfiguration.class, "defaults.name=resolved", "defaults.duration=3d",
+ "defaults.count=7");
+ PlaceholderDefaultsProperties bean = this.context.getBean(PlaceholderDefaultsProperties.class);
+ assertThat(bean.name).isEqualTo("resolved/suffix");
+ assertThat(bean.duration).isEqualTo(Duration.ofDays(3));
+ assertThat(bean.counts).containsExactly(7, 2);
+ assertThat(bean.names).containsExactly("resolved", "fallback");
+ }
+
+ @Test
+ void loadWhenDefaultValuesContainPlaceholdersShouldUsePlaceholderDefaults() {
+ load(PlaceholderDefaultsConfiguration.class);
+ PlaceholderDefaultsProperties bean = this.context.getBean(PlaceholderDefaultsProperties.class);
+ assertThat(bean.name).isEqualTo("default/suffix");
+ assertThat(bean.duration).isEqualTo(Duration.ofDays(1));
+ assertThat(bean.counts).containsExactly(1, 2);
+ assertThat(bean.names).containsExactly("default", "fallback");
+ }
+
+ @Test
+ void loadWhenExplicitValuesOverridePlaceholderDefaultsShouldUseExplicitValues() {
+ load(PlaceholderDefaultsConfiguration.class, "test.name=explicit", "test.duration=5d", "test.counts=8,9",
+ "test.names=one,two");
+ PlaceholderDefaultsProperties bean = this.context.getBean(PlaceholderDefaultsProperties.class);
+ assertThat(bean.name).isEqualTo("explicit");
+ assertThat(bean.duration).isEqualTo(Duration.ofDays(5));
+ assertThat(bean.counts).containsExactly(8, 9);
+ assertThat(bean.names).containsExactly("one", "two");
+ }
+
@Test
void loadWhenBindingToConstructorParametersWithDefaultDataUnitShouldBind() {
load(ConstructorParameterWithUnitConfiguration.class);
@@ -2467,6 +2500,35 @@ class ConfigurationPropertiesTests {
}
+ @Configuration(proxyBeanMethods = false)
+ @EnableConfigurationProperties(PlaceholderDefaultsProperties.class)
+ static class PlaceholderDefaultsConfiguration {
+
+ }
+
+ @ConfigurationProperties("test")
+ static class PlaceholderDefaultsProperties {
+
+ private final String name;
+
+ private final Duration duration;
+
+ private final int[] counts;
+
+ private final List names;
+
+ PlaceholderDefaultsProperties(@DefaultValue("${defaults.name:default}/suffix") String name,
+ @DefaultValue("${defaults.duration:1d}") Duration duration,
+ @DefaultValue({ "${defaults.count:1}", "2" }) int[] counts,
+ @DefaultValue({ "${defaults.name:default}", "${defaults.other:fallback}" }) List names) {
+ this.name = name;
+ this.duration = duration;
+ this.counts = counts;
+ this.names = names;
+ }
+
+ }
+
@ConfigurationProperties("test")
static class ConstructorParameterEmptyDefaultValueProperties {
diff --git a/core/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/ValueObjectBinderTests.java b/core/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/ValueObjectBinderTests.java
index 5295b778571..9fad957a9e4 100644
--- a/core/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/ValueObjectBinderTests.java
+++ b/core/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/ValueObjectBinderTests.java
@@ -21,6 +21,7 @@ import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDate;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;
@@ -52,6 +53,7 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
* @author Pavel Anisimov
* @author Yanming Zhou
* @author Ondřej Světlík
+ * @author Wan bin yu
*/
class ValueObjectBinderTests {
@@ -307,6 +309,26 @@ class ValueObjectBinderTests {
assertThat(bean.getDate()).hasToString("2019-05-10");
}
+ @Test
+ void createWithDefaultValuePlaceholderAndNoResolverShouldRetainPlaceholder() {
+ PlaceholderDefaultValue bean = this.binder.bindOrCreate("foo", PlaceholderDefaultValue.class);
+ assertThat(bean.value()).isEqualTo("${value}");
+ }
+
+ @Test
+ void createWithUnresolvedDefaultValuePlaceholderShouldRetainPlaceholder() {
+ Binder binder = new Binder(this.sources, new PropertySourcesPlaceholdersResolver(Collections.emptyList()));
+ PlaceholderDefaultValue bean = binder.bindOrCreate("foo", PlaceholderDefaultValue.class);
+ assertThat(bean.value()).isEqualTo("${value}");
+ }
+
+ @Test
+ void createWithDefaultValuePlaceholderResolvingToNonStringShouldConvert() {
+ Binder binder = new Binder(this.sources, (value) -> 42);
+ PlaceholderDefaultValue bean = binder.bindOrCreate("foo", PlaceholderDefaultValue.class);
+ assertThat(bean.value()).isEqualTo("42");
+ }
+
@Test
void bindWhenAllPropertiesBoundShouldClearConfigurationProperty() { // gh-18704
MockConfigurationPropertySource source = new MockConfigurationPropertySource();
@@ -639,6 +661,10 @@ class ValueObjectBinderTests {
}
+ record PlaceholderDefaultValue(@DefaultValue("${value}") String value) {
+
+ }
+
static class ExampleDefaultValueBean {
private final int intValue;