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/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..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; @@ -45,6 +46,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 +338,10 @@ 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 @@ -358,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 41f11f6edf..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. @@ -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/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 new file mode 100644 index 0000000000..2beb61749f --- /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-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.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..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. @@ -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..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,20 +25,58 @@ 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 * @since 1.2.0 */ -@Target({ ElementType.FIELD, ElementType.RECORD_COMPONENT }) +@Target({ ElementType.FIELD, ElementType.RECORD_COMPONENT, ElementType.METHOD }) @Retention(RetentionPolicy.RUNTIME) @Documented @Nested