Fix binding to constructor bound lateinit properties

Closes gh-35603
pull/35611/head
Andy Wilkinson 2 years ago
parent 5ad0d49ec1
commit a58e98af05

@ -19,6 +19,7 @@ package org.springframework.boot.context.properties.bind;
import java.beans.Introspector; import java.beans.Introspector;
import java.lang.annotation.Annotation; import java.lang.annotation.Annotation;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.lang.reflect.Modifier; import java.lang.reflect.Modifier;
import java.util.Arrays; import java.util.Arrays;
@ -382,11 +383,22 @@ class JavaBeanBinder implements DataObjectBinder {
return this.getter.invoke(instance.get()); return this.getter.invoke(instance.get());
} }
catch (Exception ex) { catch (Exception ex) {
if (isUninitializedKotlinProperty(ex)) {
return null;
}
throw new IllegalStateException("Unable to get value for property " + this.name, ex); 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() { boolean isSettable() {
return this.setter != null; return this.setter != null;
} }

@ -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 package org.springframework.boot.context.properties
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.springframework.beans.factory.support.BeanDefinitionRegistry import org.springframework.beans.factory.support.BeanDefinitionRegistry
import org.springframework.beans.factory.support.RootBeanDefinition import org.springframework.beans.factory.support.RootBeanDefinition
import org.springframework.context.annotation.AnnotationConfigApplicationContext import org.springframework.context.annotation.AnnotationConfigApplicationContext
import org.springframework.context.annotation.Configuration 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. * Tests for {@link ConfigurationProperties @ConfigurationProperties}-annotated beans.
@ -15,24 +35,74 @@ class KotlinConfigurationPropertiesTests {
private var context = AnnotationConfigApplicationContext() private var context = AnnotationConfigApplicationContext()
@AfterEach
fun cleanUp() {
this.context.close();
}
@Test //gh-18652 @Test //gh-18652
fun `type with constructor binding and existing singleton should not fail`() { fun `type with constructor binding and existing singleton should not fail`() {
val beanFactory = this.context.beanFactory val beanFactory = this.context.beanFactory
(beanFactory as BeanDefinitionRegistry).registerBeanDefinition("foo", (beanFactory as BeanDefinitionRegistry).registerBeanDefinition("foo",
RootBeanDefinition(BingProperties::class.java)) RootBeanDefinition(BingProperties::class.java))
beanFactory.registerSingleton("foo", BingProperties("")) beanFactory.registerSingleton("foo", BingProperties(""))
this.context.register(TestConfig::class.java) this.context.register(EnableConfigProperties::class.java)
this.context.refresh(); 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") @ConfigurationProperties(prefix = "foo")
class BingProperties(@Suppress("UNUSED_PARAMETER") bar: String) { class BingProperties(@Suppress("UNUSED_PARAMETER") bar: String) {
} }
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties @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 {
} }

Loading…
Cancel
Save