Make @ConstructorBinding implict for config prop records
Closes gh-27216
This commit is contained in:
@@ -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.
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user