diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/DefaultBindConstructorProvider.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/DefaultBindConstructorProvider.java index 85cbec9d3e..25d59c6bbf 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/DefaultBindConstructorProvider.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/DefaultBindConstructorProvider.java @@ -21,6 +21,8 @@ import java.lang.reflect.Modifier; import java.util.Arrays; import java.util.stream.Stream; +import kotlin.jvm.JvmClassMappingKt; + import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.KotlinDetector; @@ -41,7 +43,8 @@ class DefaultBindConstructorProvider implements BindConstructorProvider { public Constructor getBindConstructor(Bindable bindable, boolean isNestedConstructorBinding) { Constructors constructors = Constructors.getConstructors(bindable.getType().resolve(), isNestedConstructorBinding); - if (constructors.getBind() != null && constructors.isDeducedBindConstructor()) { + if (constructors.getBind() != null && constructors.isDeducedBindConstructor() + && !constructors.isImmutableType()) { if (bindable.getValue() != null && bindable.getValue().get() != null) { return null; } @@ -60,7 +63,7 @@ class DefaultBindConstructorProvider implements BindConstructorProvider { */ static final class Constructors { - private static final Constructors NONE = new Constructors(false, null, false); + private static final Constructors NONE = new Constructors(false, null, false, false); private final boolean hasAutowired; @@ -68,10 +71,14 @@ class DefaultBindConstructorProvider implements BindConstructorProvider { private final boolean deducedBindConstructor; - private Constructors(boolean hasAutowired, Constructor bind, boolean deducedBindConstructor) { + private final boolean immutableType; + + private Constructors(boolean hasAutowired, Constructor bind, boolean deducedBindConstructor, + boolean immutableType) { this.hasAutowired = hasAutowired; this.bind = bind; this.deducedBindConstructor = deducedBindConstructor; + this.immutableType = immutableType; } boolean hasAutowired() { @@ -86,6 +93,10 @@ class DefaultBindConstructorProvider implements BindConstructorProvider { return this.deducedBindConstructor; } + boolean isImmutableType() { + return this.immutableType; + } + static Constructors getConstructors(Class type, boolean isNestedConstructorBinding) { if (type == null) { return NONE; @@ -93,13 +104,15 @@ class DefaultBindConstructorProvider implements BindConstructorProvider { boolean hasAutowiredConstructor = isAutowiredPresent(type); Constructor[] candidates = getCandidateConstructors(type); MergedAnnotations[] candidateAnnotations = getAnnotations(candidates); + boolean kotlinType = isKotlinType(type); boolean deducedBindConstructor = false; + boolean immutableType = type.isRecord() || isKotlinDataClass(type); Constructor bind = getConstructorBindingAnnotated(type, candidates, candidateAnnotations); if (bind == null && !hasAutowiredConstructor) { bind = deduceBindConstructor(type, candidates); deducedBindConstructor = bind != null; } - if (bind == null && !hasAutowiredConstructor && isKotlinType(type)) { + if (bind == null && !hasAutowiredConstructor && kotlinType) { bind = deduceKotlinBindConstructor(type); deducedBindConstructor = bind != null; } @@ -107,7 +120,7 @@ class DefaultBindConstructorProvider implements BindConstructorProvider { Assert.state(!hasAutowiredConstructor, () -> type.getName() + " declares @ConstructorBinding and @Autowired constructor"); } - return new Constructors(hasAutowiredConstructor, bind, deducedBindConstructor); + return new Constructors(hasAutowiredConstructor, bind, deducedBindConstructor, immutableType); } private static boolean isAutowiredPresent(Class type) { @@ -185,6 +198,10 @@ class DefaultBindConstructorProvider implements BindConstructorProvider { return (result != null && result.getParameterCount() > 0) ? result : null; } + private static boolean isKotlinDataClass(Class type) { + return isKotlinType(type) && JvmClassMappingKt.getKotlinClass(type).isData(); + } + private static boolean isKotlinType(Class type) { return KotlinDetector.isKotlinPresent() && KotlinDetector.isKotlinType(type); } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesTests.java index a0c40b65ac..b071174cc7 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesTests.java @@ -1135,6 +1135,13 @@ class ConfigurationPropertiesTests { assertThat(bean.getNested().getTwo()).isEqualTo("bound-2"); } + @Test // gh-34407 + void loadWhenNestedRecordWithExistingInstance() { + load(NestedRecordInstancePropertiesConfiguration.class, "test.nested.name=spring"); + NestedRecordInstanceProperties bean = this.context.getBean(NestedRecordInstanceProperties.class); + assertThat(bean.getNested().name()).isEqualTo("spring"); + } + private AnnotationConfigApplicationContext load(Class configuration, String... inlinedProperties) { return load(new Class[] { configuration }, inlinedProperties); } @@ -2932,4 +2939,29 @@ class ConfigurationPropertiesTests { } + @Configuration(proxyBeanMethods = false) + @EnableConfigurationProperties(NestedRecordInstanceProperties.class) + static class NestedRecordInstancePropertiesConfiguration { + + } + + @ConfigurationProperties("test") + static class NestedRecordInstanceProperties { + + @NestedConfigurationProperty + private NestedRecord nested = new NestedRecord("unnamed"); + + NestedRecord getNested() { + return this.nested; + } + + void setNested(NestedRecord nestedRecord) { + this.nested = nestedRecord; + } + + } + + static record NestedRecord(String name) { + } + } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/DefaultBindConstructorProviderTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/DefaultBindConstructorProviderTests.java index 75042fd921..a782b9bbb8 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/DefaultBindConstructorProviderTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/DefaultBindConstructorProviderTests.java @@ -125,6 +125,14 @@ class DefaultBindConstructorProviderTests { assertThat(bindConstructor).isNotNull(); } + @Test + void getBindConstructorWhenHasExistingValueAndValueIsRecordReturnsConstructor() { + OneConstructorOnRecord existingValue = new OneConstructorOnRecord("name", 123); + Bindable bindable = Bindable.of(OneConstructorOnRecord.class).withExistingValue(existingValue); + Constructor bindConstructor = this.provider.getBindConstructor(bindable, false); + assertThat(bindConstructor).isNotNull(); + } + static class OnlyDefaultConstructor { } @@ -199,6 +207,10 @@ class DefaultBindConstructorProviderTests { } + static record OneConstructorOnRecord(String name, int age) { + + } + static class TwoConstructorsWithBothConstructorBinding { @ConstructorBinding