From c6dae5764e412e61fefa3c3776469e971275c980 Mon Sep 17 00:00:00 2001 From: Madhura Bhave Date: Wed, 12 Jun 2019 17:04:12 -0700 Subject: [PATCH] Add bindOrCreate for constructor based binding Deprecate the existing `BindResult.orElseCreate` method in favor of `bindOrCreate` methods on the `Binder`. These new methods allow us to implement custom creation logic depending on the type of object being bound. Specifically, it allows constructor based binding to create new instances that respect the `@DefaultValue` annotations. Closes gh-17098 Co-authored-by: Phillip Webb --- ...PathBasedTemplateAvailabilityProvider.java | 3 +- ...ConfigurationPropertiesBeanDefinition.java | 2 +- .../ConfigurationPropertiesBinder.java | 53 +++++--- .../context/properties/bind/BeanBinder.java | 9 ++ .../context/properties/bind/BindHandler.java | 19 ++- .../context/properties/bind/BindResult.java | 2 + .../boot/context/properties/bind/Binder.java | 121 ++++++++++++++---- .../bind/ConstructorParametersBinder.java | 29 ++++- .../properties/bind/JavaBeanBinder.java | 5 + .../properties/bind/BindResultTests.java | 4 + .../context/properties/bind/BinderTests.java | 58 +++++++++ .../ConstructorParametersBinderTests.java | 91 ++++++++++++- .../properties/bind/JavaBeanBinderTests.java | 20 +++ .../KotlinConstructorParametersBinderTests.kt | 15 ++- 14 files changed, 372 insertions(+), 59 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/template/PathBasedTemplateAvailabilityProvider.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/template/PathBasedTemplateAvailabilityProvider.java index 6c4182888e..892af9a3e5 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/template/PathBasedTemplateAvailabilityProvider.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/template/PathBasedTemplateAvailabilityProvider.java @@ -53,8 +53,7 @@ public abstract class PathBasedTemplateAvailabilityProvider implements TemplateA ResourceLoader resourceLoader) { if (ClassUtils.isPresent(this.className, classLoader)) { Binder binder = Binder.get(environment); - TemplateAvailabilityProperties properties = binder.bind(this.propertyPrefix, this.propertiesClass) - .orElseCreate(this.propertiesClass); + TemplateAvailabilityProperties properties = binder.bindOrCreate(this.propertyPrefix, this.propertiesClass); return isTemplateAvailable(view, resourceLoader, properties); } return false; diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBeanDefinition.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBeanDefinition.java index 2509e2be6f..764d7dc343 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBeanDefinition.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBeanDefinition.java @@ -55,7 +55,7 @@ final class ConfigurationPropertiesBeanDefinition extends GenericBeanDefinition ConfigurationPropertiesBinder binder = beanFactory.getBean(ConfigurationPropertiesBinder.BEAN_NAME, ConfigurationPropertiesBinder.class); try { - return binder.bind(bindable).orElseCreate(type); + return binder.bindOrCreate(bindable); } catch (Exception ex) { throw new ConfigurationPropertiesBindException(beanName, type, annotation, ex); 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 bf37e420c6..d5b643aba4 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 @@ -86,11 +86,21 @@ class ConfigurationPropertiesBinder implements ApplicationContextAware { } public BindResult bind(Bindable target) { + ConfigurationProperties annotation = getAnnotation(target); + BindHandler bindHandler = getBindHandler(target, annotation); + return getBinder().bind(annotation.prefix(), target, bindHandler); + } + + public T bindOrCreate(Bindable target) { + ConfigurationProperties annotation = getAnnotation(target); + BindHandler bindHandler = getBindHandler(target, annotation); + return getBinder().bindOrCreate(annotation.prefix(), target, bindHandler); + } + + private ConfigurationProperties getAnnotation(Bindable target) { ConfigurationProperties annotation = target.getAnnotation(ConfigurationProperties.class); Assert.state(annotation != null, () -> "Missing @ConfigurationProperties on " + target); - List validators = getValidators(target); - BindHandler bindHandler = getBindHandler(annotation, validators); - return getBinder().bind(annotation.prefix(), target, bindHandler); + return annotation; } private Validator getConfigurationPropertiesValidator(ApplicationContext applicationContext, @@ -101,6 +111,25 @@ class ConfigurationPropertiesBinder implements ApplicationContextAware { return null; } + private BindHandler getBindHandler(Bindable target, ConfigurationProperties annotation) { + List validators = getValidators(target); + BindHandler handler = new IgnoreTopLevelConverterNotFoundBindHandler(); + if (annotation.ignoreInvalidFields()) { + handler = new IgnoreErrorsBindHandler(handler); + } + if (!annotation.ignoreUnknownFields()) { + UnboundElementsSourceFilter filter = new UnboundElementsSourceFilter(); + handler = new NoUnboundElementsBindHandler(handler, filter); + } + if (!validators.isEmpty()) { + handler = new ValidationBindHandler(handler, validators.toArray(new Validator[0])); + } + for (ConfigurationPropertiesBindHandlerAdvisor advisor : getBindHandlerAdvisors()) { + handler = advisor.apply(handler); + } + return handler; + } + private List getValidators(Bindable target) { List validators = new ArrayList<>(3); if (this.configurationPropertiesValidator != null) { @@ -122,24 +151,6 @@ class ConfigurationPropertiesBinder implements ApplicationContextAware { return this.jsr303Validator; } - private BindHandler getBindHandler(ConfigurationProperties annotation, List validators) { - BindHandler handler = new IgnoreTopLevelConverterNotFoundBindHandler(); - if (annotation.ignoreInvalidFields()) { - handler = new IgnoreErrorsBindHandler(handler); - } - if (!annotation.ignoreUnknownFields()) { - UnboundElementsSourceFilter filter = new UnboundElementsSourceFilter(); - handler = new NoUnboundElementsBindHandler(handler, filter); - } - if (!validators.isEmpty()) { - handler = new ValidationBindHandler(handler, validators.toArray(new Validator[0])); - } - for (ConfigurationPropertiesBindHandlerAdvisor advisor : getBindHandlerAdvisors()) { - handler = advisor.apply(handler); - } - return handler; - } - private List getBindHandlerAdvisors() { return this.applicationContext.getBeanProvider(ConfigurationPropertiesBindHandlerAdvisor.class).orderedStream() .collect(Collectors.toList()); diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BeanBinder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BeanBinder.java index b2f27970c6..3db0ed2233 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BeanBinder.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BeanBinder.java @@ -39,4 +39,13 @@ interface BeanBinder { */ T bind(ConfigurationPropertyName name, Bindable target, Context context, BeanPropertyBinder propertyBinder); + /** + * Return a new instance for the specified type. + * @param type the type used for creating a new instance + * @param context the bind context + * @param the source type + * @return the created instance + */ + T create(Class type, Context 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 1e21ca2ad6..0924a67112 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 @@ -60,10 +60,25 @@ public interface BindHandler { return result; } + /** + * Called when binding of an element ends with an unbound result and a newly created + * instance is about to be returned. Implementations may change the ultimately + * returned result or perform addition validation. + * @param name the name of the element being bound + * @param target the item being bound + * @param context the bind context + * @param result the newly created instance (never {@code null}) + * @return the actual result that should be used (must not be {@code null}) + * @since 2.2.2 + */ + default Object onCreate(ConfigurationPropertyName name, Bindable target, BindContext context, Object result) { + return result; + } + /** * Called when binding fails for any reason (including failures from - * {@link #onSuccess} calls). Implementations may choose to swallow exceptions and - * return an alternative result. + * {@link #onSuccess} or {@link #onCreate} calls). Implementations may choose to + * swallow exceptions and return an alternative result. * @param name the name of the element being bound * @param target the item being bound * @param context the bind context diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindResult.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindResult.java index e7446915d0..fb8d1d8480 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindResult.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindResult.java @@ -118,7 +118,9 @@ public final class BindResult { * value has been bound. * @param type the type to create if no value was bound * @return the value, if bound, otherwise a new instance of {@code type} + * @deprecated since 2.2.0 in favor of {@link Binder#bindOrCreate} */ + @Deprecated public T orElseCreate(Class type) { Assert.notNull(type, "Type must not be null"); return (this.value != null) ? this.value : BeanUtils.instantiateClass(type); 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 142b5e106a..4ca02da85b 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 @@ -17,7 +17,6 @@ package org.springframework.boot.context.properties.bind; import java.util.ArrayDeque; -import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; @@ -25,11 +24,9 @@ import java.util.Deque; import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.Set; import java.util.function.Consumer; import java.util.function.Supplier; -import java.util.stream.Stream; import org.springframework.beans.PropertyEditorRegistry; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; @@ -58,14 +55,7 @@ public class Binder { private static final Set> NON_BEAN_CLASSES = Collections .unmodifiableSet(new HashSet<>(Arrays.asList(Object.class, Class.class))); - private static final List BEAN_BINDERS; - - static { - List binders = new ArrayList<>(); - binders.add(new ConstructorParametersBinder()); - binders.add(new JavaBeanBinder()); - BEAN_BINDERS = Collections.unmodifiableList(binders); - } + private static final BeanBinder[] BEAN_BINDERS = { new ConstructorParametersBinder(), new JavaBeanBinder() }; private final Iterable sources; @@ -196,24 +186,89 @@ public class Binder { * @return the binding result (never {@code null}) */ public BindResult bind(ConfigurationPropertyName name, Bindable target, BindHandler handler) { + T bound = bind(name, target, handler, false); + return BindResult.of(bound); + } + + /** + * Bind the specified target {@link Class} using this binder's + * {@link ConfigurationPropertySource property sources} or create a new instance using + * the type of the {@link Bindable} if the result of the binding is {@code null}. + * @param name the configuration property name to bind + * @param target the target class + * @param the bound type + * @return the bound or created object + * @since 2.2.0 + * @see #bind(ConfigurationPropertyName, Bindable, BindHandler) + */ + public T bindOrCreate(String name, Class target) { + return bindOrCreate(name, Bindable.of(target)); + } + + /** + * Bind the specified target {@link Bindable} using this binder's + * {@link ConfigurationPropertySource property sources} or create a new instance using + * the type of the {@link Bindable} if the result of the binding is {@code null}. + * @param name the configuration property name to bind + * @param target the target bindable + * @param the bound type + * @return the bound or created object + * @since 2.2.0 + * @see #bindOrCreate(ConfigurationPropertyName, Bindable, BindHandler) + */ + public T bindOrCreate(String name, Bindable target) { + return bindOrCreate(ConfigurationPropertyName.of(name), target, null); + } + + /** + * Bind the specified target {@link Bindable} using this binder's + * {@link ConfigurationPropertySource property sources} or create a new instance using + * the type of the {@link Bindable} if the result of the binding is {@code null}. + * @param name the configuration property name to bind + * @param target the target bindable + * @param handler the bind handler + * @param the bound type + * @return the bound or created object + * @since 2.2.0 + * @see #bindOrCreate(ConfigurationPropertyName, Bindable, BindHandler) + */ + public T bindOrCreate(String name, Bindable target, BindHandler handler) { + return bindOrCreate(ConfigurationPropertyName.of(name), target, handler); + } + + /** + * Bind the specified target {@link Bindable} using this binder's + * {@link ConfigurationPropertySource property sources} or create a new instance using + * the type of the {@link Bindable} if the result of the binding is {@code null}. + * @param name the configuration property name to bind + * @param target the target bindable + * @param handler the bind handler (may be {@code null}) + * @param the bound or created type + * @since 2.2.0 + * @return the bound or created object + */ + public T bindOrCreate(ConfigurationPropertyName name, Bindable target, BindHandler handler) { + return bind(name, target, handler, true); + } + + private T bind(ConfigurationPropertyName name, Bindable target, BindHandler handler, boolean create) { Assert.notNull(name, "Name must not be null"); Assert.notNull(target, "Target must not be null"); handler = (handler != null) ? handler : BindHandler.DEFAULT; Context context = new Context(); - T bound = bind(name, target, handler, context, false); - return BindResult.of(bound); + return bind(name, target, handler, context, false, create); } - protected final T bind(ConfigurationPropertyName name, Bindable target, BindHandler handler, Context context, - boolean allowRecursiveBinding) { + private T bind(ConfigurationPropertyName name, Bindable target, BindHandler handler, Context context, + boolean allowRecursiveBinding, boolean create) { context.clearConfigurationProperty(); try { target = handler.onStart(name, target, context); if (target == null) { - return null; + return handleBindResult(name, target, handler, context, null, create); } Object bound = bindObject(name, target, handler, context, allowRecursiveBinding); - return handleBindResult(name, target, handler, context, bound); + return handleBindResult(name, target, handler, context, bound, create); } catch (Exception ex) { return handleBindError(name, target, handler, context, ex); @@ -221,15 +276,32 @@ public class Binder { } private T handleBindResult(ConfigurationPropertyName name, Bindable target, BindHandler handler, - Context context, Object result) throws Exception { + Context context, Object result, boolean create) throws Exception { if (result != null) { result = handler.onSuccess(name, target, context, result); result = context.getConverter().convert(result, target); } + if (result == null && create) { + result = createBean(target, context); + result = handler.onCreate(name, target, context, result); + result = context.getConverter().convert(result, target); + Assert.state(result != null, () -> "Unable to create instance for " + target.getType()); + } handler.onFinish(name, target, context, result); return context.getConverter().convert(result, target); } + private Object createBean(Bindable target, Context context) { + Class type = target.getType().resolve(); + for (BeanBinder beanBinder : BEAN_BINDERS) { + Object bean = beanBinder.create(type, context); + if (bean != null) { + return bean; + } + } + return null; + } + private T handleBindError(ConfigurationPropertyName name, Bindable target, BindHandler handler, Context context, Exception error) { try { @@ -288,7 +360,7 @@ public class Binder { Context context, AggregateBinder aggregateBinder) { AggregateElementBinder elementBinder = (itemName, itemTarget, source) -> { boolean allowRecursiveBinding = aggregateBinder.isAllowRecursiveBinding(source); - Supplier supplier = () -> bind(itemName, itemTarget, handler, context, allowRecursiveBinding); + Supplier supplier = () -> bind(itemName, itemTarget, handler, context, allowRecursiveBinding, false); return context.withSource(source, supplier); }; return context.withIncreasedDepth(() -> aggregateBinder.bind(name, target, elementBinder)); @@ -325,10 +397,15 @@ public class Binder { return null; } BeanPropertyBinder propertyBinder = (propertyName, propertyTarget) -> bind(name.append(propertyName), - propertyTarget, handler, context, false); + propertyTarget, handler, context, false, false); return context.withBean(type, () -> { - Stream boundBeans = BEAN_BINDERS.stream().map((b) -> b.bind(name, target, context, propertyBinder)); - return boundBeans.filter(Objects::nonNull).findFirst().orElse(null); + for (BeanBinder beanBinder : BEAN_BINDERS) { + Object bean = beanBinder.bind(name, target, context, propertyBinder); + if (bean != null) { + return bean; + } + } + return null; }); } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/ConstructorParametersBinder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/ConstructorParametersBinder.java index 8be6c6d2b0..504f677c3e 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/ConstructorParametersBinder.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/ConstructorParametersBinder.java @@ -57,20 +57,45 @@ class ConstructorParametersBinder implements BeanBinder { return null; } List bound = bind(propertyBinder, bean, context.getConverter()); - return (T) BeanUtils.instantiateClass(bean.getConstructor(), bound.toArray()); + return (bound != null) ? (T) BeanUtils.instantiateClass(bean.getConstructor(), bound.toArray()) : null; + } + + @Override + @SuppressWarnings("unchecked") + public T create(Class type, Binder.Context context) { + Bean bean = getBean(type); + if (bean == null) { + return null; + } + Collection parameters = bean.getParameters().values(); + List parameterValues = new ArrayList<>(parameters.size()); + for (ConstructorParameter parameter : parameters) { + Object boundParameter = getDefaultValue(parameter, context.getConverter()); + parameterValues.add(boundParameter); + } + return (T) BeanUtils.instantiateClass(bean.getConstructor(), parameterValues.toArray()); + } + + private Bean getBean(Class type) { + if (KOTLIN_PRESENT && KotlinDetector.isKotlinType(type)) { + return KotlinBeanProvider.get(type); + } + return SimpleBeanProvider.get(type); } private List bind(BeanPropertyBinder propertyBinder, Bean bean, BindConverter converter) { Collection parameters = bean.getParameters().values(); List boundParameters = new ArrayList<>(parameters.size()); + int unboundParameterCount = 0; for (ConstructorParameter parameter : parameters) { Object boundParameter = bind(parameter, propertyBinder); if (boundParameter == null) { + unboundParameterCount++; boundParameter = getDefaultValue(parameter, converter); } boundParameters.add(boundParameter); } - return boundParameters; + return (unboundParameterCount != parameters.size()) ? boundParameters : null; } private Object getDefaultValue(ConstructorParameter parameter, BindConverter converter) { diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/JavaBeanBinder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/JavaBeanBinder.java index d700a349e4..7565d80035 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/JavaBeanBinder.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/JavaBeanBinder.java @@ -55,6 +55,11 @@ class JavaBeanBinder implements BeanBinder { return (bound ? beanSupplier.get() : null); } + @Override + public T create(Class type, Context context) { + return BeanUtils.instantiateClass(type); + } + private boolean hasKnownBindableProperties(ConfigurationPropertyName name, Context context) { for (ConfigurationPropertySource source : context.getSources()) { if (source.containsDescendantOf(name) == ConfigurationPropertyState.PRESENT) { diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/BindResultTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/BindResultTests.java index c7c77358cd..b39d596878 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/BindResultTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/BindResultTests.java @@ -151,6 +151,8 @@ class BindResultTests { } @Test + @Deprecated + @SuppressWarnings("deprecation") void orElseCreateWhenTypeIsNullShouldThrowException() { BindResult result = BindResult.of("foo"); assertThatIllegalArgumentException().isThrownBy(() -> result.orElseCreate(null)) @@ -158,12 +160,14 @@ class BindResultTests { } @Test + @Deprecated void orElseCreateWhenHasValueShouldReturnValue() { BindResult result = BindResult.of(new ExampleBean("foo")); assertThat(result.orElseCreate(ExampleBean.class).getValue()).isEqualTo("foo"); } @Test + @Deprecated void orElseCreateWhenHasValueNoShouldReturnCreatedValue() { BindResult result = BindResult.of(null); assertThat(result.orElseCreate(ExampleBean.class).getValue()).isEqualTo("new"); diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/BinderTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/BinderTests.java index af0a01b37d..81c4c70791 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/BinderTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/BinderTests.java @@ -162,6 +162,15 @@ class BinderTests { ordered.verify(handler).onSuccess(eq(ConfigurationPropertyName.of("foo")), eq(target), any(), eq(1)); } + @Test + void bindOrCreateWhenNotBoundShouldTriggerOnCreate() { + BindHandler handler = mock(BindHandler.class, Answers.CALLS_REAL_METHODS); + Bindable target = Bindable.of(JavaBean.class); + this.binder.bindOrCreate("foo", target, handler); + InOrder ordered = inOrder(handler); + ordered.verify(handler).onCreate(eq(ConfigurationPropertyName.of("foo")), eq(target), any(), any()); + } + @Test void bindToJavaBeanShouldReturnPopulatedBean() { this.sources.add(new MockConfigurationPropertySource("foo.value", "bar")); @@ -280,6 +289,21 @@ class BinderTests { assertThat(result.getValue()).isEqualTo("hello"); } + @Test + void bindOrCreateWhenBindSuccessfulShouldReturnBoundValue() { + this.sources.add(new MockConfigurationPropertySource("foo.value", "bar")); + JavaBean result = this.binder.bindOrCreate("foo", Bindable.of(JavaBean.class)); + assertThat(result.getValue()).isEqualTo("bar"); + assertThat(result.getItems()).isEmpty(); + } + + @Test + void bindOrCreateWhenUnboundShouldReturnCreatedValue() { + JavaBean value = this.binder.bindOrCreate("foo", Bindable.of(JavaBean.class)); + assertThat(value).isNotNull(); + assertThat(value).isInstanceOf(JavaBean.class); + } + public static class JavaBean { private String value; @@ -300,6 +324,40 @@ class BinderTests { } + public static class NestedJavaBean { + + private DefaultValuesBean valuesBean = new DefaultValuesBean(); + + public DefaultValuesBean getValuesBean() { + return this.valuesBean; + } + + public void setValuesBean(DefaultValuesBean valuesBean) { + this.valuesBean = valuesBean; + } + + } + + public static class DefaultValuesBean { + + private String value = "hello"; + + private List items = Collections.emptyList(); + + public String getValue() { + return this.value; + } + + public void setValue(String value) { + this.value = value; + } + + public List getItems() { + return this.items; + } + + } + public enum ExampleEnum { FOO_BAR, BAR_BAZ, BAZ_BOO diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/ConstructorParametersBinderTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/ConstructorParametersBinderTests.java index 5d35092245..b2c7241d00 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/ConstructorParametersBinderTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/ConstructorParametersBinderTests.java @@ -18,6 +18,7 @@ package org.springframework.boot.context.properties.bind; import java.time.LocalDate; import java.util.ArrayList; import java.util.List; +import java.util.Objects; import org.junit.jupiter.api.Test; @@ -129,14 +130,20 @@ class ConstructorParametersBinderTests { } @Test - void bindToClassWithNoValueAndDefaultValueShouldUseDefault() { + void bindToClassWithNoValueAndDefaultValueShouldNotBind() { MockConfigurationPropertySource source = new MockConfigurationPropertySource(); source.put("foo.string-value", "foo"); this.sources.add(source); - ExampleDefaultValueBean bean = this.binder.bind("foo", Bindable.of(ExampleDefaultValueBean.class)).get(); - assertThat(bean.getIntValue()).isEqualTo(5); - assertThat(bean.getStringsList()).containsOnly("a", "b", "c"); - assertThat(bean.getCustomList()).containsOnly("x,y,z"); + assertThat(this.binder.bind("foo", Bindable.of(ExampleDefaultValueBean.class)).isBound()).isFalse(); + } + + @Test + void bindToClassWhenNoParameterBoundShouldReturnNull() { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + this.sources.add(source.nonIterable()); + BindResult result = this.binder.bind("foo", + Bindable.of(ExampleFailingConstructorBean.class)); + assertThat(result.isBound()).isFalse(); } @Test @@ -149,6 +156,47 @@ class ConstructorParametersBinderTests { assertThat(bean.getDate().toString()).isEqualTo("2014-04-01"); } + @Test + void bindWithAnnotationsAndDefaultValue() { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo.bar", "hello"); + this.sources.add(source); + ConverterAnnotatedExampleBean bean = this.binder.bind("foo", Bindable.of(ConverterAnnotatedExampleBean.class)) + .get(); + assertThat(bean.getDate().toString()).isEqualTo("2019-05-10"); + } + + @Test + void createShouldReturnCreatedValue() { + ExampleValueBean value = this.binder.bindOrCreate("foo", Bindable.of(ExampleValueBean.class)); + assertThat(value.getIntValue()).isEqualTo(0); + assertThat(value.getLongValue()).isEqualTo(0); + assertThat(value.isBooleanValue()).isEqualTo(false); + assertThat(value.getStringValue()).isNull(); + assertThat(value.getEnumValue()).isNull(); + } + + @Test + void createWithNestedShouldReturnCreatedValue() { + ExampleNestedBean value = this.binder.bindOrCreate("foo", Bindable.of(ExampleNestedBean.class)); + assertThat(value.getValueBean()).isEqualTo(null); + } + + @Test + void createWithDefaultValuesShouldReturnCreatedWithDefaultValues() { + ExampleDefaultValueBean value = this.binder.bindOrCreate("foo", Bindable.of(ExampleDefaultValueBean.class)); + assertThat(value.getIntValue()).isEqualTo(5); + assertThat(value.getStringsList()).containsOnly("a", "b", "c"); + assertThat(value.getCustomList()).containsOnly("x,y,z"); + } + + @Test + void createWithDefaultValuesAndAnnotationsShouldReturnCreatedWithDefaultValues() { + ConverterAnnotatedExampleBean bean = this.binder.bindOrCreate("foo", + Bindable.of(ConverterAnnotatedExampleBean.class)); + assertThat(bean.getDate().toString()).isEqualTo("2019-05-10"); + } + public static class ExampleValueBean { private final int intValue; @@ -277,18 +325,49 @@ class ConstructorParametersBinderTests { } + public static class ExampleFailingConstructorBean { + + private final String name; + + private final Object value; + + ExampleFailingConstructorBean(String name, String value) { + Objects.requireNonNull(name, "'name' must be not null."); + Objects.requireNonNull(value, "'value' must be not null."); + this.name = name; + this.value = value; + } + + public String getName() { + return this.name; + } + + public Object getValue() { + return this.value; + } + + } + public static class ConverterAnnotatedExampleBean { private final LocalDate date; - ConverterAnnotatedExampleBean(@DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate date) { + private final String bar; + + ConverterAnnotatedExampleBean( + @DefaultValue("2019-05-10") @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate date, String bar) { this.date = date; + this.bar = bar; } public LocalDate getDate() { return this.date; } + public String getBar() { + return this.bar; + } + } } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/JavaBeanBinderTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/JavaBeanBinderTests.java index 4f3cb84324..3067e87c31 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/JavaBeanBinderTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/JavaBeanBinderTests.java @@ -523,6 +523,12 @@ class JavaBeanBinderTests { property.setValue(() -> target, "some string"); } + @Test + void bindOrCreateWithNestedShouldReturnCreatedValue() { + NestedJavaBean result = this.binder.bindOrCreate("foo", Bindable.of(NestedJavaBean.class)); + assertThat(result.getNested().getBar()).isEqualTo(456); + } + public static class ExampleValueBean { private int intValue; @@ -991,4 +997,18 @@ class JavaBeanBinderTests { } + public static class NestedJavaBean { + + private ExampleDefaultsBean nested = new ExampleDefaultsBean(); + + public ExampleDefaultsBean getNested() { + return this.nested; + } + + public void setNested(ExampleDefaultsBean nested) { + this.nested = nested; + } + + } + } diff --git a/spring-boot-project/spring-boot/src/test/kotlin/org/springframework/boot/context/properties/bind/KotlinConstructorParametersBinderTests.kt b/spring-boot-project/spring-boot/src/test/kotlin/org/springframework/boot/context/properties/bind/KotlinConstructorParametersBinderTests.kt index 7db41f66de..1ae2667b0e 100644 --- a/spring-boot-project/spring-boot/src/test/kotlin/org/springframework/boot/context/properties/bind/KotlinConstructorParametersBinderTests.kt +++ b/spring-boot-project/spring-boot/src/test/kotlin/org/springframework/boot/context/properties/bind/KotlinConstructorParametersBinderTests.kt @@ -140,12 +140,21 @@ class KotlinConstructorParametersBinderTests { } @Test - fun `Bind to class with no value and default value should use default value`() { + fun `Bind to class with no value and default value should return unbound`() { val source = MockConfigurationPropertySource() source.put("foo.string-value", "foo") val binder = Binder(source) - val bean = binder.bind("foo", Bindable.of( - ExampleDefaultValueBean::class.java)).get() + assertThat(binder.bind("foo", Bindable.of( + ExampleDefaultValueBean::class.java)).isBound()).isFalse(); + } + + @Test + fun `Bind or create to class with no value and default value should return default value`() { + val source = MockConfigurationPropertySource() + source.put("foo.string-value", "foo") + val binder = Binder(source) + val bean = binder.bindOrCreate("foo", Bindable.of( + ExampleDefaultValueBean::class.java)) assertThat(bean.intValue).isEqualTo(5) assertThat(bean.stringsList).containsOnly("a", "b", "c") assertThat(bean.customList).containsOnly("x,y,z")