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 44af31308631..ce49f72e5994 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 @@ -46,6 +46,7 @@ * * @author Phillip Webb * @author Madhura Bhave + * @author Lasse Wulff */ class JavaBeanBinder implements DataObjectBinder { @@ -92,7 +93,7 @@ private boolean bind(DataObjectPropertyBinder propertyBinder, Bean bean, private boolean bind(BeanSupplier beanSupplier, DataObjectPropertyBinder propertyBinder, BeanProperty property) { - String propertyName = property.getName(); + String propertyName = determinePropertyName(property); ResolvableType type = property.getType(); Supplier value = property.getValue(beanSupplier); Annotation[] annotations = property.getAnnotations(); @@ -110,6 +111,15 @@ else if (value == null || !bound.equals(value.get())) { return true; } + private String determinePropertyName(BeanProperty property) { + return Arrays.stream((property.getAnnotations() != null) ? property.getAnnotations() : new Annotation[0]) + .filter((annotation) -> annotation.annotationType() == Name.class) + .findFirst() + .map(Name.class::cast) + .map(Name::value) + .orElse(property.getName()); + } + /** * The properties of a bean that may be bound. */ diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/Name.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/Name.java index 474fcf104089..5af7f6a10107 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/Name.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/Name.java @@ -23,15 +23,16 @@ import java.lang.annotation.Target; /** - * Annotation that can be used to specify the name when binding to an immutable property. - * This annotation may be required when binding to names that clash with reserved language + * Annotation that can be used to specify the name when binding to a property. This + * annotation may be required when binding to names that clash with reserved language * keywords. * * @author Phillip Webb + * @author Lasse Wulff * @since 2.4.0 */ @Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.PARAMETER) +@Target({ ElementType.FIELD, ElementType.PARAMETER }) @Documented public @interface Name { diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/JavaBeanBinderTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/JavaBeanBinderTests.java index 1778d6e80e11..c3b67d0c1739 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/JavaBeanBinderTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/JavaBeanBinderTests.java @@ -52,6 +52,7 @@ * @author Phillip Webb * @author Madhura Bhave * @author Andy Wilkinson + * @author Lasse Wulff */ class JavaBeanBinderTests { @@ -74,6 +75,27 @@ void bindToClassShouldCreateBoundBean() { assertThat(bean.getEnumValue()).isEqualTo(ExampleEnum.FOO_BAR); } + @Test + void bindRenamedPropertyToClassBean() { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("renamed.public", "alpha"); + this.sources.add(source); + ExampleRenamedPropertyBean bean = this.binder.bind("renamed", Bindable.of(ExampleRenamedPropertyBean.class)) + .get(); + assertThat(bean.getExampleProperty()).isEqualTo("alpha"); + } + + @Test + void bindRenamedPropertyToRecordBean() { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("renamed.class", "alpha"); + this.sources.add(source); + ExampleRenamedPropertyRecordBean bean = this.binder + .bind("renamed", Bindable.of(ExampleRenamedPropertyRecordBean.class)) + .get(); + assertThat(bean.exampleProperty()).isEqualTo("alpha"); + } + @Test void bindToClassWhenHasNoPrefixShouldCreateBoundBean() { MockConfigurationPropertySource source = new MockConfigurationPropertySource(); @@ -648,6 +670,24 @@ void setEnumValue(ExampleEnum enumValue) { } + static class ExampleRenamedPropertyBean { + + @Name("public") + private String exampleProperty; + + String getExampleProperty() { + return this.exampleProperty; + } + + void setExampleProperty(String exampleProperty) { + this.exampleProperty = exampleProperty; + } + + } + + record ExampleRenamedPropertyRecordBean(@Name("class") String exampleProperty) { + } + static class ExampleDefaultsBean { private int foo = 123; 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 80f81a522807..d7aa59252d7b 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 @@ -26,11 +26,13 @@ import org.springframework.context.annotation.Import import org.springframework.test.context.support.TestPropertySourceUtils import org.assertj.core.api.Assertions.assertThat +import org.springframework.boot.context.properties.bind.Name /** * Tests for {@link ConfigurationProperties @ConfigurationProperties}-annotated beans. * * @author Madhura Bhave + * @author Lasse Wulff */ class KotlinConfigurationPropertiesTests { @@ -59,6 +61,22 @@ class KotlinConfigurationPropertiesTests { assertThat(this.context.getBean(LateInitProperties::class.java).inner.value).isEqualTo("alpha") } + @Test + fun `renamed property can be bound`() { + this.context.register(EnableRenamedProperties::class.java) + TestPropertySourceUtils.addInlinedPropertiesToEnvironment(this.context, "renamed.fun=beta") + this.context.refresh() + assertThat(this.context.getBean(RenamedProperties::class.java).bar).isEqualTo("beta") + } + + @Test + fun `renamed property can be bound to late init attribute`() { + this.context.register(EnableRenamedLateInitProperties::class.java) + TestPropertySourceUtils.addInlinedPropertiesToEnvironment(this.context, "renamed.var=beta") + this.context.refresh() + assertThat(this.context.getBean(RenamedLateInitProperties::class.java).bar).isEqualTo("beta") + } + @Test fun `type with constructor bound lateinit property with default can be bound`() { this.context.register(EnableLateInitPropertiesWithDefault::class.java) @@ -80,6 +98,21 @@ class KotlinConfigurationPropertiesTests { @ConfigurationProperties(prefix = "foo") class BingProperties(@Suppress("UNUSED_PARAMETER") bar: String) + @ConfigurationProperties(prefix = "renamed") + class RenamedProperties(@Name("fun") val bar: String) + + @EnableConfigurationProperties(RenamedProperties::class) + class EnableRenamedProperties + + @ConfigurationProperties(prefix = "renamed") + class RenamedLateInitProperties{ + @Name("var") + lateinit var bar: String + } + + @EnableConfigurationProperties(RenamedLateInitProperties::class) + class EnableRenamedLateInitProperties + @EnableConfigurationProperties class EnableConfigProperties