Skip to content

Commit 386c0a6

Browse files
committed
Relax @ConstructorBinding member class requirement
Update `@ConfigurationProperties` so that `@ConstructorBinding` classes no longer need to repeat the annotation for their members. Closes gh-18481
1 parent e6bb7a0 commit 386c0a6

File tree

6 files changed

+174
-8
lines changed

6 files changed

+174
-8
lines changed

spring-boot-project/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -881,7 +881,6 @@ The example in the previous section can be rewritten in an immutable fashion as
881881
882882
private final List<String> roles;
883883
884-
@ConstructorBinding
885884
public Security(String username, String password,
886885
@DefaultValue("USER") List<String> roles) {
887886
this.username = username;
@@ -903,16 +902,16 @@ The example in the previous section can be rewritten in an immutable fashion as
903902
In this setup, the `@ImmutableConfigurationProperties` annotation is used to indicate that constructor binding should be used.
904903
This means that the binder will expect to find a constructor with the parameters that you wish to have bound.
905904

906-
Nested classes that also require constructor binding (such as `Security` in the example above) should use the `@ConstructorBinding` annotation.
905+
Nested members of a `@ImmutableConfigurationProperties` class (such as `Security` in the example above) will also be bound via their constructor.
907906

908907
Default values can be specified using `@DefaultValue` and the same conversion service will be applied to coerce the `String` value to the target type of a missing property.
909908

910-
TIP: You can also use `@ConstructorBinding` on the actual constructor that should be bound.
911-
This is required if you have more than one constructor for your class.
912-
913909
NOTE: To use constructor binding the class must be enabled using `@EnableConfigurationProperties` or configuration property scanning.
914910
You cannot use constructor binding with beans that are created by the regular Spring mechanisms (e.g. `@Component` beans, beans created via `@Bean` methods or beans loaded using `@Import`)
915911

912+
TIP: `@ImmutableConfigurationProperties` is actually a meta-annotation composed of `@ConfigurationProperties` and `@ConstructorBinding`.
913+
If you have more than one constructor for your class you can also use `@ConstructorBinding` directly on actual constructor that should be bound.
914+
916915

917916

918917
[[boot-features-external-config-enabling]]

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

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import java.util.stream.Stream;
2424

2525
import javax.lang.model.element.ExecutableElement;
26+
import javax.lang.model.element.NestingKind;
2627
import javax.lang.model.element.TypeElement;
2728
import javax.lang.model.element.VariableElement;
2829
import javax.lang.model.type.TypeMirror;
@@ -169,13 +170,24 @@ private ExecutableElement findBoundConstructor() {
169170
}
170171

171172
static ConfigurationPropertiesTypeElement of(TypeElement type, MetadataGenerationEnvironment env) {
172-
boolean constructorBoundType = env.hasConstructorBindingAnnotation(type);
173+
boolean constructorBoundType = isConstructorBoundType(type, env);
173174
List<ExecutableElement> constructors = ElementFilter.constructorsIn(type.getEnclosedElements());
174175
List<ExecutableElement> boundConstructors = constructors.stream()
175176
.filter(env::hasConstructorBindingAnnotation).collect(Collectors.toList());
176177
return new ConfigurationPropertiesTypeElement(type, constructorBoundType, constructors, boundConstructors);
177178
}
178179

180+
private static boolean isConstructorBoundType(TypeElement type, MetadataGenerationEnvironment env) {
181+
if (env.hasConstructorBindingAnnotation(type)) {
182+
return true;
183+
}
184+
if (type.getNestingKind() == NestingKind.MEMBER) {
185+
return isConstructorBoundType((TypeElement) type.getEnclosingElement(), env);
186+
}
187+
return false;
188+
189+
}
190+
179191
}
180192

181193
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
* Copyright 2012-2019 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.DeducedImmutableClassProperties;
24+
25+
import static org.assertj.core.api.Assertions.assertThat;
26+
27+
/**
28+
* Metadata generation tests for immutable properties deduced because they're nested.
29+
*
30+
* @author Phillip Webb
31+
*/
32+
class DeducedImmutablePropertiesMetadataGenerationTests extends AbstractMetadataGenerationTests {
33+
34+
@Test
35+
void immutableSimpleProperties() {
36+
ConfigurationMetadata metadata = compile(DeducedImmutableClassProperties.class);
37+
assertThat(metadata).has(Metadata.withGroup("test").fromSource(DeducedImmutableClassProperties.class));
38+
assertThat(metadata).has(Metadata.withProperty("test.nested.name", String.class));
39+
}
40+
41+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*
2+
* Copyright 2012-2019 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.ConstructorBinding;
21+
22+
/**
23+
* Inner properties, in immutable format.
24+
*
25+
* @author Phillip Webb
26+
*/
27+
@ConfigurationProperties("test")
28+
@ConstructorBinding
29+
public class DeducedImmutableClassProperties {
30+
31+
private final Nested nested;
32+
33+
public DeducedImmutableClassProperties(Nested nested) {
34+
this.nested = nested;
35+
}
36+
37+
public Nested getNested() {
38+
return this.nested;
39+
}
40+
41+
public static class Nested {
42+
43+
private String name;
44+
45+
public Nested(String name) {
46+
this.name = name;
47+
}
48+
49+
public String getName() {
50+
return this.name;
51+
}
52+
53+
}
54+
55+
}

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -265,13 +265,17 @@ public enum BindMethod {
265265
VALUE_OBJECT;
266266

267267
static BindMethod forClass(Class<?> type) {
268-
if (MergedAnnotations.from(type, SearchStrategy.TYPE_HIERARCHY).isPresent(ConstructorBinding.class)
269-
|| findBindConstructor(type) != null) {
268+
if (isConstructorBindingType(type) || findBindConstructor(type) != null) {
270269
return VALUE_OBJECT;
271270
}
272271
return JAVA_BEAN;
273272
}
274273

274+
private static boolean isConstructorBindingType(Class<?> type) {
275+
return MergedAnnotations.from(type, SearchStrategy.TYPE_HIERARCHY_AND_ENCLOSING_CLASSES)
276+
.isPresent(ConstructorBinding.class);
277+
}
278+
275279
static Constructor<?> findBindConstructor(Class<?> type) {
276280
if (KotlinDetector.isKotlinPresent() && KotlinDetector.isKotlinType(type)) {
277281
Constructor<?> constructor = BeanUtils.findPrimaryConstructor(type);

spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesTests.java

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -836,6 +836,19 @@ void loadWhenBindingToMultiConstructorConfigurationPropertiesUsingShortcutSyntax
836836
assertThat(nested.getAge()).isEqualTo(0);
837837
}
838838

839+
@Test // gh-18481
840+
void loadWhenBindingToNestedConstructorPropertiesWithDeducedNestedShouldBind() {
841+
MutablePropertySources sources = this.context.getEnvironment().getPropertySources();
842+
Map<String, Object> source = new HashMap<>();
843+
source.put("test.name", "spring");
844+
source.put("test.nested.age", "5");
845+
sources.addLast(new MapPropertySource("test", source));
846+
load(DeducedNestedConstructorPropertiesConfiguration.class);
847+
DeducedNestedConstructorProperties bean = this.context.getBean(DeducedNestedConstructorProperties.class);
848+
assertThat(bean.getName()).isEqualTo("spring");
849+
assertThat(bean.getNested().getAge()).isEqualTo(5);
850+
}
851+
839852
private AnnotationConfigApplicationContext load(Class<?> configuration, String... inlinedProperties) {
840853
return load(new Class<?>[] { configuration }, inlinedProperties);
841854
}
@@ -2014,4 +2027,46 @@ static class MultiConstructorConfigurationPropertiesConfiguration {
20142027

20152028
}
20162029

2030+
@Configuration(proxyBeanMethods = false)
2031+
@EnableConfigurationProperties(DeducedNestedConstructorProperties.class)
2032+
static class DeducedNestedConstructorPropertiesConfiguration {
2033+
2034+
}
2035+
2036+
@ImmutableConfigurationProperties("test")
2037+
static class DeducedNestedConstructorProperties {
2038+
2039+
private final String name;
2040+
2041+
private final Nested nested;
2042+
2043+
DeducedNestedConstructorProperties(String name, Nested nested) {
2044+
this.name = name;
2045+
this.nested = nested;
2046+
}
2047+
2048+
String getName() {
2049+
return this.name;
2050+
}
2051+
2052+
Nested getNested() {
2053+
return this.nested;
2054+
}
2055+
2056+
static class Nested {
2057+
2058+
private final int age;
2059+
2060+
Nested(int age) {
2061+
this.age = age;
2062+
}
2063+
2064+
int getAge() {
2065+
return this.age;
2066+
}
2067+
2068+
}
2069+
2070+
}
2071+
20172072
}

0 commit comments

Comments
 (0)