From 857cdf1f89e6e2d74375cd3a523e021c1788db5c Mon Sep 17 00:00:00 2001 From: Tom Hombergs Date: Sat, 16 Dec 2017 22:52:06 +0100 Subject: [PATCH 1/2] 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 #7986 --- .../main/asciidoc/spring-boot-features.adoc | 9 + .../ConfigurationPropertiesBinder.java | 25 ++- .../ConfigurationPropertyValue.java | 53 ++++++ ...gurationPropertyValueBindingException.java | 50 ++++++ .../ConfigurationPropertyValueReader.java | 137 ++++++++++++++ .../properties/bind/AbstractBindHandler.java | 5 + .../context/properties/bind/BindHandler.java | 12 ++ .../boot/context/properties/bind/Binder.java | 17 +- .../bind/handler/FallbackBindHandler.java | 66 +++++++ .../ConfigurationPropertiesBinderTests.java | 170 ++++++++++++++++++ 10 files changed, 528 insertions(+), 16 deletions(-) create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertyValue.java create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertyValueBindingException.java create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertyValueReader.java create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/handler/FallbackBindHandler.java diff --git a/spring-boot-project/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc b/spring-boot-project/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc index 254e0bd7567a..d500053ea0d2 100644 --- a/spring-boot-project/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc +++ b/spring-boot-project/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc @@ -904,6 +904,9 @@ your application, as shown in the following example: private InetAddress remoteAddress; + @ConfigurationPropertyValue(fallback="acmeCorp.companyName") + private String companyName; + private final Security security = new Security(); public boolean isEnabled() { ... } @@ -914,6 +917,10 @@ your application, as shown in the following example: public void setRemoteAddress(InetAddress remoteAddress) { ... } + public String getCompanyName() { ... } + + public void setCompanyName(String companyName) { ... } + public Security getSecurity() { ... } public static class Security { @@ -944,6 +951,8 @@ The preceding POJO defines the following properties: * `acme.enabled`, `false` by default. * `acme.remote-address`, with a type that can be coerced from `String`. +* `acme.companyName`, with a `String` value that is read from the property `acmeCorp.companyName` if + `acme.companyName` has not been set * `acme.security.username`, with a nested "security" object whose name is determined by the name of the property. In particular, the return type is not used at all there and could have been `SecurityProperties`. diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBinder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBinder.java index 6ac4048c0db6..a2a6d8693631 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBinder.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBinder.java @@ -16,13 +16,17 @@ package org.springframework.boot.context.properties; +import java.util.Map; + import org.springframework.boot.context.properties.bind.BindHandler; import org.springframework.boot.context.properties.bind.Bindable; import org.springframework.boot.context.properties.bind.Binder; import org.springframework.boot.context.properties.bind.PropertySourcesPlaceholdersResolver; +import org.springframework.boot.context.properties.bind.handler.FallbackBindHandler; import org.springframework.boot.context.properties.bind.handler.IgnoreErrorsBindHandler; import org.springframework.boot.context.properties.bind.handler.NoUnboundElementsBindHandler; import org.springframework.boot.context.properties.bind.validation.ValidationBindHandler; +import org.springframework.boot.context.properties.source.ConfigurationPropertyName; import org.springframework.boot.context.properties.source.ConfigurationPropertySource; import org.springframework.boot.context.properties.source.ConfigurationPropertySources; import org.springframework.boot.context.properties.source.UnboundElementsSourceFilter; @@ -70,10 +74,12 @@ void bind(Object target, ConfigurationProperties annotation) { Binder binder = new Binder(this.configurationSources, new PropertySourcesPlaceholdersResolver(this.propertySources), this.conversionService); - Validator validator = determineValidator(target); - BindHandler handler = getBindHandler(annotation, validator); - Bindable bindable = Bindable.ofInstance(target); try { + Map fallbacks = getConfigurationPropertyFallbacks( + target, annotation.prefix()); + Validator validator = determineValidator(target); + BindHandler handler = getBindHandler(annotation, validator, fallbacks); + Bindable bindable = Bindable.ofInstance(target); binder.bind(annotation.prefix(), bindable, handler); } catch (Exception ex) { @@ -97,7 +103,8 @@ private Validator determineValidator(Object bean) { } private BindHandler getBindHandler(ConfigurationProperties annotation, - Validator validator) { + Validator validator, + Map fallbacks) { BindHandler handler = BindHandler.DEFAULT; if (annotation.ignoreInvalidFields()) { handler = new IgnoreErrorsBindHandler(handler); @@ -109,6 +116,9 @@ private BindHandler getBindHandler(ConfigurationProperties annotation, if (validator != null) { handler = new ValidationBindHandler(handler, validator); } + if (!fallbacks.isEmpty()) { + handler = new FallbackBindHandler(fallbacks); + } return handler; } @@ -123,6 +133,13 @@ private String getAnnotationDetails(ConfigurationProperties annotation) { return details.toString(); } + private Map getConfigurationPropertyFallbacks( + Object bean, String prefix) { + ConfigurationPropertyValueReader annotationReader = new ConfigurationPropertyValueReader( + bean, prefix); + return annotationReader.getPropertyFallbacks(); + } + /** * {@link Validator} implementation that wraps {@link Validator} instances and chains * their execution. diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertyValue.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertyValue.java new file mode 100644 index 000000000000..e4ce249b16c7 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertyValue.java @@ -0,0 +1,53 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.context.properties; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation to be used on accessor methods or fields when the class is annotated with + * {@link ConfigurationProperties}. It allows to specify additional metadata for a single + * property. + *

+ * Annotating a method that is not a getter or setter in the sense of the JavaBeans spec + * will cause an exception. + * + * @author Tom Hombergs + * @since 2.0.0 + * @see ConfigurationProperties + */ +@Target({ ElementType.FIELD, ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface ConfigurationPropertyValue { + + /** + * Name of the property whose value to use if the property with the name of the + * annotated field itself is not defined. + *

+ * The fallback property name has to be specified including potential prefixes defined + * in {@link ConfigurationProperties} annotations. + * + * @return the name of the fallback property + */ + String fallback() default ""; + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertyValueBindingException.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertyValueBindingException.java new file mode 100644 index 000000000000..9ae042048af1 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertyValueBindingException.java @@ -0,0 +1,50 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.context.properties; + +import org.springframework.util.ClassUtils; + +/** + * Exception thrown when a {@code @ConfigurationPropertyValue} annotation on a field or + * method conflicts with another annotation. + * + * @author Tom Hombergs + * @since 2.0.0 + */ +public final class ConfigurationPropertyValueBindingException extends RuntimeException { + + private ConfigurationPropertyValueBindingException(String message) { + super(message); + } + + public static ConfigurationPropertyValueBindingException invalidUseOnMethod( + Class targetClass, String methodName, String reason) { + return new ConfigurationPropertyValueBindingException(String.format( + "Invalid use of annotation %s on method '%s' of class '%s': %s", + ClassUtils.getShortName(ConfigurationPropertyValue.class), methodName, + targetClass.getName(), reason)); + } + + public static ConfigurationPropertyValueBindingException invalidUseOnField( + Class targetClass, String fieldName, String reason) { + return new ConfigurationPropertyValueBindingException(String.format( + "Invalid use of annotation %s on field '%s' of class '%s': %s", + ClassUtils.getShortName(ConfigurationPropertyValue.class), fieldName, + targetClass.getName(), reason)); + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertyValueReader.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertyValueReader.java new file mode 100644 index 000000000000..30c9cdf5630a --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertyValueReader.java @@ -0,0 +1,137 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.context.properties; + +import java.beans.PropertyDescriptor; +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Collectors; + +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.boot.context.properties.source.ConfigurationPropertyName; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.util.ReflectionUtils; +import org.springframework.util.StringUtils; + +/** + * Utility class that reads + * {@link org.springframework.boot.context.properties.ConfigurationPropertyValue} + * annotations from getters and fields of a bean and provides methods to access the + * metadata contained in the annotations. + * + * @author Tom Hombergs + * @since 2.0.0 + * @see org.springframework.boot.context.properties.ConfigurationPropertyValue + */ +public class ConfigurationPropertyValueReader { + + private Map annotations = new HashMap<>(); + + public ConfigurationPropertyValueReader(Object bean, String prefix) { + this.annotations = findAnnotations(bean, prefix); + } + + /** + * Returns a map that maps configuration property names to their fallback properties. + * If this map does not contain a value for a certain configuration property, it means + * that there is no fallback specified for this property. + * + * @return a map of configuration property names to their fallback property names. + */ + public Map getPropertyFallbacks() { + return this.annotations.entrySet().stream() + .filter(entry -> !StringUtils.isEmpty(entry.getValue().fallback())) + .collect(Collectors.toMap(Map.Entry::getKey, + entry -> ConfigurationPropertyName + .of(entry.getValue().fallback()))); + } + + /** + * Walks through the methods and fields of the specified bean to extract + * {@link ConfigurationPropertyValue} annotations. A field can be annotated either by + * directly annotating the field or by annotating the corresponding getter or setter + * method. This method will throw a {@link BeanCreationException} if multiple + * annotations are found for the same field. + * + * @param bean the bean whose annotations to retrieve. + * @param prefix the prefix of the superordinate {@link ConfigurationProperties} + * annotation. May be null. + * @return a map that maps configuration property names to the annotations that were + * found for them. + * @throws ConfigurationPropertyValueBindingException if multiple + * {@link ConfigurationPropertyValue} annotations have been found for the same field. + */ + private Map findAnnotations( + Object bean, String prefix) { + Map fieldAnnotations = new HashMap<>(); + ReflectionUtils.doWithMethods(bean.getClass(), method -> { + ConfigurationPropertyValue annotation = AnnotationUtils.findAnnotation(method, + ConfigurationPropertyValue.class); + if (annotation != null) { + PropertyDescriptor propertyDescriptor = findPropertyDescriptorOrFail( + method); + ConfigurationPropertyName name = getConfigurationPropertyName(prefix, + propertyDescriptor.getName()); + if (fieldAnnotations.containsKey(name)) { + throw ConfigurationPropertyValueBindingException.invalidUseOnMethod( + bean.getClass(), method.getName(), + "You may either annotate a field, a getter or a setter but not two of these."); + } + fieldAnnotations.put(name, annotation); + } + }); + + ReflectionUtils.doWithFields(bean.getClass(), field -> { + ConfigurationPropertyValue annotation = AnnotationUtils.findAnnotation(field, + ConfigurationPropertyValue.class); + if (annotation != null) { + ConfigurationPropertyName name = getConfigurationPropertyName(prefix, + field.getName()); + if (fieldAnnotations.containsKey(name)) { + throw ConfigurationPropertyValueBindingException.invalidUseOnField( + bean.getClass(), field.getName(), + "You may either annotate a field, a getter or a setter but not two of these."); + } + fieldAnnotations.put(name, annotation); + } + }); + return fieldAnnotations; + } + + private PropertyDescriptor findPropertyDescriptorOrFail(Method method) { + PropertyDescriptor propertyDescriptor = BeanUtils.findPropertyForMethod(method); + if (propertyDescriptor == null) { + throw ConfigurationPropertyValueBindingException.invalidUseOnMethod( + method.getDeclaringClass(), method.getName(), + "This annotation may only be used on getter and setter methods or fields."); + } + return propertyDescriptor; + } + + private ConfigurationPropertyName getConfigurationPropertyName(String prefix, + String fieldName) { + if (StringUtils.isEmpty(prefix)) { + return ConfigurationPropertyName.of(fieldName); + } + else { + return ConfigurationPropertyName.of(prefix + "." + fieldName); + } + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/AbstractBindHandler.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/AbstractBindHandler.java index e10177d816d2..e841631f4114 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/AbstractBindHandler.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/AbstractBindHandler.java @@ -70,4 +70,9 @@ public void onFinish(ConfigurationPropertyName name, Bindable target, this.parent.onFinish(name, target, context, result); } + @Override + public Object onNull(ConfigurationPropertyName name, Bindable target, + BindContext context) { + return this.parent.onNull(name, target, context); + } } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindHandler.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindHandler.java index 49983f30e6de..27db2a74f88e 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindHandler.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindHandler.java @@ -89,4 +89,16 @@ default void onFinish(ConfigurationPropertyName name, Bindable target, BindContext context, Object result) throws Exception { } + /** + * Called when binding resolves to null. + * @param name the name of the element being bound + * @param target the item being bound + * @param context the bind context + * @return the actual result that should be used instead of the null value. + */ + default Object onNull(ConfigurationPropertyName name, Bindable target, + BindContext context) { + return null; + } + } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/Binder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/Binder.java index e6ece53ae0c2..43311d10998b 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/Binder.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/Binder.java @@ -210,6 +210,9 @@ private T handleBindResult(ConfigurationPropertyName name, Bindable targe result = handler.onSuccess(name, target, context, result); result = convert(result, target); } + else { + result = handler.onNull(name, target, context); + } handler.onFinish(name, target, context, result); return convert(result, target); } @@ -235,11 +238,8 @@ private T convert(Object value, Bindable target) { private Object bindObject(ConfigurationPropertyName name, Bindable target, BindHandler handler, Context context, boolean allowRecursiveBinding) - throws Exception { + throws Exception { ConfigurationProperty property = findProperty(name, context); - if (property == null && containsNoDescendantOf(context.streamSources(), name)) { - return null; - } AggregateBinder aggregateBinder = getAggregateBinder(target, context); if (aggregateBinder != null) { return bindAggregate(name, target, handler, context, aggregateBinder); @@ -306,8 +306,7 @@ private Object bindProperty(ConfigurationPropertyName name, Bindable targ private Object bindBean(ConfigurationPropertyName name, Bindable target, BindHandler handler, Context context, boolean allowRecursiveBinding) { - if (containsNoDescendantOf(context.streamSources(), name) - || isUnbindableBean(name, target, context)) { + if (isUnbindableBean(name, target, context)) { return null; } BeanPropertyBinder propertyBinder = (propertyName, propertyTarget) -> bind( @@ -338,12 +337,6 @@ private boolean isUnbindableBean(ConfigurationPropertyName name, Bindable tar return packageName.startsWith("java."); } - private boolean containsNoDescendantOf(Stream sources, - ConfigurationPropertyName name) { - return sources.allMatch( - (s) -> s.containsDescendantOf(name) == ConfigurationPropertyState.ABSENT); - } - /** * Create a new {@link Binder} instance from the specified environment. * @param environment the environment source (must have attached diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/handler/FallbackBindHandler.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/handler/FallbackBindHandler.java new file mode 100644 index 000000000000..e117ab094f15 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/handler/FallbackBindHandler.java @@ -0,0 +1,66 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.context.properties.bind.handler; + +import java.util.Map; +import java.util.Objects; +import java.util.stream.Stream; + +import org.springframework.boot.context.properties.bind.AbstractBindHandler; +import org.springframework.boot.context.properties.bind.BindContext; +import org.springframework.boot.context.properties.bind.BindHandler; +import org.springframework.boot.context.properties.bind.Bindable; +import org.springframework.boot.context.properties.source.ConfigurationProperty; +import org.springframework.boot.context.properties.source.ConfigurationPropertyName; +import org.springframework.boot.context.properties.source.ConfigurationPropertySource; + +/** + * {@link BindHandler} that falls back on a default value when a property could not be + * bound (i.e. resolved to null). + * + * @author Tom Hombergs + * @since 2.0.0 + */ +public class FallbackBindHandler extends AbstractBindHandler { + + private final Map fallbacks; + + public FallbackBindHandler( + Map fallbacks) { + this.fallbacks = fallbacks; + } + + @Override + public Object onNull(ConfigurationPropertyName name, Bindable target, + BindContext context) { + ConfigurationPropertyName fallbackPropertyName = this.fallbacks.get(name); + if (fallbackPropertyName != null) { + ConfigurationProperty fallbackProperty = findProperty(fallbackPropertyName, + context.streamSources()); + if (fallbackProperty != null && fallbackProperty.getValue() != null) { + return fallbackProperty.getValue(); + } + } + return super.onNull(name, target, context); + } + + private ConfigurationProperty findProperty(ConfigurationPropertyName name, + Stream sources) { + return sources.map((source) -> source.getConfigurationProperty(name)) + .filter(Objects::nonNull).findFirst().orElse(null); + } +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesBinderTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesBinderTests.java index a2f770facca7..1a71e000ff57 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesBinderTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesBinderTests.java @@ -254,6 +254,85 @@ public void validationWithCustomValidatorNotSupported() { verify(validator, times(0)).validate(eq(target), any(Errors.class)); } + @Test + public void propertyWithFallback() throws Exception { + TestPropertySourceUtils.addInlinedPropertiesToEnvironment(this.environment, + "test.bar=fallbackValue"); + ConfigurationPropertiesBinder binder = new ConfigurationPropertiesBinder( + this.environment.getPropertySources(), null, null); + PropertyWithFallback target = new PropertyWithFallback(); + bind(binder, target); + assertThat(target.getFoo()).isEqualTo("fallbackValue"); + assertThat(target.getBaz()).isEqualTo("fallbackValue"); + } + + @Test + public void propertyWithFallbackFromDifferentNamespace() throws Exception { + TestPropertySourceUtils.addInlinedPropertiesToEnvironment(this.environment, + "test.bar=fallbackValue"); + ConfigurationPropertiesBinder binder = new ConfigurationPropertiesBinder( + this.environment.getPropertySources(), null, null); + PropertyWithFallbackFromDifferentNamespace target = new PropertyWithFallbackFromDifferentNamespace(); + bind(binder, target); + assertThat(target.getFoo()).isEqualTo("fallbackValue"); + } + + @Test + public void propertyWithNonExistingFallback() throws Exception { + TestPropertySourceUtils.addInlinedPropertiesToEnvironment(this.environment, + "foo=bar"); + ConfigurationPropertiesBinder binder = new ConfigurationPropertiesBinder( + this.environment.getPropertySources(), null, null); + PropertyWithFallbackFromDifferentNamespace target = new PropertyWithFallbackFromDifferentNamespace(); + bind(binder, target); + assertThat(target.getFoo()).isNull(); + } + + @Test + public void invalidMethodAnnotated() throws Exception { + ConfigurationPropertiesBinder binder = new ConfigurationPropertiesBinder( + this.environment.getPropertySources(), null, null); + InvalidMethodAnnotated target = new InvalidMethodAnnotated(); + try { + bind(binder, target); + fail("Expected exception"); + } + catch (ConfigurationPropertiesBindingException ex) { + assertThat(ex.getRootCause()) + .isInstanceOf(ConfigurationPropertyValueBindingException.class); + } + } + + @Test + public void fieldAndAccessorAnnotated() throws Exception { + ConfigurationPropertiesBinder binder = new ConfigurationPropertiesBinder( + this.environment.getPropertySources(), null, null); + FieldAndGetterAnnotated target = new FieldAndGetterAnnotated(); + try { + bind(binder, target); + fail("Expected exception"); + } + catch (ConfigurationPropertiesBindingException ex) { + assertThat(ex.getRootCause()) + .isInstanceOf(ConfigurationPropertyValueBindingException.class); + } + } + + @Test + public void getterAndSetterAnnotated() throws Exception { + ConfigurationPropertiesBinder binder = new ConfigurationPropertiesBinder( + this.environment.getPropertySources(), null, null); + GetterAndSetterAnnotated target = new GetterAndSetterAnnotated(); + try { + bind(binder, target); + fail("Expected exception"); + } + catch (ConfigurationPropertiesBindingException ex) { + assertThat(ex.getRootCause()) + .isInstanceOf(ConfigurationPropertyValueBindingException.class); + } + } + private ValidationErrors bindWithValidationErrors( ConfigurationPropertiesBinder binder, Object target) { try { @@ -479,4 +558,95 @@ public void validate(Object o, Errors errors) { } + @ConfigurationProperties(prefix = "test") + public static class PropertyWithFallback { + + @ConfigurationPropertyValue(fallback = "test.bar") + private String foo; + + private String bar; + + private String baz; + + public String getFoo() { + return this.foo; + } + + public void setFoo(String foo) { + this.foo = foo; + } + + public String getBar() { + return this.bar; + } + + public void setBar(String bar) { + this.bar = bar; + } + + @ConfigurationPropertyValue(fallback = "test.bar") + public String getBaz() { + return this.baz; + } + + public void setBaz(String baz) { + this.baz = baz; + } + } + + @ConfigurationProperties(prefix = "different-namespace") + public static class PropertyWithFallbackFromDifferentNamespace { + + @ConfigurationPropertyValue(fallback = "test.bar") + private String foo; + + public String getFoo() { + return this.foo; + } + + public void setFoo(String value) { + this.foo = value; + } + } + + @ConfigurationProperties + public static class InvalidMethodAnnotated { + + @ConfigurationPropertyValue + public void doStuff() { + } + } + + @ConfigurationProperties + public static class FieldAndGetterAnnotated { + + @ConfigurationPropertyValue + private String value; + + @ConfigurationPropertyValue + public String getValue() { + return this.value; + } + + public void setValue(String value) { + this.value = value; + } + } + + @ConfigurationProperties + public static class GetterAndSetterAnnotated { + + private String value; + + @ConfigurationPropertyValue + public String getValue() { + return this.value; + } + + @ConfigurationPropertyValue + public void setValue(String value) { + this.value = value; + } + } + } From 32bdd3135d171722e67302f6399e61997a95dc5f Mon Sep 17 00:00:00 2001 From: Tom Hombergs Date: Sun, 17 Dec 2017 00:19:52 +0100 Subject: [PATCH 2/2] Re-introduced descendant check because of failing test in class CollectionBinderTests. --- .../boot/context/properties/bind/Binder.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/Binder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/Binder.java index 43311d10998b..0e1342b08a19 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/Binder.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/Binder.java @@ -32,6 +32,7 @@ import java.util.stream.StreamSupport; import org.springframework.boot.context.properties.bind.convert.BinderConversionService; +import org.springframework.boot.context.properties.bind.handler.FallbackBindHandler; import org.springframework.boot.context.properties.source.ConfigurationProperty; import org.springframework.boot.context.properties.source.ConfigurationPropertyName; import org.springframework.boot.context.properties.source.ConfigurationPropertySource; @@ -306,6 +307,12 @@ private Object bindProperty(ConfigurationPropertyName name, Bindable targ private Object bindBean(ConfigurationPropertyName name, Bindable target, BindHandler handler, Context context, boolean allowRecursiveBinding) { + // if we want to handle fallback properties, we need to be able to look into other + // namespaces, so we don't check descendants + if (!(handler instanceof FallbackBindHandler) + && containsNoDescendantOf(context.streamSources(), name)) { + return null; + } if (isUnbindableBean(name, target, context)) { return null; } @@ -337,6 +344,12 @@ private boolean isUnbindableBean(ConfigurationPropertyName name, Bindable tar return packageName.startsWith("java."); } + private boolean containsNoDescendantOf(Stream sources, + ConfigurationPropertyName name) { + return sources.allMatch( + (s) -> s.containsDescendantOf(name) == ConfigurationPropertyState.ABSENT); + } + /** * Create a new {@link Binder} instance from the specified environment. * @param environment the environment source (must have attached