Make @ConstructorBinding implict for config prop records

Closes gh-27216
This commit is contained in:
Andy Wilkinson 2021-07-15 14:37:29 +01:00
parent 3ff20ed4d9
commit a5656e0932
3 changed files with 46 additions and 4 deletions

View File

@ -706,6 +706,8 @@ include::{docs-java}/features/externalconfig/typesafeconfigurationproperties/con
In this setup, the `@ConstructorBinding` annotation is used to indicate that constructor binding should be used.
This means that the binder will expect to find a constructor with the parameters that you wish to have bound.
If you are using Java 16 or later, constructor binding can be used with records.
In this case, unless your record has multiple constructors, there is no need to use `@ConstructorBinding`.
Nested members of a `@ConstructorBinding` class (such as `Security` in the example above) will also be bound via their constructor.

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2020 the original author or authors.
* Copyright 2012-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -46,7 +46,7 @@ class ConfigurationPropertiesBindConstructorProvider implements BindConstructorP
return null;
}
Constructor<?> constructor = findConstructorBindingAnnotatedConstructor(type);
if (constructor == null && (isConstructorBindingAnnotatedType(type) || isNestedConstructorBinding)) {
if (constructor == null && (isConstructorBindingType(type) || isNestedConstructorBinding)) {
constructor = deduceBindConstructor(type);
}
return constructor;
@ -76,6 +76,15 @@ class ConfigurationPropertiesBindConstructorProvider implements BindConstructorP
return constructor;
}
private boolean isConstructorBindingType(Class<?> type) {
return isImplicitConstructorBindingType(type) || isConstructorBindingAnnotatedType(type);
}
private boolean isImplicitConstructorBindingType(Class<?> type) {
Class<?> superclass = type.getSuperclass();
return (superclass != null) && "java.lang.Record".equals(superclass.getName());
}
private boolean isConstructorBindingAnnotatedType(Class<?> type) {
return MergedAnnotations.from(type, MergedAnnotations.SearchStrategy.TYPE_HIERARCHY_AND_ENCLOSING_CLASSES)
.isPresent(ConstructorBinding.class);

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2020 the original author or authors.
* Copyright 2012-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -16,9 +16,15 @@
package org.springframework.boot.context.properties;
import java.lang.reflect.Constructor;
import java.util.Map;
import net.bytebuddy.ByteBuddy;
import net.bytebuddy.ClassFileVersion;
import net.bytebuddy.description.annotation.AnnotationDescription;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledForJreRange;
import org.junit.jupiter.api.condition.JRE;
import org.junit.jupiter.api.function.ThrowingConsumer;
import org.springframework.boot.context.properties.ConfigurationPropertiesBean.BindMethod;
@ -201,7 +207,7 @@ class ConfigurationPropertiesBeanTests {
}
@Test
void forValueObjectReturnsBean() {
void forValueObjectWithConstructorBindingAnnotatedClassReturnsBean() {
ConfigurationPropertiesBean propertiesBean = ConfigurationPropertiesBean
.forValueObject(ConstructorBindingOnConstructor.class, "valueObjectBean");
assertThat(propertiesBean.getName()).isEqualTo("valueObjectBean");
@ -216,6 +222,31 @@ class ConfigurationPropertiesBeanTests {
.getBindConstructor(ConstructorBindingOnConstructor.class, false)).isNotNull();
}
@Test
@EnabledForJreRange(min = JRE.JAVA_16)
void forValueObjectWithUnannotatedRecordReturnsBean() {
Class<?> implicitConstructorBinding = new ByteBuddy(ClassFileVersion.JAVA_V16).makeRecord()
.name("org.springframework.boot.context.properties.ImplicitConstructorBinding")
.annotateType(AnnotationDescription.Builder.ofType(ConfigurationProperties.class)
.define("prefix", "implicit").build())
.defineRecordComponent("someString", String.class).defineRecordComponent("someInteger", Integer.class)
.make().load(getClass().getClassLoader()).getLoaded();
ConfigurationPropertiesBean propertiesBean = ConfigurationPropertiesBean
.forValueObject(implicitConstructorBinding, "implicitBindingRecord");
assertThat(propertiesBean.getName()).isEqualTo("implicitBindingRecord");
assertThat(propertiesBean.getInstance()).isNull();
assertThat(propertiesBean.getType()).isEqualTo(implicitConstructorBinding);
assertThat(propertiesBean.getBindMethod()).isEqualTo(BindMethod.VALUE_OBJECT);
assertThat(propertiesBean.getAnnotation()).isNotNull();
Bindable<?> target = propertiesBean.asBindTarget();
assertThat(target.getType()).isEqualTo(ResolvableType.forClass(implicitConstructorBinding));
assertThat(target.getValue()).isNull();
Constructor<?> bindConstructor = ConfigurationPropertiesBindConstructorProvider.INSTANCE
.getBindConstructor(implicitConstructorBinding, false);
assertThat(bindConstructor).isNotNull();
assertThat(bindConstructor.getParameterTypes()).containsExactly(String.class, Integer.class);
}
@Test
void forValueObjectWhenJavaBeanBindTypeThrowsException() {
assertThatIllegalStateException()