Skip to content

Commit a5656e0

Browse files
committed
Make @ConstructorBinding implict for config prop records
Closes gh-27216
1 parent 3ff20ed commit a5656e0

File tree

3 files changed

+46
-4
lines changed

3 files changed

+46
-4
lines changed

spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/external-config.adoc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -706,6 +706,8 @@ include::{docs-java}/features/externalconfig/typesafeconfigurationproperties/con
706706

707707
In this setup, the `@ConstructorBinding` annotation is used to indicate that constructor binding should be used.
708708
This means that the binder will expect to find a constructor with the parameters that you wish to have bound.
709+
If you are using Java 16 or later, constructor binding can be used with records.
710+
In this case, unless your record has multiple constructors, there is no need to use `@ConstructorBinding`.
709711

710712
Nested members of a `@ConstructorBinding` class (such as `Security` in the example above) will also be bound via their constructor.
711713

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

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2020 the original author or authors.
2+
* Copyright 2012-2021 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -46,7 +46,7 @@ Constructor<?> getBindConstructor(Class<?> type, boolean isNestedConstructorBind
4646
return null;
4747
}
4848
Constructor<?> constructor = findConstructorBindingAnnotatedConstructor(type);
49-
if (constructor == null && (isConstructorBindingAnnotatedType(type) || isNestedConstructorBinding)) {
49+
if (constructor == null && (isConstructorBindingType(type) || isNestedConstructorBinding)) {
5050
constructor = deduceBindConstructor(type);
5151
}
5252
return constructor;
@@ -76,6 +76,15 @@ private Constructor<?> findAnnotatedConstructor(Class<?> type, Constructor<?>...
7676
return constructor;
7777
}
7878

79+
private boolean isConstructorBindingType(Class<?> type) {
80+
return isImplicitConstructorBindingType(type) || isConstructorBindingAnnotatedType(type);
81+
}
82+
83+
private boolean isImplicitConstructorBindingType(Class<?> type) {
84+
Class<?> superclass = type.getSuperclass();
85+
return (superclass != null) && "java.lang.Record".equals(superclass.getName());
86+
}
87+
7988
private boolean isConstructorBindingAnnotatedType(Class<?> type) {
8089
return MergedAnnotations.from(type, MergedAnnotations.SearchStrategy.TYPE_HIERARCHY_AND_ENCLOSING_CLASSES)
8190
.isPresent(ConstructorBinding.class);

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

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2020 the original author or authors.
2+
* Copyright 2012-2021 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,9 +16,15 @@
1616

1717
package org.springframework.boot.context.properties;
1818

19+
import java.lang.reflect.Constructor;
1920
import java.util.Map;
2021

22+
import net.bytebuddy.ByteBuddy;
23+
import net.bytebuddy.ClassFileVersion;
24+
import net.bytebuddy.description.annotation.AnnotationDescription;
2125
import org.junit.jupiter.api.Test;
26+
import org.junit.jupiter.api.condition.EnabledForJreRange;
27+
import org.junit.jupiter.api.condition.JRE;
2228
import org.junit.jupiter.api.function.ThrowingConsumer;
2329

2430
import org.springframework.boot.context.properties.ConfigurationPropertiesBean.BindMethod;
@@ -201,7 +207,7 @@ void getWhenHasValidatedBeanAndFactoryMethodBindsWithFactoryMethodAnnotation() t
201207
}
202208

203209
@Test
204-
void forValueObjectReturnsBean() {
210+
void forValueObjectWithConstructorBindingAnnotatedClassReturnsBean() {
205211
ConfigurationPropertiesBean propertiesBean = ConfigurationPropertiesBean
206212
.forValueObject(ConstructorBindingOnConstructor.class, "valueObjectBean");
207213
assertThat(propertiesBean.getName()).isEqualTo("valueObjectBean");
@@ -216,6 +222,31 @@ void forValueObjectReturnsBean() {
216222
.getBindConstructor(ConstructorBindingOnConstructor.class, false)).isNotNull();
217223
}
218224

225+
@Test
226+
@EnabledForJreRange(min = JRE.JAVA_16)
227+
void forValueObjectWithUnannotatedRecordReturnsBean() {
228+
Class<?> implicitConstructorBinding = new ByteBuddy(ClassFileVersion.JAVA_V16).makeRecord()
229+
.name("org.springframework.boot.context.properties.ImplicitConstructorBinding")
230+
.annotateType(AnnotationDescription.Builder.ofType(ConfigurationProperties.class)
231+
.define("prefix", "implicit").build())
232+
.defineRecordComponent("someString", String.class).defineRecordComponent("someInteger", Integer.class)
233+
.make().load(getClass().getClassLoader()).getLoaded();
234+
ConfigurationPropertiesBean propertiesBean = ConfigurationPropertiesBean
235+
.forValueObject(implicitConstructorBinding, "implicitBindingRecord");
236+
assertThat(propertiesBean.getName()).isEqualTo("implicitBindingRecord");
237+
assertThat(propertiesBean.getInstance()).isNull();
238+
assertThat(propertiesBean.getType()).isEqualTo(implicitConstructorBinding);
239+
assertThat(propertiesBean.getBindMethod()).isEqualTo(BindMethod.VALUE_OBJECT);
240+
assertThat(propertiesBean.getAnnotation()).isNotNull();
241+
Bindable<?> target = propertiesBean.asBindTarget();
242+
assertThat(target.getType()).isEqualTo(ResolvableType.forClass(implicitConstructorBinding));
243+
assertThat(target.getValue()).isNull();
244+
Constructor<?> bindConstructor = ConfigurationPropertiesBindConstructorProvider.INSTANCE
245+
.getBindConstructor(implicitConstructorBinding, false);
246+
assertThat(bindConstructor).isNotNull();
247+
assertThat(bindConstructor.getParameterTypes()).containsExactly(String.class, Integer.class);
248+
}
249+
219250
@Test
220251
void forValueObjectWhenJavaBeanBindTypeThrowsException() {
221252
assertThatIllegalStateException()

0 commit comments

Comments
 (0)