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;