diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/ValueObjectBinder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/ValueObjectBinder.java index 0b320a6262..5848f99c16 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/ValueObjectBinder.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/ValueObjectBinder.java @@ -36,6 +36,7 @@ import org.springframework.core.KotlinDetector; import org.springframework.core.MethodParameter; import org.springframework.core.ParameterNameDiscoverer; import org.springframework.core.ResolvableType; +import org.springframework.core.convert.ConversionException; import org.springframework.util.Assert; /** @@ -94,16 +95,30 @@ class ValueObjectBinder implements DataObjectBinder { Annotation[] annotations = parameter.getAnnotations(); for (Annotation annotation : annotations) { if (annotation instanceof DefaultValue) { - DefaultValue defaultValue = (DefaultValue) annotation; - if (defaultValue.value().length == 0) { + String[] defaultValue = ((DefaultValue) annotation).value(); + if (defaultValue.length == 0) { return getNewInstanceIfPossible(context, type); } - return context.getConverter().convert(defaultValue.value(), type, annotations); + return convertDefaultValue(context.getConverter(), defaultValue, type, annotations); } } return null; } + private T convertDefaultValue(BindConverter converter, String[] defaultValue, ResolvableType type, + Annotation[] annotations) { + try { + return converter.convert(defaultValue, type, annotations); + } + catch (ConversionException ex) { + // Try again in case ArrayToObjectConverter is not in play + if (defaultValue.length == 1) { + return converter.convert(defaultValue[0], type, annotations); + } + throw ex; + } + } + @SuppressWarnings("unchecked") private T getNewInstanceIfPossible(Binder.Context context, ResolvableType type) { Class resolved = (Class) type.resolve(); diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/ValueObjectBinderTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/ValueObjectBinderTests.java index 6d017a35b6..4321cf47ef 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/ValueObjectBinderTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/ValueObjectBinderTests.java @@ -16,6 +16,8 @@ package org.springframework.boot.context.properties.bind; import java.lang.reflect.Constructor; +import java.nio.file.Path; +import java.nio.file.Paths; import java.time.LocalDate; import java.util.ArrayList; import java.util.List; @@ -307,6 +309,29 @@ class ValueObjectBinderTests { .withStackTraceContaining("Parameter of type int must have a non-empty default value."); } + @Test + void bindWhenBindingToPathTypeWithValue() { // gh-21263 + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("test.name", "test"); + source.put("test.path", "specific_value"); + this.sources.add(source); + Bindable target = Bindable.of(PathBean.class); + PathBean bound = this.binder.bind("test", target).get(); + assertThat(bound.getName()).isEqualTo("test"); + assertThat(bound.getPath()).isEqualTo(Paths.get("specific_value")); + } + + @Test + void bindWhenBindingToPathTypeWithDefaultValue() { // gh-21263 + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("test.name", "test"); + this.sources.add(source); + Bindable target = Bindable.of(PathBean.class); + PathBean bound = this.binder.bindOrCreate("test", target); + assertThat(bound.getName()).isEqualTo("test"); + assertThat(bound.getPath()).isEqualTo(Paths.get("default_value")); + } + private void noConfigurationProperty(BindException ex) { assertThat(ex.getProperty()).isNull(); } @@ -684,4 +709,25 @@ class ValueObjectBinderTests { } + static class PathBean { + + private final String name; + + private final Path path; + + PathBean(String name, @DefaultValue("default_value") Path path) { + this.name = name; + this.path = path; + } + + String getName() { + return this.name; + } + + Path getPath() { + return this.path; + } + + } + }