Skip to content

Commit 10bd5b7

Browse files
committed
Added @ConfigurationPropertyValue annotation and its processing (spring-projects#7986).
Added onNull() to BindHandler to be able to react to properties that bind to null to choose a fallback value instead. ConfigurationPropertyValue annotations (which allow defining a fallback value for a property) are now collected by ConfigurationPropertyValueReader and a map of fallback properties is passed into a FallbackBindHandler. The handler then reacts on a null value by providing the fallback value instead of the null value.
1 parent 2f51340 commit 10bd5b7

File tree

8 files changed

+444
-3
lines changed

8 files changed

+444
-3
lines changed

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

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,11 @@
3838
import org.springframework.boot.context.properties.bind.Binder;
3939
import org.springframework.boot.context.properties.bind.PropertySourcesPlaceholdersResolver;
4040
import org.springframework.boot.context.properties.bind.convert.BinderConversionService;
41+
import org.springframework.boot.context.properties.bind.handler.FallbackBindHandler;
4142
import org.springframework.boot.context.properties.bind.handler.IgnoreErrorsBindHandler;
4243
import org.springframework.boot.context.properties.bind.handler.NoUnboundElementsBindHandler;
4344
import org.springframework.boot.context.properties.bind.validation.ValidationBindHandler;
45+
import org.springframework.boot.context.properties.source.ConfigurationPropertyName;
4446
import org.springframework.boot.context.properties.source.ConfigurationPropertySource;
4547
import org.springframework.boot.context.properties.source.ConfigurationPropertySources;
4648
import org.springframework.boot.context.properties.source.UnboundElementsSourceFilter;
@@ -329,8 +331,10 @@ private void postProcessBeforeInitialization(Object bean, String beanName,
329331
Binder binder = new Binder(this.configurationSources,
330332
new PropertySourcesPlaceholdersResolver(this.propertySources),
331333
getBinderConversionService());
334+
Map<ConfigurationPropertyName, ConfigurationPropertyName> fallbacks = getConfigurationPropertyFallbacks(
335+
bean, beanName, annotation.prefix());
332336
Validator validator = determineValidator(bean);
333-
BindHandler handler = getBindHandler(annotation, validator);
337+
BindHandler handler = getBindHandler(annotation, validator, fallbacks);
334338
Bindable<?> bindable = Bindable.ofInstance(bean);
335339
try {
336340
binder.bind(annotation.prefix(), bindable, handler);
@@ -415,7 +419,8 @@ private boolean isJsr303Present() {
415419
}
416420

417421
private BindHandler getBindHandler(ConfigurationProperties annotation,
418-
Validator validator) {
422+
Validator validator,
423+
Map<ConfigurationPropertyName, ConfigurationPropertyName> fallbacks) {
419424
BindHandler handler = BindHandler.DEFAULT;
420425
if (annotation.ignoreInvalidFields()) {
421426
handler = new IgnoreErrorsBindHandler(handler);
@@ -427,9 +432,19 @@ private BindHandler getBindHandler(ConfigurationProperties annotation,
427432
if (validator != null) {
428433
handler = new ValidationBindHandler(handler, validator);
429434
}
435+
if (!fallbacks.isEmpty()) {
436+
handler = new FallbackBindHandler(fallbacks);
437+
}
430438
return handler;
431439
}
432440

441+
private Map<ConfigurationPropertyName, ConfigurationPropertyName> getConfigurationPropertyFallbacks(
442+
Object bean, String beanName, String prefix) {
443+
ConfigurationPropertyValueReader annotationReader = new ConfigurationPropertyValueReader(
444+
bean, beanName, prefix);
445+
return annotationReader.getPropertyFallbacks();
446+
}
447+
433448
/**
434449
* {@link LocalValidatorFactoryBean} supports classes annotated with
435450
* {@link Validated @Validated}.
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/*
2+
* Copyright 2012-2017 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+
* http://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+
import java.lang.annotation.Documented;
20+
import java.lang.annotation.ElementType;
21+
import java.lang.annotation.Retention;
22+
import java.lang.annotation.RetentionPolicy;
23+
import java.lang.annotation.Target;
24+
25+
/**
26+
* Annotation to be used on accessor methods or fields when the class is annotated with
27+
* {@link ConfigurationProperties}. It allows to specify additional metadata
28+
* for a single property.
29+
* <p>
30+
* Annotating a method that is not a getter or setter in the sense of the JavaBeans spec
31+
* will cause an exception.
32+
*
33+
* @author Tom Hombergs
34+
* @since 2.0.0
35+
* @see ConfigurationProperties
36+
*/
37+
@Target({ ElementType.FIELD, ElementType.METHOD })
38+
@Retention(RetentionPolicy.RUNTIME)
39+
@Documented
40+
public @interface ConfigurationPropertyValue {
41+
42+
/**
43+
* Name of the property whose value to use if the property with the name of
44+
* the annotated field itself is not defined.
45+
* <p>
46+
* The fallback property name has to be specified including potential prefixes defined
47+
* in {@link ConfigurationProperties} annotations.
48+
*
49+
* @return the name of the fallback property
50+
*/
51+
String fallback() default "";
52+
53+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
/*
2+
* Copyright 2012-2017 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+
* http://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+
import java.beans.PropertyDescriptor;
20+
import java.lang.reflect.Method;
21+
import java.util.HashMap;
22+
import java.util.Map;
23+
import java.util.stream.Collectors;
24+
25+
import org.springframework.beans.BeanUtils;
26+
import org.springframework.beans.factory.BeanCreationException;
27+
import org.springframework.boot.context.properties.source.ConfigurationPropertyName;
28+
import org.springframework.core.annotation.AnnotationUtils;
29+
import org.springframework.util.ClassUtils;
30+
import org.springframework.util.ReflectionUtils;
31+
import org.springframework.util.StringUtils;
32+
33+
/**
34+
* Utility class that reads {@link org.springframework.boot.context.properties.ConfigurationPropertyValue} annotations
35+
* from getters and fields of a bean and provides methods to access the metadata contained in the annotations.
36+
*
37+
* @author Tom Hombergs
38+
* @since 2.0.0
39+
* @see org.springframework.boot.context.properties.ConfigurationPropertyValue
40+
*/
41+
public class ConfigurationPropertyValueReader {
42+
43+
private Map<ConfigurationPropertyName, ConfigurationPropertyValue> annotations = new HashMap<>();
44+
45+
public ConfigurationPropertyValueReader(Object bean, String beanName, String prefix) {
46+
this.annotations = findAnnotations(bean, beanName, prefix);
47+
}
48+
49+
/**
50+
* Returns a map that maps configuration property names to their fallback properties. If this map does not contain
51+
* a value for a certain configuration property, it means that there is no fallback specified for this property.
52+
*
53+
* @return a map of configuration property names to their fallback property names.
54+
*/
55+
public Map<ConfigurationPropertyName, ConfigurationPropertyName> getPropertyFallbacks() {
56+
return this.annotations.entrySet().stream()
57+
.filter(entry -> !StringUtils.isEmpty(entry.getValue().fallback()))
58+
.collect(Collectors.toMap(Map.Entry::getKey, entry -> ConfigurationPropertyName.of(entry.getValue().fallback())));
59+
}
60+
61+
/**
62+
* Walks through the methods and fields of the specified bean to extract {@link ConfigurationPropertyValue}
63+
* annotations. A field can be annotated either by directly annotating the field or by annotating
64+
* the corresponding getter or setter method. This method will throw a {@link BeanCreationException} if
65+
* multiple annotations are found for the same field.
66+
*
67+
* @param bean the bean whose annotations to retrieve.
68+
* @param beanName the name of the bean (only used for proper error messages).
69+
* @param prefix the prefix of the superordinate {@link ConfigurationProperties} annotation. May be null.
70+
* @return a map that maps configuration property names to the annotations that were found for them.
71+
* @throws BeanCreationException if multiple {@link ConfigurationPropertyValue} annotations have been found for
72+
* the same field.
73+
*/
74+
private Map<ConfigurationPropertyName, ConfigurationPropertyValue> findAnnotations(Object bean, String beanName, String prefix) {
75+
Map<ConfigurationPropertyName, ConfigurationPropertyValue> fieldAnnotations = new HashMap<>();
76+
ReflectionUtils.doWithMethods(bean.getClass(), method -> {
77+
ConfigurationPropertyValue annotation = AnnotationUtils
78+
.findAnnotation(method, ConfigurationPropertyValue.class);
79+
if (annotation != null) {
80+
PropertyDescriptor propertyDescriptor = findPropertyDescriptorOrFail(
81+
beanName, method);
82+
ConfigurationPropertyName name = getConfigurationPropertyName(prefix, propertyDescriptor.getName());
83+
if (fieldAnnotations.containsKey(name)) {
84+
throw new BeanCreationException(beanName, "Invalid use of "
85+
+ ClassUtils
86+
.getShortName(ConfigurationPropertyValue.class)
87+
+ " on method '" + method.getName()
88+
+ "'. You may either annotate a field, a getter or a setter but not two of these.");
89+
}
90+
fieldAnnotations.put(name, annotation);
91+
}
92+
});
93+
94+
ReflectionUtils.doWithFields(bean.getClass(), field -> {
95+
ConfigurationPropertyValue annotation = AnnotationUtils
96+
.findAnnotation(field, ConfigurationPropertyValue.class);
97+
if (annotation != null) {
98+
ConfigurationPropertyName name = getConfigurationPropertyName(prefix, field.getName());
99+
if (fieldAnnotations.containsKey(name)) {
100+
throw new BeanCreationException(beanName, "Invalid use of "
101+
+ ClassUtils
102+
.getShortName(ConfigurationPropertyValue.class)
103+
+ " on field '" + field.getName()
104+
+ "'. You may either annotate a field, a getter or a setter but not two of these.");
105+
}
106+
fieldAnnotations.put(name, annotation);
107+
}
108+
});
109+
return fieldAnnotations;
110+
}
111+
112+
private PropertyDescriptor findPropertyDescriptorOrFail(String beanName,
113+
Method method) {
114+
PropertyDescriptor propertyDescriptor = BeanUtils.findPropertyForMethod(method);
115+
if (propertyDescriptor == null) {
116+
throw new BeanCreationException(beanName, "Invalid use of "
117+
+ ClassUtils.getShortName(ConfigurationPropertyValue.class)
118+
+ " on method '" + method.getName()
119+
+ "'. This annotation may only be used on getter and setter methods or fields.");
120+
}
121+
return propertyDescriptor;
122+
}
123+
124+
private ConfigurationPropertyName getConfigurationPropertyName(String prefix, String fieldName) {
125+
if (StringUtils.isEmpty(prefix)) {
126+
return ConfigurationPropertyName.of(fieldName);
127+
}
128+
else {
129+
return ConfigurationPropertyName.of(prefix + "." + fieldName);
130+
}
131+
}
132+
133+
}

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,4 +70,8 @@ public void onFinish(ConfigurationPropertyName name, Bindable<?> target,
7070
this.parent.onFinish(name, target, context, result);
7171
}
7272

73+
@Override
74+
public Object onNull(ConfigurationPropertyName name, Bindable<?> target, BindContext context) {
75+
return this.parent.onNull(name, target, context);
76+
}
7377
}

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,4 +89,16 @@ default void onFinish(ConfigurationPropertyName name, Bindable<?> target,
8989
BindContext context, Object result) throws Exception {
9090
}
9191

92+
/**
93+
* Called when binding resolves to null.
94+
* @param name the name of the element being bound
95+
* @param target the item being bound
96+
* @param context the bind context
97+
* @return the actual result that should be used instead of the null value.
98+
*/
99+
default Object onNull(ConfigurationPropertyName name, Bindable<?> target,
100+
BindContext context) {
101+
return null;
102+
}
103+
92104
}

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,9 @@ private <T> T handleBindResult(ConfigurationPropertyName name, Bindable<T> targe
209209
result = handler.onSuccess(name, target, context, result);
210210
result = convert(result, target);
211211
}
212+
else {
213+
result = handler.onNull(name, target, context);
214+
}
212215
handler.onFinish(name, target, context, result);
213216
return convert(result, target);
214217
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/*
2+
* Copyright 2012-2017 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+
* http://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+
package org.springframework.boot.context.properties.bind.handler;
17+
18+
import java.util.Map;
19+
import java.util.Objects;
20+
import java.util.stream.Stream;
21+
22+
import org.springframework.boot.context.properties.bind.AbstractBindHandler;
23+
import org.springframework.boot.context.properties.bind.BindContext;
24+
import org.springframework.boot.context.properties.bind.BindHandler;
25+
import org.springframework.boot.context.properties.bind.Bindable;
26+
import org.springframework.boot.context.properties.source.ConfigurationProperty;
27+
import org.springframework.boot.context.properties.source.ConfigurationPropertyName;
28+
import org.springframework.boot.context.properties.source.ConfigurationPropertySource;
29+
30+
/**
31+
* {@link BindHandler} that falls back on a default value when a property could not be
32+
* bound (i.e. resolved to null).
33+
*
34+
* @author Tom Hombergs
35+
* @since 2.0.0
36+
*/
37+
public class FallbackBindHandler extends AbstractBindHandler {
38+
39+
private final Map<ConfigurationPropertyName, ConfigurationPropertyName> fallbacks;
40+
41+
public FallbackBindHandler(
42+
Map<ConfigurationPropertyName, ConfigurationPropertyName> fallbacks) {
43+
this.fallbacks = fallbacks;
44+
}
45+
46+
@Override
47+
public Object onNull(ConfigurationPropertyName name, Bindable<?> target, BindContext context) {
48+
ConfigurationPropertyName fallbackPropertyName = this.fallbacks.get(name);
49+
if (fallbackPropertyName != null) {
50+
ConfigurationProperty fallbackProperty = findProperty(fallbackPropertyName,
51+
context.streamSources());
52+
if (fallbackProperty != null && fallbackProperty.getValue() != null) {
53+
return fallbackProperty.getValue();
54+
}
55+
}
56+
return super.onNull(name, target, context);
57+
}
58+
59+
private ConfigurationProperty findProperty(ConfigurationPropertyName name,
60+
Stream<ConfigurationPropertySource> sources) {
61+
return sources.map((source) -> source.getConfigurationProperty(name))
62+
.filter(Objects::nonNull).findFirst().orElse(null);
63+
}
64+
}

0 commit comments

Comments
 (0)