From a5656e09322589c8467c68643c39dee8b2930643 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 15 Jul 2021 14:37:29 +0100 Subject: [PATCH] Make @ConstructorBinding implict for config prop records Closes gh-27216 --- .../asciidoc/features/external-config.adoc | 2 ++ ...tionPropertiesBindConstructorProvider.java | 13 +++++-- .../ConfigurationPropertiesBeanTests.java | 35 +++++++++++++++++-- 3 files changed, 46 insertions(+), 4 deletions(-) diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/external-config.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/external-config.adoc index 86d156097f..88e4c2290a 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/external-config.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/external-config.adoc @@ -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. diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindConstructorProvider.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindConstructorProvider.java index d9e4723c66..86efdc9f08 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindConstructorProvider.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindConstructorProvider.java @@ -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); diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesBeanTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesBeanTests.java index 1530b29139..6a26e696a3 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesBeanTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesBeanTests.java @@ -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()