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")