Skip to content

Commit c268f5d

Browse files
philwebbmbhave
andcommitted
Skip direct @ConfugurationProperties binding
Add a `BindRestriction` option to `Bindable` which allows direct property binding to be bypassed. The option is automatically applied by the `ConfigurationPropertiesBinder`. Prior to this commit, `@ConfugurationProperties` binding could silently fail if a direct property existed that could be converted to the properties class. This can be the case if a single-argument constructor is available as the `ObjectToObject` converter would kick in. Closes gh-16038 Co-authored-by: Madhura Bhave <[email protected]>
1 parent 5fc49aa commit c268f5d

File tree

8 files changed

+239
-14
lines changed

8 files changed

+239
-14
lines changed

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

Lines changed: 29 additions & 1 deletion
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-2021 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.
@@ -29,22 +29,27 @@
2929
import org.springframework.beans.factory.support.AbstractBeanDefinition;
3030
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
3131
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
32+
import org.springframework.boot.context.properties.bind.AbstractBindHandler;
33+
import org.springframework.boot.context.properties.bind.BindContext;
3234
import org.springframework.boot.context.properties.bind.BindHandler;
3335
import org.springframework.boot.context.properties.bind.BindResult;
3436
import org.springframework.boot.context.properties.bind.Bindable;
37+
import org.springframework.boot.context.properties.bind.Bindable.BindRestriction;
3538
import org.springframework.boot.context.properties.bind.Binder;
3639
import org.springframework.boot.context.properties.bind.BoundPropertiesTrackingBindHandler;
3740
import org.springframework.boot.context.properties.bind.PropertySourcesPlaceholdersResolver;
3841
import org.springframework.boot.context.properties.bind.handler.IgnoreErrorsBindHandler;
3942
import org.springframework.boot.context.properties.bind.handler.IgnoreTopLevelConverterNotFoundBindHandler;
4043
import org.springframework.boot.context.properties.bind.handler.NoUnboundElementsBindHandler;
4144
import org.springframework.boot.context.properties.bind.validation.ValidationBindHandler;
45+
import org.springframework.boot.context.properties.source.ConfigurationPropertyName;
4246
import org.springframework.boot.context.properties.source.ConfigurationPropertySource;
4347
import org.springframework.boot.context.properties.source.ConfigurationPropertySources;
4448
import org.springframework.boot.context.properties.source.UnboundElementsSourceFilter;
4549
import org.springframework.context.ApplicationContext;
4650
import org.springframework.context.ApplicationContextAware;
4751
import org.springframework.context.ConfigurableApplicationContext;
52+
import org.springframework.core.annotation.MergedAnnotations;
4853
import org.springframework.core.convert.ConversionService;
4954
import org.springframework.core.env.PropertySources;
5055
import org.springframework.validation.Validator;
@@ -108,6 +113,7 @@ private Validator getConfigurationPropertiesValidator(ApplicationContext applica
108113
private <T> BindHandler getBindHandler(Bindable<T> target, ConfigurationProperties annotation) {
109114
List<Validator> validators = getValidators(target);
110115
BindHandler handler = getHandler();
116+
handler = new ConfigurationPropertiesBindHander(handler);
111117
if (annotation.ignoreInvalidFields()) {
112118
handler = new IgnoreErrorsBindHandler(handler);
113119
}
@@ -230,4 +236,26 @@ ConfigurationPropertiesBinder create() {
230236

231237
}
232238

239+
/**
240+
* {@link BindHandler} to deal with
241+
* {@link ConfigurationProperties @ConfigurationProperties} concerns.
242+
*/
243+
private static class ConfigurationPropertiesBindHander extends AbstractBindHandler {
244+
245+
ConfigurationPropertiesBindHander(BindHandler handler) {
246+
super(handler);
247+
}
248+
249+
@Override
250+
public <T> Bindable<T> onStart(ConfigurationPropertyName name, Bindable<T> target, BindContext context) {
251+
return isConfigurationProperties(target.getType().resolve())
252+
? target.withBindRestrictions(BindRestriction.NO_DIRECT_PROPERTY) : target;
253+
}
254+
255+
private boolean isConfigurationProperties(Class<?> target) {
256+
return target != null && MergedAnnotations.from(target).isPresent(ConfigurationProperties.class);
257+
}
258+
259+
}
260+
233261
}

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

Lines changed: 53 additions & 6 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-2021 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.
@@ -18,11 +18,14 @@
1818

1919
import java.lang.annotation.Annotation;
2020
import java.lang.reflect.Array;
21+
import java.util.Arrays;
22+
import java.util.EnumSet;
2123
import java.util.List;
2224
import java.util.Map;
2325
import java.util.Set;
2426
import java.util.function.Supplier;
2527

28+
import org.springframework.boot.context.properties.source.ConfigurationProperty;
2629
import org.springframework.core.ResolvableType;
2730
import org.springframework.core.style.ToStringCreator;
2831
import org.springframework.util.Assert;
@@ -42,6 +45,8 @@ public final class Bindable<T> {
4245

4346
private static final Annotation[] NO_ANNOTATIONS = {};
4447

48+
private static final EnumSet<BindRestriction> NO_BIND_RESTRICTIONS = EnumSet.noneOf(BindRestriction.class);
49+
4550
private final ResolvableType type;
4651

4752
private final ResolvableType boxedType;
@@ -50,11 +55,15 @@ public final class Bindable<T> {
5055

5156
private final Annotation[] annotations;
5257

53-
private Bindable(ResolvableType type, ResolvableType boxedType, Supplier<T> value, Annotation[] annotations) {
58+
private final EnumSet<BindRestriction> bindRestrictions;
59+
60+
private Bindable(ResolvableType type, ResolvableType boxedType, Supplier<T> value, Annotation[] annotations,
61+
EnumSet<BindRestriction> bindRestrictions) {
5462
this.type = type;
5563
this.boxedType = boxedType;
5664
this.value = value;
5765
this.annotations = annotations;
66+
this.bindRestrictions = bindRestrictions;
5867
}
5968

6069
/**
@@ -105,6 +114,16 @@ public <A extends Annotation> A getAnnotation(Class<A> type) {
105114
return null;
106115
}
107116

117+
/**
118+
* Returns {@code true} if the specified bind restriction has been added.
119+
* @param bindRestriction the bind restriction to check
120+
* @return if the bind restriction has been added
121+
* @since 2.5.0
122+
*/
123+
public boolean hasBindRestriction(BindRestriction bindRestriction) {
124+
return this.bindRestrictions.contains(bindRestriction);
125+
}
126+
108127
@Override
109128
public boolean equals(Object obj) {
110129
if (this == obj) {
@@ -117,6 +136,7 @@ public boolean equals(Object obj) {
117136
boolean result = true;
118137
result = result && nullSafeEquals(this.type.resolve(), other.type.resolve());
119138
result = result && nullSafeEquals(this.annotations, other.annotations);
139+
result = result && nullSafeEquals(this.bindRestrictions, other.bindRestrictions);
120140
return result;
121141
}
122142

@@ -126,6 +146,7 @@ public int hashCode() {
126146
int result = 1;
127147
result = prime * result + ObjectUtils.nullSafeHashCode(this.type);
128148
result = prime * result + ObjectUtils.nullSafeHashCode(this.annotations);
149+
result = prime * result + ObjectUtils.nullSafeHashCode(this.bindRestrictions);
129150
return result;
130151
}
131152

@@ -149,7 +170,7 @@ private boolean nullSafeEquals(Object o1, Object o2) {
149170
*/
150171
public Bindable<T> withAnnotations(Annotation... annotations) {
151172
return new Bindable<>(this.type, this.boxedType, this.value,
152-
(annotations != null) ? annotations : NO_ANNOTATIONS);
173+
(annotations != null) ? annotations : NO_ANNOTATIONS, NO_BIND_RESTRICTIONS);
153174
}
154175

155176
/**
@@ -162,7 +183,7 @@ public Bindable<T> withExistingValue(T existingValue) {
162183
existingValue == null || this.type.isArray() || this.boxedType.resolve().isInstance(existingValue),
163184
() -> "ExistingValue must be an instance of " + this.type);
164185
Supplier<T> value = (existingValue != null) ? () -> existingValue : null;
165-
return new Bindable<>(this.type, this.boxedType, value, this.annotations);
186+
return new Bindable<>(this.type, this.boxedType, value, this.annotations, this.bindRestrictions);
166187
}
167188

168189
/**
@@ -171,7 +192,19 @@ public Bindable<T> withExistingValue(T existingValue) {
171192
* @return an updated {@link Bindable}
172193
*/
173194
public Bindable<T> withSuppliedValue(Supplier<T> suppliedValue) {
174-
return new Bindable<>(this.type, this.boxedType, suppliedValue, this.annotations);
195+
return new Bindable<>(this.type, this.boxedType, suppliedValue, this.annotations, this.bindRestrictions);
196+
}
197+
198+
/**
199+
* Create an updated {@link Bindable} instance with additional bind restrictions.
200+
* @param additionalRestrictions any additional restrictions to apply
201+
* @return an updated {@link Bindable}
202+
* @since 2.5.0
203+
*/
204+
public Bindable<T> withBindRestrictions(BindRestriction... additionalRestrictions) {
205+
EnumSet<BindRestriction> bindRestrictions = EnumSet.copyOf(this.bindRestrictions);
206+
bindRestrictions.addAll(Arrays.asList(additionalRestrictions));
207+
return new Bindable<>(this.type, this.boxedType, this.value, this.annotations, bindRestrictions);
175208
}
176209

177210
/**
@@ -244,7 +277,7 @@ public static <K, V> Bindable<Map<K, V>> mapOf(Class<K> keyType, Class<V> valueT
244277
public static <T> Bindable<T> of(ResolvableType type) {
245278
Assert.notNull(type, "Type must not be null");
246279
ResolvableType boxedType = box(type);
247-
return new Bindable<>(type, boxedType, null, NO_ANNOTATIONS);
280+
return new Bindable<>(type, boxedType, null, NO_ANNOTATIONS, NO_BIND_RESTRICTIONS);
248281
}
249282

250283
private static ResolvableType box(ResolvableType type) {
@@ -260,4 +293,18 @@ private static ResolvableType box(ResolvableType type) {
260293
return type;
261294
}
262295

296+
/**
297+
* Restrictions that can be applied when binding values.
298+
*
299+
* @since 2.5.0
300+
*/
301+
public enum BindRestriction {
302+
303+
/**
304+
* Do not bind direct {@link ConfigurationProperty} matches.
305+
*/
306+
NO_DIRECT_PROPERTY
307+
308+
}
309+
263310
}

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

Lines changed: 6 additions & 4 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-2021 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.
@@ -30,6 +30,7 @@
3030

3131
import org.springframework.beans.PropertyEditorRegistry;
3232
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
33+
import org.springframework.boot.context.properties.bind.Bindable.BindRestriction;
3334
import org.springframework.boot.context.properties.source.ConfigurationProperty;
3435
import org.springframework.boot.context.properties.source.ConfigurationPropertyName;
3536
import org.springframework.boot.context.properties.source.ConfigurationPropertySource;
@@ -366,7 +367,7 @@ private <T> T handleBindError(ConfigurationPropertyName name, Bindable<T> target
366367

367368
private <T> Object bindObject(ConfigurationPropertyName name, Bindable<T> target, BindHandler handler,
368369
Context context, boolean allowRecursiveBinding) {
369-
ConfigurationProperty property = findProperty(name, context);
370+
ConfigurationProperty property = findProperty(name, target, context);
370371
if (property == null && context.depth != 0 && containsNoDescendantOf(context.getSources(), name)) {
371372
return null;
372373
}
@@ -414,8 +415,9 @@ private <T> Object bindAggregate(ConfigurationPropertyName name, Bindable<T> tar
414415
return context.withIncreasedDepth(() -> aggregateBinder.bind(name, target, elementBinder));
415416
}
416417

417-
private ConfigurationProperty findProperty(ConfigurationPropertyName name, Context context) {
418-
if (name.isEmpty()) {
418+
private <T> ConfigurationProperty findProperty(ConfigurationPropertyName name, Bindable<T> target,
419+
Context context) {
420+
if (name.isEmpty() || target.hasBindRestriction(BindRestriction.NO_DIRECT_PROPERTY)) {
419421
return null;
420422
}
421423
for (ConfigurationPropertySource source : context.getSources()) {

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

Lines changed: 19 additions & 1 deletion
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-2021 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.
@@ -1010,6 +1010,18 @@ void loadWhenConstructorBindingWithOuterClassDeducedConstructorBound() {
10101010
assertThat(bean.getNested().getOuter().getAge()).isEqualTo(5);
10111011
}
10121012

1013+
@Test
1014+
void loadWhenConfigurationPropertiesPrefixMatchesPropertyInEnvironment() {
1015+
MutablePropertySources sources = this.context.getEnvironment().getPropertySources();
1016+
Map<String, Object> source = new HashMap<>();
1017+
source.put("test", "bar");
1018+
source.put("test.a", "baz");
1019+
sources.addLast(new MapPropertySource("test", source));
1020+
load(WithPublicStringConstructorPropertiesConfiguration.class);
1021+
WithPublicStringConstructorProperties bean = this.context.getBean(WithPublicStringConstructorProperties.class);
1022+
assertThat(bean.getA()).isEqualTo("baz");
1023+
}
1024+
10131025
@Test
10141026
void boundPropertiesShouldBeRecorded() {
10151027
load(NestedConfiguration.class, "name=foo", "nested.name=bar");
@@ -2584,4 +2596,10 @@ int getAge() {
25842596

25852597
}
25862598

2599+
@Configuration
2600+
@EnableConfigurationProperties(WithPublicStringConstructorProperties.class)
2601+
static class WithPublicStringConstructorPropertiesConfiguration {
2602+
2603+
}
2604+
25872605
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/*
2+
* Copyright 2012-2021 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.context.properties;
18+
19+
/**
20+
* A {@link ConfigurationProperties @ConfigurationProperties} with an additional
21+
* single-arg public constructor. Used in {@link ConfigurationPropertiesTests}.
22+
*
23+
* @author Madhura Bhave
24+
*/
25+
@ConfigurationProperties(prefix = "test")
26+
public class WithPublicStringConstructorProperties {
27+
28+
private String a;
29+
30+
public WithPublicStringConstructorProperties() {
31+
}
32+
33+
public WithPublicStringConstructorProperties(String a) {
34+
this.a = a;
35+
}
36+
37+
public String getA() {
38+
return this.a;
39+
}
40+
41+
public void setA(String a) {
42+
this.a = a;
43+
}
44+
45+
}

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

Lines changed: 18 additions & 1 deletion
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-2021 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.
@@ -22,6 +22,7 @@
2222

2323
import org.junit.jupiter.api.Test;
2424

25+
import org.springframework.boot.context.properties.bind.Bindable.BindRestriction;
2526
import org.springframework.context.annotation.Bean;
2627
import org.springframework.core.ResolvableType;
2728
import org.springframework.core.annotation.AnnotationUtils;
@@ -173,6 +174,22 @@ void withSuppliedValueDoesNotForgetAnnotations() {
173174
assertThat(bindable.getAnnotations()).containsExactly(annotation);
174175
}
175176

177+
@Test
178+
void hasBindRestrictionWhenDefaultReturnsFalse() {
179+
Bindable<String> bindable = Bindable.of(String.class);
180+
for (BindRestriction bindRestriction : BindRestriction.values()) {
181+
assertThat(bindable.hasBindRestriction(bindRestriction)).isFalse();
182+
}
183+
}
184+
185+
@Test
186+
void withBindRestrictionAddsBindRestriction() {
187+
Bindable<String> bindable = Bindable.of(String.class);
188+
Bindable<String> restricted = bindable.withBindRestrictions(BindRestriction.NO_DIRECT_PROPERTY);
189+
assertThat(bindable.hasBindRestriction(BindRestriction.NO_DIRECT_PROPERTY)).isFalse();
190+
assertThat(restricted.hasBindRestriction(BindRestriction.NO_DIRECT_PROPERTY)).isTrue();
191+
}
192+
176193
@Retention(RetentionPolicy.RUNTIME)
177194
@interface TestAnnotation {
178195

0 commit comments

Comments
 (0)