Skip to content

Commit 285097d

Browse files
committed
Bind nested record types even if they have an existing value
Update logic in `DefaultBindConstructorProvider` introduced in commit 84b13f0 so that record types are always bound. Fixes gh-34407
1 parent 814b77c commit 285097d

File tree

3 files changed

+66
-5
lines changed

3 files changed

+66
-5
lines changed

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/DefaultBindConstructorProvider.java

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
import java.util.Arrays;
2222
import java.util.stream.Stream;
2323

24+
import kotlin.jvm.JvmClassMappingKt;
25+
2426
import org.springframework.beans.BeanUtils;
2527
import org.springframework.beans.factory.annotation.Autowired;
2628
import org.springframework.core.KotlinDetector;
@@ -41,7 +43,8 @@ class DefaultBindConstructorProvider implements BindConstructorProvider {
4143
public Constructor<?> getBindConstructor(Bindable<?> bindable, boolean isNestedConstructorBinding) {
4244
Constructors constructors = Constructors.getConstructors(bindable.getType().resolve(),
4345
isNestedConstructorBinding);
44-
if (constructors.getBind() != null && constructors.isDeducedBindConstructor()) {
46+
if (constructors.getBind() != null && constructors.isDeducedBindConstructor()
47+
&& !constructors.isImmutableType()) {
4548
if (bindable.getValue() != null && bindable.getValue().get() != null) {
4649
return null;
4750
}
@@ -60,18 +63,22 @@ public Constructor<?> getBindConstructor(Class<?> type, boolean isNestedConstruc
6063
*/
6164
static final class Constructors {
6265

63-
private static final Constructors NONE = new Constructors(false, null, false);
66+
private static final Constructors NONE = new Constructors(false, null, false, false);
6467

6568
private final boolean hasAutowired;
6669

6770
private final Constructor<?> bind;
6871

6972
private final boolean deducedBindConstructor;
7073

71-
private Constructors(boolean hasAutowired, Constructor<?> bind, boolean deducedBindConstructor) {
74+
private final boolean immutableType;
75+
76+
private Constructors(boolean hasAutowired, Constructor<?> bind, boolean deducedBindConstructor,
77+
boolean immutableType) {
7278
this.hasAutowired = hasAutowired;
7379
this.bind = bind;
7480
this.deducedBindConstructor = deducedBindConstructor;
81+
this.immutableType = immutableType;
7582
}
7683

7784
boolean hasAutowired() {
@@ -86,28 +93,34 @@ boolean isDeducedBindConstructor() {
8693
return this.deducedBindConstructor;
8794
}
8895

96+
boolean isImmutableType() {
97+
return this.immutableType;
98+
}
99+
89100
static Constructors getConstructors(Class<?> type, boolean isNestedConstructorBinding) {
90101
if (type == null) {
91102
return NONE;
92103
}
93104
boolean hasAutowiredConstructor = isAutowiredPresent(type);
94105
Constructor<?>[] candidates = getCandidateConstructors(type);
95106
MergedAnnotations[] candidateAnnotations = getAnnotations(candidates);
107+
boolean kotlinType = isKotlinType(type);
96108
boolean deducedBindConstructor = false;
109+
boolean immutableType = type.isRecord() || isKotlinDataClass(type);
97110
Constructor<?> bind = getConstructorBindingAnnotated(type, candidates, candidateAnnotations);
98111
if (bind == null && !hasAutowiredConstructor) {
99112
bind = deduceBindConstructor(type, candidates);
100113
deducedBindConstructor = bind != null;
101114
}
102-
if (bind == null && !hasAutowiredConstructor && isKotlinType(type)) {
115+
if (bind == null && !hasAutowiredConstructor && kotlinType) {
103116
bind = deduceKotlinBindConstructor(type);
104117
deducedBindConstructor = bind != null;
105118
}
106119
if (bind != null || isNestedConstructorBinding) {
107120
Assert.state(!hasAutowiredConstructor,
108121
() -> type.getName() + " declares @ConstructorBinding and @Autowired constructor");
109122
}
110-
return new Constructors(hasAutowiredConstructor, bind, deducedBindConstructor);
123+
return new Constructors(hasAutowiredConstructor, bind, deducedBindConstructor, immutableType);
111124
}
112125

113126
private static boolean isAutowiredPresent(Class<?> type) {
@@ -185,6 +198,10 @@ private static Constructor<?> deduceBindConstructor(Class<?> type, Constructor<?
185198
return (result != null && result.getParameterCount() > 0) ? result : null;
186199
}
187200

201+
private static boolean isKotlinDataClass(Class<?> type) {
202+
return isKotlinType(type) && JvmClassMappingKt.getKotlinClass(type).isData();
203+
}
204+
188205
private static boolean isKotlinType(Class<?> type) {
189206
return KotlinDetector.isKotlinPresent() && KotlinDetector.isKotlinType(type);
190207
}

spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesTests.java

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1135,6 +1135,13 @@ void loadWhenConstructorUsedInNestedPropertyAndNotAsConstructorBinding() {
11351135
assertThat(bean.getNested().getTwo()).isEqualTo("bound-2");
11361136
}
11371137

1138+
@Test // gh-34407
1139+
void loadWhenNestedRecordWithExistingInstance() {
1140+
load(NestedRecordInstancePropertiesConfiguration.class, "test.nested.name=spring");
1141+
NestedRecordInstanceProperties bean = this.context.getBean(NestedRecordInstanceProperties.class);
1142+
assertThat(bean.getNested().name()).isEqualTo("spring");
1143+
}
1144+
11381145
private AnnotationConfigApplicationContext load(Class<?> configuration, String... inlinedProperties) {
11391146
return load(new Class<?>[] { configuration }, inlinedProperties);
11401147
}
@@ -2932,4 +2939,29 @@ void setTwo(String two) {
29322939

29332940
}
29342941

2942+
@Configuration(proxyBeanMethods = false)
2943+
@EnableConfigurationProperties(NestedRecordInstanceProperties.class)
2944+
static class NestedRecordInstancePropertiesConfiguration {
2945+
2946+
}
2947+
2948+
@ConfigurationProperties("test")
2949+
static class NestedRecordInstanceProperties {
2950+
2951+
@NestedConfigurationProperty
2952+
private NestedRecord nested = new NestedRecord("unnamed");
2953+
2954+
NestedRecord getNested() {
2955+
return this.nested;
2956+
}
2957+
2958+
void setNested(NestedRecord nestedRecord) {
2959+
this.nested = nestedRecord;
2960+
}
2961+
2962+
}
2963+
2964+
static record NestedRecord(String name) {
2965+
}
2966+
29352967
}

spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/DefaultBindConstructorProviderTests.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,14 @@ void getBindConstructorWhenHasExistingValueAndOneConstructorWithConstructorBindi
125125
assertThat(bindConstructor).isNotNull();
126126
}
127127

128+
@Test
129+
void getBindConstructorWhenHasExistingValueAndValueIsRecordReturnsConstructor() {
130+
OneConstructorOnRecord existingValue = new OneConstructorOnRecord("name", 123);
131+
Bindable<?> bindable = Bindable.of(OneConstructorOnRecord.class).withExistingValue(existingValue);
132+
Constructor<?> bindConstructor = this.provider.getBindConstructor(bindable, false);
133+
assertThat(bindConstructor).isNotNull();
134+
}
135+
128136
static class OnlyDefaultConstructor {
129137

130138
}
@@ -199,6 +207,10 @@ static class OneConstructorWithoutAnnotations {
199207

200208
}
201209

210+
static record OneConstructorOnRecord(String name, int age) {
211+
212+
}
213+
202214
static class TwoConstructorsWithBothConstructorBinding {
203215

204216
@ConstructorBinding

0 commit comments

Comments
 (0)