From 27c4d1ff2477b91f7df1292453f799c7f6662e31 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Wed, 14 May 2025 18:12:14 -0700 Subject: [PATCH] Document the way that primary Kotlin constructors are used when binding Improve constructor binding documentation for Kotlin and add a test to prove a no-param primary constructor disables constructor binding. Closes gh-44849 --- .../pages/features/external-config.adoc | 9 +++- .../primaryconstructor/MyBean.java | 21 ++++++++ .../primaryconstructor/MyProperties.java | 49 +++++++++++++++++++ .../primaryconstructor/MyProperties.kt | 34 +++++++++++++ ...tlinDefaultBindConstructorProviderTests.kt | 24 +++++++++ 5 files changed, 136 insertions(+), 1 deletion(-) create mode 100644 spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/constructorbinding/primaryconstructor/MyBean.java create mode 100644 spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/constructorbinding/primaryconstructor/MyProperties.java create mode 100644 spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/constructorbinding/primaryconstructor/MyProperties.kt diff --git a/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/features/external-config.adoc b/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/features/external-config.adoc index 671c62da78d..c2d30dcfe1f 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/features/external-config.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/features/external-config.adoc @@ -791,7 +791,14 @@ include-code::MyProperties[] In this setup, the presence of a single parameterized constructor implies that constructor binding should be used. This means that the binder will find a constructor with the parameters that you wish to have bound. If your class has multiple constructors, the javadoc:org.springframework.boot.context.properties.bind.ConstructorBinding[format=annotation] annotation can be used to specify which constructor to use for constructor binding. -To opt out of constructor binding for a class with a single parameterized constructor, the constructor must be annotated with javadoc:org.springframework.beans.factory.annotation.Autowired[format=annotation] or made `private`. + +To opt-out of constructor binding for a class, the parameterized constructor must be annotated with javadoc:org.springframework.beans.factory.annotation.Autowired[format=annotation] or made `private`. +Kotlin developers can use an empty primary constructor to opt-out of constructor binding. + +For example: + +include-code::primaryconstructor/MyProperties[] + Constructor binding can be used with records. Unless your record has multiple constructors, there is no need to use javadoc:org.springframework.boot.context.properties.bind.ConstructorBinding[format=annotation]. diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/constructorbinding/primaryconstructor/MyBean.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/constructorbinding/primaryconstructor/MyBean.java new file mode 100644 index 00000000000..ab4f9aa3fa4 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/constructorbinding/primaryconstructor/MyBean.java @@ -0,0 +1,21 @@ +/* + * Copyright 2012-2025 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.externalconfig.typesafeconfigurationproperties.constructorbinding.primaryconstructor; + +class MyBean { + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/constructorbinding/primaryconstructor/MyProperties.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/constructorbinding/primaryconstructor/MyProperties.java new file mode 100644 index 00000000000..566f2e494e2 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/constructorbinding/primaryconstructor/MyProperties.java @@ -0,0 +1,49 @@ +/* + * Copyright 2012-2025 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.externalconfig.typesafeconfigurationproperties.constructorbinding.primaryconstructor; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties("my") +public class MyProperties { + + // @fold:on // fields... + final MyBean myBean; + + private String name; + + // @fold:off + + @Autowired + public MyProperties(MyBean myBean) { + this.myBean = myBean; + } + + // @fold:on // getters / setters... + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + // @fold:off + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/constructorbinding/primaryconstructor/MyProperties.kt b/spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/constructorbinding/primaryconstructor/MyProperties.kt new file mode 100644 index 00000000000..938f621a92d --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/constructorbinding/primaryconstructor/MyProperties.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2012-2025 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.externalconfig.typesafeconfigurationproperties.constructorbinding.primaryconstructor + +import org.springframework.boot.context.properties.ConfigurationProperties +import org.springframework.boot.context.properties.bind.DefaultValue +import java.net.InetAddress + +@ConfigurationProperties("my") +class MyProperties() { + + constructor(name: String) : this() { + this.name = name + } + + // @fold:on // vars... + var name: String? = null + // @fold:off + +} diff --git a/spring-boot-project/spring-boot/src/test/kotlin/org/springframework/boot/context/properties/bind/KotlinDefaultBindConstructorProviderTests.kt b/spring-boot-project/spring-boot/src/test/kotlin/org/springframework/boot/context/properties/bind/KotlinDefaultBindConstructorProviderTests.kt index 1abb7526714..be13746f7f9 100644 --- a/spring-boot-project/spring-boot/src/test/kotlin/org/springframework/boot/context/properties/bind/KotlinDefaultBindConstructorProviderTests.kt +++ b/spring-boot-project/spring-boot/src/test/kotlin/org/springframework/boot/context/properties/bind/KotlinDefaultBindConstructorProviderTests.kt @@ -61,6 +61,18 @@ class KotlinDefaultBindConstructorProviderTests { assertThat(bindConstructor).isNull() } + @Test + fun `type with no param primary constructor and secondary params constructor should not use constructor binding`() { + val bindConstructor = this.constructorProvider.getBindConstructor(NoParamPrimaryWithParamsSecondaryProperties::class.java, false) + assertThat(bindConstructor).isNull() + } + + @Test + fun `type with params primary constructor and no param secondary constructor should use constructor binding`() { + val bindConstructor = this.constructorProvider.getBindConstructor(ParamsPrimaryWithNoParamSecondaryProperties::class.java, false) + assertThat(bindConstructor).isNotNull() + } + @Test fun `type with autowired secondary constructor should not use constructor binding`() { val bindConstructor = this.constructorProvider.getBindConstructor(AutowiredSecondaryProperties::class.java, false) @@ -127,6 +139,18 @@ class KotlinDefaultBindConstructorProviderTests { constructor(@Suppress("UNUSED_PARAMETER") foo: String) : this(foo, 21) } + class NoParamPrimaryWithParamsSecondaryProperties() { + + constructor(@Suppress("UNUSED_PARAMETER") name: String) : this() + + var name: String? = null + } + + class ParamsPrimaryWithNoParamSecondaryProperties(var name: String?) { + + constructor() : this(null) + } + class AutowiredSecondaryProperties { @Autowired