Commit 2cac2646 authored by Madhura Bhave's avatar Madhura Bhave

Merge branch '2.2.x'

Closes gh-21049
parents 49d20aea af6d5387
...@@ -1020,6 +1020,37 @@ This means that the binder will expect to find a constructor with the parameters ...@@ -1020,6 +1020,37 @@ This means that the binder will expect to find a constructor with the parameters
Nested members of a `@ConstructorBinding` class (such as `Security` in the example above) will also be bound via their constructor. Nested members of a `@ConstructorBinding` class (such as `Security` in the example above) will also be bound via their constructor.
Default values can be specified using `@DefaultValue` and the same conversion service will be applied to coerce the `String` value to the target type of a missing property. Default values can be specified using `@DefaultValue` and the same conversion service will be applied to coerce the `String` value to the target type of a missing property.
By default, if no properties are bound to `Security`, the `AcmeProperties` instance will contain a `null` value for `security`.
If you wish you return a non-null instance of `Security` even when no properties are bound to it, you can use an empty `@DefaultValue` annotation to do so:
[source,java,indent=0]
----
package com.example;
import java.net.InetAddress;
import java.util.List;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.ConstructorBinding;
import org.springframework.boot.context.properties.bind.DefaultValue;
@ConstructorBinding
@ConfigurationProperties("acme")
public class AcmeProperties {
private final boolean enabled;
private final InetAddress remoteAddress;
private final Security security;
public AcmeProperties(boolean enabled, InetAddress remoteAddress, @DefaultValue Security security) {
this.enabled = enabled;
this.remoteAddress = remoteAddress;
this.security = security;
}
}
----
NOTE: To use constructor binding the class must be enabled using `@EnableConfigurationProperties` or configuration property scanning. NOTE: To use constructor binding the class must be enabled using `@EnableConfigurationProperties` or configuration property scanning.
You cannot use constructor binding with beans that are created by the regular Spring mechanisms (e.g. `@Component` beans, beans created via `@Bean` methods or beans loaded using `@Import`) You cannot use constructor binding with beans that are created by the regular Spring mechanisms (e.g. `@Component` beans, beans created via `@Bean` methods or beans loaded using `@Import`)
......
...@@ -23,7 +23,8 @@ import java.lang.annotation.Target; ...@@ -23,7 +23,8 @@ import java.lang.annotation.Target;
/** /**
* Annotation that can be used to specify the default value when binding to an immutable * Annotation that can be used to specify the default value when binding to an immutable
* property. * property. This annotation can also be used with nested properties to indicate that a
* value should always be bound (rather than binding {@code null}).
* *
* @author Madhura Bhave * @author Madhura Bhave
* @since 2.2.0 * @since 2.2.0
...@@ -38,6 +39,6 @@ public @interface DefaultValue { ...@@ -38,6 +39,6 @@ public @interface DefaultValue {
* array-based properties. * array-based properties.
* @return the default value of the property. * @return the default value of the property.
*/ */
String[] value(); String[] value() default {};
} }
...@@ -20,8 +20,10 @@ import java.lang.reflect.Constructor; ...@@ -20,8 +20,10 @@ import java.lang.reflect.Constructor;
import java.lang.reflect.Modifier; import java.lang.reflect.Modifier;
import java.lang.reflect.Parameter; import java.lang.reflect.Parameter;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map;
import kotlin.reflect.KFunction; import kotlin.reflect.KFunction;
import kotlin.reflect.KParameter; import kotlin.reflect.KParameter;
...@@ -65,7 +67,7 @@ class ValueObjectBinder implements DataObjectBinder { ...@@ -65,7 +67,7 @@ class ValueObjectBinder implements DataObjectBinder {
for (ConstructorParameter parameter : parameters) { for (ConstructorParameter parameter : parameters) {
Object arg = parameter.bind(propertyBinder); Object arg = parameter.bind(propertyBinder);
bound = bound || arg != null; bound = bound || arg != null;
arg = (arg != null) ? arg : parameter.getDefaultValue(context.getConverter()); arg = (arg != null) ? arg : getDefaultValue(context, parameter);
args.add(arg); args.add(arg);
} }
context.clearConfigurationProperty(); context.clearConfigurationProperty();
...@@ -82,11 +84,49 @@ class ValueObjectBinder implements DataObjectBinder { ...@@ -82,11 +84,49 @@ class ValueObjectBinder implements DataObjectBinder {
List<ConstructorParameter> parameters = valueObject.getConstructorParameters(); List<ConstructorParameter> parameters = valueObject.getConstructorParameters();
List<Object> args = new ArrayList<>(parameters.size()); List<Object> args = new ArrayList<>(parameters.size());
for (ConstructorParameter parameter : parameters) { for (ConstructorParameter parameter : parameters) {
args.add(parameter.getDefaultValue(context.getConverter())); args.add(getDefaultValue(context, parameter));
} }
return valueObject.instantiate(args); return valueObject.instantiate(args);
} }
private <T> T getDefaultValue(Binder.Context context, ConstructorParameter parameter) {
ResolvableType type = parameter.getType();
Annotation[] annotations = parameter.getAnnotations();
for (Annotation annotation : annotations) {
if (annotation instanceof DefaultValue) {
DefaultValue defaultValue = (DefaultValue) annotation;
if (defaultValue.value().length == 0) {
return getNewInstanceIfPossible(context, type);
}
return context.getConverter().convert(defaultValue.value(), type, annotations);
}
}
return null;
}
@SuppressWarnings("unchecked")
private <T> T getNewInstanceIfPossible(Binder.Context context, ResolvableType type) {
Class<T> resolved = (Class<T>) type.resolve();
Assert.state(resolved == null || isEmptyDefaultValueAllowed(resolved),
() -> "Parameter of type " + type + " must have a non-empty default value.");
T instance = create(Bindable.of(type), context);
if (instance != null) {
return instance;
}
return (resolved != null) ? BeanUtils.instantiateClass(resolved) : null;
}
private boolean isEmptyDefaultValueAllowed(Class<?> type) {
if (type.isPrimitive() || type.isEnum() || isAggregate(type) || type.getName().startsWith("java.lang")) {
return false;
}
return true;
}
private boolean isAggregate(Class<?> type) {
return type.isArray() || Map.class.isAssignableFrom(type) || Collection.class.isAssignableFrom(type);
}
/** /**
* The value object being bound. * The value object being bound.
* *
...@@ -228,19 +268,18 @@ class ValueObjectBinder implements DataObjectBinder { ...@@ -228,19 +268,18 @@ class ValueObjectBinder implements DataObjectBinder {
this.annotations = annotations; this.annotations = annotations;
} }
Object getDefaultValue(BindConverter converter) {
for (Annotation annotation : this.annotations) {
if (annotation instanceof DefaultValue) {
return converter.convert(((DefaultValue) annotation).value(), this.type, this.annotations);
}
}
return null;
}
Object bind(DataObjectPropertyBinder propertyBinder) { Object bind(DataObjectPropertyBinder propertyBinder) {
return propertyBinder.bindProperty(this.name, Bindable.of(this.type).withAnnotations(this.annotations)); return propertyBinder.bindProperty(this.name, Bindable.of(this.type).withAnnotations(this.annotations));
} }
Annotation[] getAnnotations() {
return this.annotations;
}
ResolvableType getType() {
return this.type;
}
} }
} }
...@@ -248,6 +248,65 @@ class ValueObjectBinderTests { ...@@ -248,6 +248,65 @@ class ValueObjectBinderTests {
assertThat(bean.getValue().get("bar")).isEqualTo("baz"); assertThat(bean.getValue().get("bar")).isEqualTo("baz");
} }
@Test
void bindWhenParametersWithDefaultValueShouldReturnNonNullValues() {
NestedConstructorBeanWithDefaultValue bound = this.binder.bindOrCreate("foo",
Bindable.of(NestedConstructorBeanWithDefaultValue.class));
assertThat(bound.getNestedImmutable().getFoo()).isEqualTo("hello");
assertThat(bound.getNestedJavaBean()).isNotNull();
}
@Test
void bindWhenJavaLangParameterWithEmptyDefaultValueShouldThrowException() {
assertThatExceptionOfType(BindException.class)
.isThrownBy(() -> this.binder.bindOrCreate("foo",
Bindable.of(NestedConstructorBeanWithEmptyDefaultValueForJavaLangTypes.class)))
.withStackTraceContaining("Parameter of type java.lang.String must have a non-empty default value.");
}
@Test
void bindWhenCollectionParameterWithEmptyDefaultValueShouldThrowException() {
assertThatExceptionOfType(BindException.class)
.isThrownBy(() -> this.binder.bindOrCreate("foo",
Bindable.of(NestedConstructorBeanWithEmptyDefaultValueForCollectionTypes.class)))
.withStackTraceContaining(
"Parameter of type java.util.List<java.lang.String> must have a non-empty default value.");
}
@Test
void bindWhenMapParametersWithEmptyDefaultValueShouldThrowException() {
assertThatExceptionOfType(BindException.class)
.isThrownBy(() -> this.binder.bindOrCreate("foo",
Bindable.of(NestedConstructorBeanWithEmptyDefaultValueForMapTypes.class)))
.withStackTraceContaining(
"Parameter of type java.util.Map<java.lang.String, java.lang.String> must have a non-empty default value.");
}
@Test
void bindWhenArrayParameterWithEmptyDefaultValueShouldThrowException() {
assertThatExceptionOfType(BindException.class)
.isThrownBy(() -> this.binder.bindOrCreate("foo",
Bindable.of(NestedConstructorBeanWithEmptyDefaultValueForArrayTypes.class)))
.withStackTraceContaining("Parameter of type java.lang.String[] must have a non-empty default value.");
}
@Test
void bindWhenEnumParameterWithEmptyDefaultValueShouldThrowException() {
assertThatExceptionOfType(BindException.class)
.isThrownBy(() -> this.binder.bindOrCreate("foo",
Bindable.of(NestedConstructorBeanWithEmptyDefaultValueForEnumTypes.class)))
.withStackTraceContaining(
"Parameter of type org.springframework.boot.context.properties.bind.ValueObjectBinderTests$NestedConstructorBeanWithEmptyDefaultValueForEnumTypes$Foo must have a non-empty default value.");
}
@Test
void bindWhenPrimitiveParameterWithEmptyDefaultValueShouldThrowException() {
assertThatExceptionOfType(BindException.class)
.isThrownBy(() -> this.binder.bindOrCreate("foo",
Bindable.of(NestedConstructorBeanWithEmptyDefaultValueForPrimitiveTypes.class)))
.withStackTraceContaining("Parameter of type int must have a non-empty default value.");
}
private void noConfigurationProperty(BindException ex) { private void noConfigurationProperty(BindException ex) {
assertThat(ex.getProperty()).isNull(); assertThat(ex.getProperty()).isNull();
} }
...@@ -481,4 +540,148 @@ class ValueObjectBinderTests { ...@@ -481,4 +540,148 @@ class ValueObjectBinderTests {
} }
static class NestedConstructorBeanWithDefaultValue {
private final NestedImmutable nestedImmutable;
private final NestedJavaBean nestedJavaBean;
NestedConstructorBeanWithDefaultValue(@DefaultValue NestedImmutable nestedImmutable,
@DefaultValue NestedJavaBean nestedJavaBean) {
this.nestedImmutable = nestedImmutable;
this.nestedJavaBean = nestedJavaBean;
}
NestedImmutable getNestedImmutable() {
return this.nestedImmutable;
}
NestedJavaBean getNestedJavaBean() {
return this.nestedJavaBean;
}
}
static class NestedImmutable {
private final String foo;
private final String bar;
NestedImmutable(@DefaultValue("hello") String foo, String bar) {
this.foo = foo;
this.bar = bar;
}
String getFoo() {
return this.foo;
}
String getBar() {
return this.bar;
}
}
static class NestedJavaBean {
private String value;
String getValue() {
return this.value;
}
}
static class NestedConstructorBeanWithEmptyDefaultValueForJavaLangTypes {
private final String stringValue;
NestedConstructorBeanWithEmptyDefaultValueForJavaLangTypes(@DefaultValue String stringValue) {
this.stringValue = stringValue;
}
String getStringValue() {
return this.stringValue;
}
}
static class NestedConstructorBeanWithEmptyDefaultValueForCollectionTypes {
private final List<String> listValue;
NestedConstructorBeanWithEmptyDefaultValueForCollectionTypes(@DefaultValue List<String> listValue) {
this.listValue = listValue;
}
List<String> getListValue() {
return this.listValue;
}
}
static class NestedConstructorBeanWithEmptyDefaultValueForMapTypes {
private final Map<String, String> mapValue;
NestedConstructorBeanWithEmptyDefaultValueForMapTypes(@DefaultValue Map<String, String> mapValue) {
this.mapValue = mapValue;
}
Map<String, String> getMapValue() {
return this.mapValue;
}
}
static class NestedConstructorBeanWithEmptyDefaultValueForArrayTypes {
private final String[] arrayValue;
NestedConstructorBeanWithEmptyDefaultValueForArrayTypes(@DefaultValue String[] arrayValue,
@DefaultValue Integer intValue) {
this.arrayValue = arrayValue;
}
String[] getArrayValue() {
return this.arrayValue;
}
}
static class NestedConstructorBeanWithEmptyDefaultValueForEnumTypes {
private Foo foo;
NestedConstructorBeanWithEmptyDefaultValueForEnumTypes(@DefaultValue Foo foo) {
this.foo = foo;
}
Foo getFoo() {
return this.foo;
}
enum Foo {
BAR, BAZ
}
}
static class NestedConstructorBeanWithEmptyDefaultValueForPrimitiveTypes {
private int intValue;
NestedConstructorBeanWithEmptyDefaultValueForPrimitiveTypes(@DefaultValue int intValue) {
this.intValue = intValue;
}
int getIntValue() {
return this.intValue;
}
}
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment