Skip to content

Commit 857cdf1

Browse files
committed
Added ConfigurationPropertyValue annotation and its processing.
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. Removed checking for descendant properties, because otherwise fallback properties must always be from the same namespace. Fixes spring-projects#7986
1 parent 5e9cfea commit 857cdf1

File tree

10 files changed

+528
-16
lines changed

10 files changed

+528
-16
lines changed

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -904,6 +904,9 @@ your application, as shown in the following example:
904904
905905
private InetAddress remoteAddress;
906906
907+
@ConfigurationPropertyValue(fallback="acmeCorp.companyName")
908+
private String companyName;
909+
907910
private final Security security = new Security();
908911
909912
public boolean isEnabled() { ... }
@@ -914,6 +917,10 @@ your application, as shown in the following example:
914917
915918
public void setRemoteAddress(InetAddress remoteAddress) { ... }
916919
920+
public String getCompanyName() { ... }
921+
922+
public void setCompanyName(String companyName) { ... }
923+
917924
public Security getSecurity() { ... }
918925
919926
public static class Security {
@@ -944,6 +951,8 @@ The preceding POJO defines the following properties:
944951

945952
* `acme.enabled`, `false` by default.
946953
* `acme.remote-address`, with a type that can be coerced from `String`.
954+
* `acme.companyName`, with a `String` value that is read from the property `acmeCorp.companyName` if
955+
`acme.companyName` has not been set
947956
* `acme.security.username`, with a nested "security" object whose name is determined by
948957
the name of the property. In particular, the return type is not used at all there and
949958
could have been `SecurityProperties`.

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

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,17 @@
1616

1717
package org.springframework.boot.context.properties;
1818

19+
import java.util.Map;
20+
1921
import org.springframework.boot.context.properties.bind.BindHandler;
2022
import org.springframework.boot.context.properties.bind.Bindable;
2123
import org.springframework.boot.context.properties.bind.Binder;
2224
import org.springframework.boot.context.properties.bind.PropertySourcesPlaceholdersResolver;
25+
import org.springframework.boot.context.properties.bind.handler.FallbackBindHandler;
2326
import org.springframework.boot.context.properties.bind.handler.IgnoreErrorsBindHandler;
2427
import org.springframework.boot.context.properties.bind.handler.NoUnboundElementsBindHandler;
2528
import org.springframework.boot.context.properties.bind.validation.ValidationBindHandler;
29+
import org.springframework.boot.context.properties.source.ConfigurationPropertyName;
2630
import org.springframework.boot.context.properties.source.ConfigurationPropertySource;
2731
import org.springframework.boot.context.properties.source.ConfigurationPropertySources;
2832
import org.springframework.boot.context.properties.source.UnboundElementsSourceFilter;
@@ -70,10 +74,12 @@ void bind(Object target, ConfigurationProperties annotation) {
7074
Binder binder = new Binder(this.configurationSources,
7175
new PropertySourcesPlaceholdersResolver(this.propertySources),
7276
this.conversionService);
73-
Validator validator = determineValidator(target);
74-
BindHandler handler = getBindHandler(annotation, validator);
75-
Bindable<?> bindable = Bindable.ofInstance(target);
7677
try {
78+
Map<ConfigurationPropertyName, ConfigurationPropertyName> fallbacks = getConfigurationPropertyFallbacks(
79+
target, annotation.prefix());
80+
Validator validator = determineValidator(target);
81+
BindHandler handler = getBindHandler(annotation, validator, fallbacks);
82+
Bindable<?> bindable = Bindable.ofInstance(target);
7783
binder.bind(annotation.prefix(), bindable, handler);
7884
}
7985
catch (Exception ex) {
@@ -97,7 +103,8 @@ private Validator determineValidator(Object bean) {
97103
}
98104

99105
private BindHandler getBindHandler(ConfigurationProperties annotation,
100-
Validator validator) {
106+
Validator validator,
107+
Map<ConfigurationPropertyName, ConfigurationPropertyName> fallbacks) {
101108
BindHandler handler = BindHandler.DEFAULT;
102109
if (annotation.ignoreInvalidFields()) {
103110
handler = new IgnoreErrorsBindHandler(handler);
@@ -109,6 +116,9 @@ private BindHandler getBindHandler(ConfigurationProperties annotation,
109116
if (validator != null) {
110117
handler = new ValidationBindHandler(handler, validator);
111118
}
119+
if (!fallbacks.isEmpty()) {
120+
handler = new FallbackBindHandler(fallbacks);
121+
}
112122
return handler;
113123
}
114124

@@ -123,6 +133,13 @@ private String getAnnotationDetails(ConfigurationProperties annotation) {
123133
return details.toString();
124134
}
125135

136+
private Map<ConfigurationPropertyName, ConfigurationPropertyName> getConfigurationPropertyFallbacks(
137+
Object bean, String prefix) {
138+
ConfigurationPropertyValueReader annotationReader = new ConfigurationPropertyValueReader(
139+
bean, prefix);
140+
return annotationReader.getPropertyFallbacks();
141+
}
142+
126143
/**
127144
* {@link Validator} implementation that wraps {@link Validator} instances and chains
128145
* their execution.
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 for a single
28+
* 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 the
44+
* 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,50 @@
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 org.springframework.util.ClassUtils;
20+
21+
/**
22+
* Exception thrown when a {@code @ConfigurationPropertyValue} annotation on a field or
23+
* method conflicts with another annotation.
24+
*
25+
* @author Tom Hombergs
26+
* @since 2.0.0
27+
*/
28+
public final class ConfigurationPropertyValueBindingException extends RuntimeException {
29+
30+
private ConfigurationPropertyValueBindingException(String message) {
31+
super(message);
32+
}
33+
34+
public static ConfigurationPropertyValueBindingException invalidUseOnMethod(
35+
Class<?> targetClass, String methodName, String reason) {
36+
return new ConfigurationPropertyValueBindingException(String.format(
37+
"Invalid use of annotation %s on method '%s' of class '%s': %s",
38+
ClassUtils.getShortName(ConfigurationPropertyValue.class), methodName,
39+
targetClass.getName(), reason));
40+
}
41+
42+
public static ConfigurationPropertyValueBindingException invalidUseOnField(
43+
Class<?> targetClass, String fieldName, String reason) {
44+
return new ConfigurationPropertyValueBindingException(String.format(
45+
"Invalid use of annotation %s on field '%s' of class '%s': %s",
46+
ClassUtils.getShortName(ConfigurationPropertyValue.class), fieldName,
47+
targetClass.getName(), reason));
48+
}
49+
50+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
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.ReflectionUtils;
30+
import org.springframework.util.StringUtils;
31+
32+
/**
33+
* Utility class that reads
34+
* {@link org.springframework.boot.context.properties.ConfigurationPropertyValue}
35+
* annotations from getters and fields of a bean and provides methods to access the
36+
* metadata contained in the annotations.
37+
*
38+
* @author Tom Hombergs
39+
* @since 2.0.0
40+
* @see org.springframework.boot.context.properties.ConfigurationPropertyValue
41+
*/
42+
public class ConfigurationPropertyValueReader {
43+
44+
private Map<ConfigurationPropertyName, ConfigurationPropertyValue> annotations = new HashMap<>();
45+
46+
public ConfigurationPropertyValueReader(Object bean, String prefix) {
47+
this.annotations = findAnnotations(bean, prefix);
48+
}
49+
50+
/**
51+
* Returns a map that maps configuration property names to their fallback properties.
52+
* If this map does not contain a value for a certain configuration property, it means
53+
* that there is no fallback specified for this property.
54+
*
55+
* @return a map of configuration property names to their fallback property names.
56+
*/
57+
public Map<ConfigurationPropertyName, ConfigurationPropertyName> getPropertyFallbacks() {
58+
return this.annotations.entrySet().stream()
59+
.filter(entry -> !StringUtils.isEmpty(entry.getValue().fallback()))
60+
.collect(Collectors.toMap(Map.Entry::getKey,
61+
entry -> ConfigurationPropertyName
62+
.of(entry.getValue().fallback())));
63+
}
64+
65+
/**
66+
* Walks through the methods and fields of the specified bean to extract
67+
* {@link ConfigurationPropertyValue} annotations. A field can be annotated either by
68+
* directly annotating the field or by annotating the corresponding getter or setter
69+
* method. This method will throw a {@link BeanCreationException} if multiple
70+
* annotations are found for the same field.
71+
*
72+
* @param bean the bean whose annotations to retrieve.
73+
* @param prefix the prefix of the superordinate {@link ConfigurationProperties}
74+
* annotation. May be null.
75+
* @return a map that maps configuration property names to the annotations that were
76+
* found for them.
77+
* @throws ConfigurationPropertyValueBindingException if multiple
78+
* {@link ConfigurationPropertyValue} annotations have been found for the same field.
79+
*/
80+
private Map<ConfigurationPropertyName, ConfigurationPropertyValue> findAnnotations(
81+
Object bean, String prefix) {
82+
Map<ConfigurationPropertyName, ConfigurationPropertyValue> fieldAnnotations = new HashMap<>();
83+
ReflectionUtils.doWithMethods(bean.getClass(), method -> {
84+
ConfigurationPropertyValue annotation = AnnotationUtils.findAnnotation(method,
85+
ConfigurationPropertyValue.class);
86+
if (annotation != null) {
87+
PropertyDescriptor propertyDescriptor = findPropertyDescriptorOrFail(
88+
method);
89+
ConfigurationPropertyName name = getConfigurationPropertyName(prefix,
90+
propertyDescriptor.getName());
91+
if (fieldAnnotations.containsKey(name)) {
92+
throw ConfigurationPropertyValueBindingException.invalidUseOnMethod(
93+
bean.getClass(), method.getName(),
94+
"You may either annotate a field, a getter or a setter but not two of these.");
95+
}
96+
fieldAnnotations.put(name, annotation);
97+
}
98+
});
99+
100+
ReflectionUtils.doWithFields(bean.getClass(), field -> {
101+
ConfigurationPropertyValue annotation = AnnotationUtils.findAnnotation(field,
102+
ConfigurationPropertyValue.class);
103+
if (annotation != null) {
104+
ConfigurationPropertyName name = getConfigurationPropertyName(prefix,
105+
field.getName());
106+
if (fieldAnnotations.containsKey(name)) {
107+
throw ConfigurationPropertyValueBindingException.invalidUseOnField(
108+
bean.getClass(), field.getName(),
109+
"You may either annotate a field, a getter or a setter but not two of these.");
110+
}
111+
fieldAnnotations.put(name, annotation);
112+
}
113+
});
114+
return fieldAnnotations;
115+
}
116+
117+
private PropertyDescriptor findPropertyDescriptorOrFail(Method method) {
118+
PropertyDescriptor propertyDescriptor = BeanUtils.findPropertyForMethod(method);
119+
if (propertyDescriptor == null) {
120+
throw ConfigurationPropertyValueBindingException.invalidUseOnMethod(
121+
method.getDeclaringClass(), method.getName(),
122+
"This annotation may only be used on getter and setter methods or fields.");
123+
}
124+
return propertyDescriptor;
125+
}
126+
127+
private ConfigurationPropertyName getConfigurationPropertyName(String prefix,
128+
String fieldName) {
129+
if (StringUtils.isEmpty(prefix)) {
130+
return ConfigurationPropertyName.of(fieldName);
131+
}
132+
else {
133+
return ConfigurationPropertyName.of(prefix + "." + fieldName);
134+
}
135+
}
136+
137+
}

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,4 +70,9 @@ 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,
75+
BindContext context) {
76+
return this.parent.onNull(name, target, context);
77+
}
7378
}

spring-boot-project/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
}

0 commit comments

Comments
 (0)