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
This commit is contained in:
Wan bin yu
2026-09-11 16:56:09 +01:00
committed by Andy Wilkinson
parent 042c69a4f9
commit 57f1c947d1
4 changed files with 103 additions and 3 deletions
@@ -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.
* <p>
* 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)
@@ -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 <T> @Nullable T convertDefaultValue(BindConverter converter, String[] defaultValue, ResolvableType type,
Annotation[] annotations) {
try {
@@ -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<String> 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<String> names) {
this.name = name;
this.duration = duration;
this.counts = counts;
this.names = names;
}
}
@ConfigurationProperties("test")
static class ConstructorParameterEmptyDefaultValueProperties {
@@ -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;