diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/JavaBeanBinder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/JavaBeanBinder.java index 6ef232fa84..4134cc58e4 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/JavaBeanBinder.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/JavaBeanBinder.java @@ -19,6 +19,7 @@ package org.springframework.boot.context.properties.bind; import java.beans.Introspector; import java.lang.annotation.Annotation; import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.Arrays; @@ -382,11 +383,22 @@ class JavaBeanBinder implements DataObjectBinder { return this.getter.invoke(instance.get()); } catch (Exception ex) { + if (isUninitializedKotlinProperty(ex)) { + return null; + } throw new IllegalStateException("Unable to get value for property " + this.name, ex); } }; } + private boolean isUninitializedKotlinProperty(Exception ex) { + if (ex instanceof InvocationTargetException ite) { + return "kotlin.UninitializedPropertyAccessException" + .equals(ite.getTargetException().getClass().getName()); + } + return false; + } + boolean isSettable() { return this.setter != null; } diff --git a/spring-boot-project/spring-boot/src/test/kotlin/org/springframework/boot/context/properties/KotlinConfigurationPropertiesTests.kt b/spring-boot-project/spring-boot/src/test/kotlin/org/springframework/boot/context/properties/KotlinConfigurationPropertiesTests.kt index ec2c5ce58c..6420fc8dbb 100644 --- a/spring-boot-project/spring-boot/src/test/kotlin/org/springframework/boot/context/properties/KotlinConfigurationPropertiesTests.kt +++ b/spring-boot-project/spring-boot/src/test/kotlin/org/springframework/boot/context/properties/KotlinConfigurationPropertiesTests.kt @@ -1,10 +1,30 @@ +/* + * Copyright 2012-2023 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.context.properties +import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Test import org.springframework.beans.factory.support.BeanDefinitionRegistry import org.springframework.beans.factory.support.RootBeanDefinition import org.springframework.context.annotation.AnnotationConfigApplicationContext import org.springframework.context.annotation.Configuration +import org.springframework.test.context.support.TestPropertySourceUtils + +import org.assertj.core.api.Assertions.assertThat /** * Tests for {@link ConfigurationProperties @ConfigurationProperties}-annotated beans. @@ -15,24 +35,74 @@ class KotlinConfigurationPropertiesTests { private var context = AnnotationConfigApplicationContext() + @AfterEach + fun cleanUp() { + this.context.close(); + } + @Test //gh-18652 fun `type with constructor binding and existing singleton should not fail`() { val beanFactory = this.context.beanFactory (beanFactory as BeanDefinitionRegistry).registerBeanDefinition("foo", RootBeanDefinition(BingProperties::class.java)) beanFactory.registerSingleton("foo", BingProperties("")) - this.context.register(TestConfig::class.java) + this.context.register(EnableConfigProperties::class.java) this.context.refresh(); } + @Test + fun `type with constructor bound lateinit property can be bound`() { + this.context.register(EnableLateInitProperties::class.java) + TestPropertySourceUtils.addInlinedPropertiesToEnvironment(this.context, "lateinit.inner.value=alpha"); + this.context.refresh(); + assertThat(this.context.getBean(LateInitProperties::class.java).inner.value).isEqualTo("alpha") + } + + @Test + fun `type with constructor bound lateinit property with default can be bound`() { + this.context.register(EnableLateInitPropertiesWithDefault::class.java) + TestPropertySourceUtils.addInlinedPropertiesToEnvironment(this.context, "lateinit-with-default.inner.bravo=two"); + this.context.refresh(); + val properties = this.context.getBean(LateInitPropertiesWithDefault::class.java) + assertThat(properties.inner.alpha).isEqualTo("apple") + assertThat(properties.inner.bravo).isEqualTo("two") + } + @ConfigurationProperties(prefix = "foo") class BingProperties(@Suppress("UNUSED_PARAMETER") bar: String) { } - @Configuration(proxyBeanMethods = false) @EnableConfigurationProperties - internal open class TestConfig { + class EnableConfigProperties { + + } + + @ConfigurationProperties("lateinit") + class LateInitProperties { + + lateinit var inner: Inner + + } + + data class Inner(val value: String) + + @EnableConfigurationProperties(LateInitPropertiesWithDefault::class) + class EnableLateInitPropertiesWithDefault { + + } + + @ConfigurationProperties("lateinit-with-default") + class LateInitPropertiesWithDefault { + + lateinit var inner: InnerWithDefault + + } + + data class InnerWithDefault(val alpha: String = "apple", val bravo: String = "banana") + + @EnableConfigurationProperties(LateInitProperties::class) + class EnableLateInitProperties { }