Skip to content

Commit 0209ab5

Browse files
committed
Merge pull request #39452 from BenchmarkingBuffalo
* gh-39452: Polish "Support `@Name` with JavaBean-based configuration properties" Support `@Name` with JavaBean-based configuration properties Closes gh-39452
2 parents c790deb + 23b3446 commit 0209ab5

File tree

13 files changed

+229
-63
lines changed

13 files changed

+229
-63
lines changed

spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/features/external-config.adoc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -713,6 +713,8 @@ The preceding POJO defines the following properties:
713713
* `my.service.security.password`.
714714
* `my.service.security.roles`, with a collection of `String` that defaults to `USER`.
715715

716+
TIP: To use a reserved keyword in the name of a property, such as `my.service.import`, use the `@Name` annotation on the property's field.
717+
716718
NOTE: The properties that map to `@ConfigurationProperties` classes available in Spring Boot, which are configured through properties files, YAML files, environment variables, and other mechanisms, are public API but the accessors (getters/setters) of the class itself are not meant to be used directly.
717719

718720
[NOTE]

spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/PropertyDescriptorResolver.java

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ private Stream<PropertyDescriptor> resolveConstructorBoundProperties(TypeElement
8585

8686
private PropertyDescriptor extracted(TypeElement declaringElement, TypeElementMembers members,
8787
VariableElement parameter) {
88-
String name = getParameterName(parameter);
88+
String name = getPropertyName(parameter);
8989
TypeMirror type = parameter.asType();
9090
ExecutableElement getter = members.getPublicGetter(name, type);
9191
ExecutableElement setter = members.getPublicSetter(name, type);
@@ -98,12 +98,16 @@ private PropertyDescriptor extracted(TypeElement declaringElement, TypeElementMe
9898
field);
9999
}
100100

101-
private String getParameterName(VariableElement parameter) {
101+
private String getPropertyName(VariableElement parameter) {
102+
return getPropertyName(parameter, parameter.getSimpleName().toString());
103+
}
104+
105+
private String getPropertyName(VariableElement parameter, String fallback) {
102106
AnnotationMirror nameAnnotation = this.environment.getNameAnnotation(parameter);
103107
if (nameAnnotation != null) {
104108
return this.environment.getAnnotationElementStringValue(nameAnnotation, "value");
105109
}
106-
return parameter.getSimpleName().toString();
110+
return fallback;
107111
}
108112

109113
private Stream<PropertyDescriptor> resolveJavaBeanProperties(TypeElement declaringElement,
@@ -114,16 +118,16 @@ private Stream<PropertyDescriptor> resolveJavaBeanProperties(TypeElement declari
114118
VariableElement field = members.getFields().get(name);
115119
ExecutableElement getter = findMatchingGetter(members, getters, field);
116120
TypeMirror propertyType = getter.getReturnType();
117-
register(candidates, new JavaBeanPropertyDescriptor(name, propertyType, declaringElement, getter,
118-
members.getPublicSetter(name, propertyType), field, factoryMethod));
121+
register(candidates, new JavaBeanPropertyDescriptor(getPropertyName(field, name), propertyType,
122+
declaringElement, getter, members.getPublicSetter(name, propertyType), field, factoryMethod));
119123
});
120124
// Then check for Lombok ones
121125
members.getFields().forEach((name, field) -> {
122126
TypeMirror propertyType = field.asType();
123127
ExecutableElement getter = members.getPublicGetter(name, propertyType);
124128
ExecutableElement setter = members.getPublicSetter(name, propertyType);
125-
register(candidates, new LombokPropertyDescriptor(name, propertyType, declaringElement, getter, setter,
126-
field, factoryMethod));
129+
register(candidates, new LombokPropertyDescriptor(getPropertyName(field, name), propertyType,
130+
declaringElement, getter, setter, field, factoryMethod));
127131
});
128132
return candidates.values().stream();
129133
}

spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/ImmutableNameAnnotationPropertiesTests.java

Lines changed: 0 additions & 41 deletions
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/*
2+
* Copyright 2012-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.configurationprocessor;
18+
19+
import org.junit.jupiter.api.Test;
20+
21+
import org.springframework.boot.configurationprocessor.metadata.ConfigurationMetadata;
22+
import org.springframework.boot.configurationprocessor.metadata.Metadata;
23+
import org.springframework.boot.configurationsample.immutable.ConstructorParameterNameAnnotationProperties;
24+
import org.springframework.boot.configurationsample.immutable.JavaBeanNameAnnotationProperties;
25+
import org.springframework.boot.configurationsample.immutable.RecordComponentNameAnnotationProperties;
26+
27+
import static org.assertj.core.api.Assertions.assertThat;
28+
29+
/**
30+
* Metadata generation tests for using {@code @Name}.
31+
*
32+
* @author Phillip Webb
33+
*/
34+
class NameAnnotationPropertiesTests extends AbstractMetadataGenerationTests {
35+
36+
@Test
37+
void constructorParameterNameAnnotationProperties() {
38+
ConfigurationMetadata metadata = compile(ConstructorParameterNameAnnotationProperties.class);
39+
assertThat(metadata).has(Metadata.withProperty("named.import", String.class)
40+
.fromSource(ConstructorParameterNameAnnotationProperties.class));
41+
}
42+
43+
@Test
44+
void recordComponentNameAnnotationProperties() {
45+
ConfigurationMetadata metadata = compile(RecordComponentNameAnnotationProperties.class);
46+
assertThat(metadata).has(Metadata.withProperty("named.import", String.class)
47+
.fromSource(RecordComponentNameAnnotationProperties.class));
48+
}
49+
50+
@Test
51+
void javaBeanNameAnnotationProperties() {
52+
ConfigurationMetadata metadata = compile(JavaBeanNameAnnotationProperties.class);
53+
assertThat(metadata).has(
54+
Metadata.withProperty("named.import", String.class).fromSource(JavaBeanNameAnnotationProperties.class));
55+
}
56+
57+
}

spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/PropertyDescriptorResolverTests.java

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,13 @@
3131
import org.springframework.boot.configurationprocessor.metadata.ItemMetadata;
3232
import org.springframework.boot.configurationprocessor.test.RoundEnvironmentTester;
3333
import org.springframework.boot.configurationprocessor.test.TestableAnnotationProcessor;
34+
import org.springframework.boot.configurationsample.immutable.ConstructorParameterNameAnnotationProperties;
3435
import org.springframework.boot.configurationsample.immutable.ImmutableClassConstructorBindingProperties;
3536
import org.springframework.boot.configurationsample.immutable.ImmutableDeducedConstructorBindingProperties;
3637
import org.springframework.boot.configurationsample.immutable.ImmutableMultiConstructorProperties;
37-
import org.springframework.boot.configurationsample.immutable.ImmutableNameAnnotationProperties;
3838
import org.springframework.boot.configurationsample.immutable.ImmutableSimpleProperties;
39+
import org.springframework.boot.configurationsample.immutable.JavaBeanNameAnnotationProperties;
40+
import org.springframework.boot.configurationsample.immutable.RecordComponentNameAnnotationProperties;
3941
import org.springframework.boot.configurationsample.lombok.LombokExplicitProperties;
4042
import org.springframework.boot.configurationsample.lombok.LombokSimpleDataProperties;
4143
import org.springframework.boot.configurationsample.lombok.LombokSimpleProperties;
@@ -155,8 +157,20 @@ void propertiesWithMultiConstructorNoDirective() {
155157
}
156158

157159
@Test
158-
void propertiesWithNameAnnotationParameter() {
159-
process(ImmutableNameAnnotationProperties.class,
160+
void contructorParameterPropertyWithNameAnnotationParameter() {
161+
process(ConstructorParameterNameAnnotationProperties.class,
162+
propertyNames((stream) -> assertThat(stream).containsExactly("import")));
163+
}
164+
165+
@Test
166+
void recordComponentPropertyWithNameAnnotationParameter() {
167+
process(RecordComponentNameAnnotationProperties.class,
168+
propertyNames((stream) -> assertThat(stream).containsExactly("import")));
169+
}
170+
171+
@Test
172+
void javaBeanPropertyWithNameAnnotationParameter() {
173+
process(JavaBeanNameAnnotationProperties.class,
160174
propertyNames((stream) -> assertThat(stream).containsExactly("import")));
161175
}
162176

spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/Name.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2020 the original author or authors.
2+
* Copyright 2012-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -28,7 +28,7 @@
2828
*
2929
* @author Phillip Webb
3030
*/
31-
@Target(ElementType.PARAMETER)
31+
@Target({ ElementType.PARAMETER, ElementType.FIELD })
3232
@Retention(RetentionPolicy.RUNTIME)
3333
@Documented
3434
public @interface Name {
Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2022 the original author or authors.
2+
* Copyright 2012-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -20,16 +20,16 @@
2020
import org.springframework.boot.configurationsample.Name;
2121

2222
/**
23-
* Immutable properties making use of {@code @Name}.
23+
* Immutable class properties making use of {@code @Name}.
2424
*
2525
* @author Phillip Webb
2626
*/
2727
@ConfigurationProperties("named")
28-
public class ImmutableNameAnnotationProperties {
28+
public class ConstructorParameterNameAnnotationProperties {
2929

3030
private final String imports;
3131

32-
public ImmutableNameAnnotationProperties(@Name("import") String imports) {
32+
public ConstructorParameterNameAnnotationProperties(@Name("import") String imports) {
3333
this.imports = imports;
3434
}
3535

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
* Copyright 2012-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.configurationsample.immutable;
18+
19+
import org.springframework.boot.configurationsample.ConfigurationProperties;
20+
import org.springframework.boot.configurationsample.Name;
21+
22+
/**
23+
* Java bean properties making use of {@code @Name}.
24+
*
25+
* @author Andy Wilkinson
26+
*/
27+
@ConfigurationProperties("named")
28+
public class JavaBeanNameAnnotationProperties {
29+
30+
@Name("import")
31+
private String imports;
32+
33+
public String getImports() {
34+
return this.imports;
35+
}
36+
37+
public void setImports(String imports) {
38+
this.imports = imports;
39+
}
40+
41+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
* Copyright 2012-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.configurationsample.immutable;
18+
19+
import org.springframework.boot.configurationsample.ConfigurationProperties;
20+
import org.springframework.boot.configurationsample.Name;
21+
22+
/**
23+
* Immutable record properties making use of {@code @Name}.
24+
*
25+
* @author Andy Wilkinson
26+
*/
27+
@ConfigurationProperties("named")
28+
public record RecordComponentNameAnnotationProperties(@Name("import") String imports) {
29+
30+
}

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/JavaBeanBinder.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
*
4747
* @author Phillip Webb
4848
* @author Madhura Bhave
49+
* @author Lasse Wulff
4950
*/
5051
class JavaBeanBinder implements DataObjectBinder {
5152

@@ -92,7 +93,7 @@ private <T> boolean bind(DataObjectPropertyBinder propertyBinder, Bean<T> bean,
9293

9394
private <T> boolean bind(BeanSupplier<T> beanSupplier, DataObjectPropertyBinder propertyBinder,
9495
BeanProperty property) {
95-
String propertyName = property.getName();
96+
String propertyName = determinePropertyName(property);
9697
ResolvableType type = property.getType();
9798
Supplier<Object> value = property.getValue(beanSupplier);
9899
Annotation[] annotations = property.getAnnotations();
@@ -110,6 +111,15 @@ else if (value == null || !bound.equals(value.get())) {
110111
return true;
111112
}
112113

114+
private String determinePropertyName(BeanProperty property) {
115+
return Arrays.stream((property.getAnnotations() != null) ? property.getAnnotations() : new Annotation[0])
116+
.filter((annotation) -> annotation.annotationType() == Name.class)
117+
.findFirst()
118+
.map(Name.class::cast)
119+
.map(Name::value)
120+
.orElse(property.getName());
121+
}
122+
113123
/**
114124
* The properties of a bean that may be bound.
115125
*/

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/Name.java

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2022 the original author or authors.
2+
* Copyright 2012-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -23,15 +23,19 @@
2323
import java.lang.annotation.Target;
2424

2525
/**
26-
* Annotation that can be used to specify the name when binding to an immutable property.
27-
* This annotation may be required when binding to names that clash with reserved language
26+
* Annotation that can be used to specify the name when binding to a property. This
27+
* annotation may be required when binding to names that clash with reserved language
2828
* keywords.
29+
* <p>
30+
* When naming a JavaBean-based property, annotate the field. When naming a
31+
* constructor-bound property, annotate the constructor parameter or record component.
2932
*
3033
* @author Phillip Webb
34+
* @author Lasse Wulff
3135
* @since 2.4.0
3236
*/
3337
@Retention(RetentionPolicy.RUNTIME)
34-
@Target(ElementType.PARAMETER)
38+
@Target({ ElementType.FIELD, ElementType.PARAMETER })
3539
@Documented
3640
public @interface Name {
3741

0 commit comments

Comments
 (0)