From 6c8987e0d8b4da857021bcfb56f7924e3f65baf3 Mon Sep 17 00:00:00 2001 From: Jared Bates Date: Sat, 16 Dec 2023 17:56:41 -0600 Subject: [PATCH 1/2] Allow NestedConfigurationProperty on getters This commit adds support adding `@NestedConfigurationProperty` on a getter. See gh-38844 --- .../JavaBeanPropertyDescriptor.java | 3 +- ...ationMetadataAnnotationProcessorTests.java | 4 ++ .../NestedConfigurationProperty.java | 2 +- .../specific/DeprecatedSimplePojo.java | 37 +++++++++++++++++++ .../specific/InnerClassProperties.java | 7 ++++ .../NestedConfigurationProperty.java | 2 +- 6 files changed, 52 insertions(+), 3 deletions(-) create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/specific/DeprecatedSimplePojo.java diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/JavaBeanPropertyDescriptor.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/JavaBeanPropertyDescriptor.java index 5c98c3fbdd..88023f5a49 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/JavaBeanPropertyDescriptor.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/JavaBeanPropertyDescriptor.java @@ -53,7 +53,8 @@ class JavaBeanPropertyDescriptor extends PropertyDescriptor { @Override protected boolean isMarkedAsNested(MetadataGenerationEnvironment environment) { - return environment.getNestedConfigurationPropertyAnnotation(this.field) != null; + return environment.getNestedConfigurationPropertyAnnotation(this.field) != null + || environment.getNestedConfigurationPropertyAnnotation(getGetter()) != null; } @Override diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessorTests.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessorTests.java index 4b8350814d..035b2b9e94 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessorTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessorTests.java @@ -45,6 +45,7 @@ import org.springframework.boot.configurationsample.specific.AnnotatedGetter; import org.springframework.boot.configurationsample.specific.BoxingPojo; import org.springframework.boot.configurationsample.specific.BuilderPojo; import org.springframework.boot.configurationsample.specific.DeprecatedLessPreciseTypePojo; +import org.springframework.boot.configurationsample.specific.DeprecatedSimplePojo; import org.springframework.boot.configurationsample.specific.DeprecatedUnrelatedMethodPojo; import org.springframework.boot.configurationsample.specific.DoubleRegistrationProperties; import org.springframework.boot.configurationsample.specific.EmptyDefaultValueProperties; @@ -336,6 +337,9 @@ class ConfigurationMetadataAnnotationProcessorTests extends AbstractMetadataGene assertThat(metadata).has(Metadata.withProperty("config.third.value")); assertThat(metadata).has(Metadata.withProperty("config.fourth")); assertThat(metadata).isNotEqualTo(Metadata.withGroup("config.fourth")); + assertThat(metadata) + .has(Metadata.withGroup("config.fifth").ofType(DeprecatedSimplePojo.class).fromSource(InnerClassProperties.class)); + assertThat(metadata).has(Metadata.withProperty("config.fifth.value").withDeprecation()); } @Test diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/NestedConfigurationProperty.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/NestedConfigurationProperty.java index 41f11f6edf..b50f889001 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/NestedConfigurationProperty.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/NestedConfigurationProperty.java @@ -30,7 +30,7 @@ import java.lang.annotation.Target; * @author Phillip Webb * @since 1.2.0 */ -@Target({ ElementType.FIELD, ElementType.RECORD_COMPONENT }) +@Target({ ElementType.FIELD, ElementType.RECORD_COMPONENT, ElementType.METHOD }) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface NestedConfigurationProperty { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/specific/DeprecatedSimplePojo.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/specific/DeprecatedSimplePojo.java new file mode 100644 index 0000000000..8bca93258c --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/specific/DeprecatedSimplePojo.java @@ -0,0 +1,37 @@ +/* + * Copyright 2012-2019 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.configurationsample.specific; + +/** + * POJO for use with samples needing a deprecated value. + * + * @author Jared Bates + */ +public class DeprecatedSimplePojo { + + private int value; + + @Deprecated + public int getValue() { + return this.value; + } + + public void setValue(int value) { + this.value = value; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/specific/InnerClassProperties.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/specific/InnerClassProperties.java index 1534568faa..3e7f7d079a 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/specific/InnerClassProperties.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/specific/InnerClassProperties.java @@ -36,6 +36,8 @@ public class InnerClassProperties { private Fourth fourth; + private final DeprecatedSimplePojo fifth = new DeprecatedSimplePojo(); + public Foo getFirst() { return this.first; } @@ -60,6 +62,11 @@ public class InnerClassProperties { this.fourth = fourth; } + @NestedConfigurationProperty + public DeprecatedSimplePojo getFifth() { + return this.fifth; + } + public static class Foo { private String name; diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/NestedConfigurationProperty.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/NestedConfigurationProperty.java index 83555d7670..cced7a62a6 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/NestedConfigurationProperty.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/NestedConfigurationProperty.java @@ -38,7 +38,7 @@ import org.springframework.boot.context.properties.bind.Nested; * @author Phillip Webb * @since 1.2.0 */ -@Target({ ElementType.FIELD, ElementType.RECORD_COMPONENT }) +@Target({ ElementType.FIELD, ElementType.RECORD_COMPONENT, ElementType.METHOD }) @Retention(RetentionPolicy.RUNTIME) @Documented @Nested From 4415b4ab1ce60282646100c988bed30e817bf241 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 15 Jul 2024 09:24:42 +0200 Subject: [PATCH 2/2] Polish "Allow NestedConfigurationProperty on getters" See gh-38844 --- .../native-image/advanced-topics.adoc | 1 + .../annotation-processor.adoc | 2 +- ...ationMetadataAnnotationProcessorTests.java | 15 +++- .../NestedConfigurationProperty.java | 2 +- .../method/NestedPropertiesMethod.java | 69 +++++++++++++++++++ .../method/NestedProperty.java | 31 +++++++++ .../specific/DeprecatedSimplePojo.java | 2 +- .../specific/InnerClassProperties.java | 2 +- .../NestedConfigurationProperty.java | 48 +++++++++++-- 9 files changed, 161 insertions(+), 11 deletions(-) create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/method/NestedPropertiesMethod.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/method/NestedProperty.java diff --git a/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/packaging/native-image/advanced-topics.adoc b/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/packaging/native-image/advanced-topics.adoc index 5b7e5f6ac3..d89da49cdd 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/packaging/native-image/advanced-topics.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/packaging/native-image/advanced-topics.adoc @@ -17,6 +17,7 @@ include-code::Nested[] The example above produces configuration properties for `my.properties.name` and `my.properties.nested.number`. Without the `@NestedConfigurationProperty` annotation on the `nested` field, the `my.properties.nested.number` property would not be bindable in a native image. +You can also annotate the getter method. When using constructor binding, you have to annotate the field with `@NestedConfigurationProperty`: diff --git a/spring-boot-project/spring-boot-docs/src/docs/antora/modules/specification/pages/configuration-metadata/annotation-processor.adoc b/spring-boot-project/spring-boot-docs/src/docs/antora/modules/specification/pages/configuration-metadata/annotation-processor.adoc index 6276bae645..976ef57035 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/antora/modules/specification/pages/configuration-metadata/annotation-processor.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/antora/modules/specification/pages/configuration-metadata/annotation-processor.adoc @@ -130,7 +130,7 @@ Consider the updated example: include-code::MyServerProperties[] The preceding example produces metadata information for `my.server.name`, `my.server.host.ip`, and `my.server.host.port` properties. -You can use the `@NestedConfigurationProperty` annotation on a field to indicate that a regular (non-inner) class should be treated as if it were nested. +You can use the `@NestedConfigurationProperty` annotation on a field or a getter method to indicate that a regular (non-inner) class should be treated as if it were nested. TIP: This has no effect on collections and maps, as those types are automatically identified, and a single metadata property is generated for each of them. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessorTests.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessorTests.java index 035b2b9e94..070cc69bbd 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessorTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessorTests.java @@ -22,6 +22,7 @@ import org.springframework.boot.configurationprocessor.metadata.ConfigurationMet import org.springframework.boot.configurationprocessor.metadata.ItemMetadata; import org.springframework.boot.configurationprocessor.metadata.Metadata; import org.springframework.boot.configurationsample.deprecation.Dbcp2Configuration; +import org.springframework.boot.configurationsample.method.NestedPropertiesMethod; import org.springframework.boot.configurationsample.record.ExampleRecord; import org.springframework.boot.configurationsample.record.NestedPropertiesRecord; import org.springframework.boot.configurationsample.record.RecordWithGetter; @@ -337,8 +338,9 @@ class ConfigurationMetadataAnnotationProcessorTests extends AbstractMetadataGene assertThat(metadata).has(Metadata.withProperty("config.third.value")); assertThat(metadata).has(Metadata.withProperty("config.fourth")); assertThat(metadata).isNotEqualTo(Metadata.withGroup("config.fourth")); - assertThat(metadata) - .has(Metadata.withGroup("config.fifth").ofType(DeprecatedSimplePojo.class).fromSource(InnerClassProperties.class)); + assertThat(metadata).has(Metadata.withGroup("config.fifth") + .ofType(DeprecatedSimplePojo.class) + .fromSource(InnerClassProperties.class)); assertThat(metadata).has(Metadata.withProperty("config.fifth.value").withDeprecation()); } @@ -362,6 +364,15 @@ class ConfigurationMetadataAnnotationProcessorTests extends AbstractMetadataGene assertThat(metadata).isNotEqualTo(Metadata.withProperty("specific.foo")); } + @Test + void nestedClassMethod() { + ConfigurationMetadata metadata = compile(NestedPropertiesMethod.class); + assertThat(metadata).has(Metadata.withGroup("method-nested.nested")); + assertThat(metadata).has(Metadata.withProperty("method-nested.nested.my-nested-property")); + assertThat(metadata).has(Metadata.withGroup("method-nested.inner.nested")); + assertThat(metadata).has(Metadata.withProperty("method-nested.inner.nested.my-nested-property")); + } + @Test void nestedClassChildProperties() { ConfigurationMetadata metadata = compile(ClassWithNestedProperties.class); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/NestedConfigurationProperty.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/NestedConfigurationProperty.java index b50f889001..c16fab5ef6 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/NestedConfigurationProperty.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/NestedConfigurationProperty.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2024 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. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/method/NestedPropertiesMethod.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/method/NestedPropertiesMethod.java new file mode 100644 index 0000000000..2139e830dc --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/method/NestedPropertiesMethod.java @@ -0,0 +1,69 @@ +/* + * Copyright 2012-2024 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.configurationsample.method; + +import org.springframework.boot.configurationsample.ConfigurationProperties; +import org.springframework.boot.configurationsample.NestedConfigurationProperty; + +@ConfigurationProperties("method-nested") +public class NestedPropertiesMethod { + + private String myProperty; + + private final NestedProperty nested = new NestedProperty(); + + private final Inner inner = new Inner(); + + public String getMyProperty() { + return this.myProperty; + } + + public void setMyProperty(String myProperty) { + this.myProperty = myProperty; + } + + @NestedConfigurationProperty + public NestedProperty getNested() { + return this.nested; + } + + public Inner getInner() { + return this.inner; + } + + public static class Inner { + + private String myInnerProperty; + + private final NestedProperty nested = new NestedProperty(); + + public String getMyInnerProperty() { + return this.myInnerProperty; + } + + public void setMyInnerProperty(String myInnerProperty) { + this.myInnerProperty = myInnerProperty; + } + + @NestedConfigurationProperty + public NestedProperty getNested() { + return this.nested; + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/method/NestedProperty.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/method/NestedProperty.java new file mode 100644 index 0000000000..fde146e405 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/method/NestedProperty.java @@ -0,0 +1,31 @@ +/* + * Copyright 2012-2024 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.configurationsample.method; + +public class NestedProperty { + + private String myNestedProperty; + + public String getMyNestedProperty() { + return this.myNestedProperty; + } + + public void setMyNestedProperty(String myNestedProperty) { + this.myNestedProperty = myNestedProperty; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/specific/DeprecatedSimplePojo.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/specific/DeprecatedSimplePojo.java index 8bca93258c..2beb61749f 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/specific/DeprecatedSimplePojo.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/specific/DeprecatedSimplePojo.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2024 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. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/specific/InnerClassProperties.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/specific/InnerClassProperties.java index 3e7f7d079a..049371b45f 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/specific/InnerClassProperties.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/specific/InnerClassProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2024 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. diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/NestedConfigurationProperty.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/NestedConfigurationProperty.java index cced7a62a6..3701772d4b 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/NestedConfigurationProperty.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/NestedConfigurationProperty.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2024 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. @@ -25,14 +25,52 @@ import java.lang.annotation.Target; import org.springframework.boot.context.properties.bind.Nested; /** - * Indicates that a field in a {@link ConfigurationProperties @ConfigurationProperties} + * Indicates that a property in a {@link ConfigurationProperties @ConfigurationProperties} * object should be treated as if it were a nested type. This annotation has no bearing on * the actual binding processes, but it is used by the - * {@code spring-boot-configuration-processor} as a hint that a field is not bound as a - * single value. When this is specified, a nested group is created for the field and its - * type is harvested. + * {@code spring-boot-configuration-processor} as a hint that a property is not bound as a + * single value. When this is specified, a nested group is created for the property and + * its type is harvested. + *

+ * In the example below, {@code Host} is flagged as a nested property using its field and + * an {@code example.server.host} nested group is created with any property that + * {@code Host} defines:


+ * @ConfigurationProperties("example.server")
+ * class ServerProperties {
+ *
+ *     @NestedConfigurationProperty
+ *     private final Host host = new Host();
+ *
+ *     public Host getHost() { ... }
+ *
+ *     // Other properties, getter, setter.
+ *
+ * }
+ *

+ * The annotation can also be specified on a getter method. If you use records, you can + * annotate the record component. *

* This has no effect on collections and maps as these types are automatically identified. + * Also, the annotation is not necessary if the target type is an inner class of the + * {@link ConfigurationProperties @ConfigurationProperties} object. In the example below, + * {@code Host} is detected as a nested type as it is defined as an inner class: + *


+ * @ConfigurationProperties("example.server")
+ * class ServerProperties {
+ *
+ *     private final Host host = new Host();
+ *
+ *     public Host getHost() { ... }
+ *
+ *     // Other properties, getter, setter.
+ *
+ *     public static class Host {
+ *
+ *         // properties, getter, setter.
+ *
+ *     }
+ *
+ * }
* * @author Stephane Nicoll * @author Phillip Webb