commit
1eb0dd5e39
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2012-2020 the original author or authors.
|
* Copyright 2012-2022 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -17,6 +17,7 @@
|
||||||
package org.springframework.boot.context.properties.bind;
|
package org.springframework.boot.context.properties.bind;
|
||||||
|
|
||||||
import java.lang.annotation.Annotation;
|
import java.lang.annotation.Annotation;
|
||||||
|
import java.lang.reflect.Array;
|
||||||
import java.lang.reflect.Constructor;
|
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;
|
||||||
|
@ -25,6 +26,7 @@ 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 java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
import kotlin.reflect.KFunction;
|
import kotlin.reflect.KFunction;
|
||||||
import kotlin.reflect.KParameter;
|
import kotlin.reflect.KParameter;
|
||||||
|
@ -32,6 +34,7 @@ import kotlin.reflect.jvm.ReflectJvmMapping;
|
||||||
|
|
||||||
import org.springframework.beans.BeanUtils;
|
import org.springframework.beans.BeanUtils;
|
||||||
import org.springframework.boot.context.properties.source.ConfigurationPropertyName;
|
import org.springframework.boot.context.properties.source.ConfigurationPropertyName;
|
||||||
|
import org.springframework.core.CollectionFactory;
|
||||||
import org.springframework.core.DefaultParameterNameDiscoverer;
|
import org.springframework.core.DefaultParameterNameDiscoverer;
|
||||||
import org.springframework.core.KotlinDetector;
|
import org.springframework.core.KotlinDetector;
|
||||||
import org.springframework.core.MethodParameter;
|
import org.springframework.core.MethodParameter;
|
||||||
|
@ -101,7 +104,7 @@ class ValueObjectBinder implements DataObjectBinder {
|
||||||
if (annotation instanceof DefaultValue) {
|
if (annotation instanceof DefaultValue) {
|
||||||
String[] defaultValue = ((DefaultValue) annotation).value();
|
String[] defaultValue = ((DefaultValue) annotation).value();
|
||||||
if (defaultValue.length == 0) {
|
if (defaultValue.length == 0) {
|
||||||
return getNewInstanceIfPossible(context, type);
|
return getNewDefaultValueInstanceIfPossible(context, type);
|
||||||
}
|
}
|
||||||
return convertDefaultValue(context.getConverter(), defaultValue, type, annotations);
|
return convertDefaultValue(context.getConverter(), defaultValue, type, annotations);
|
||||||
}
|
}
|
||||||
|
@ -124,7 +127,7 @@ class ValueObjectBinder implements DataObjectBinder {
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
private <T> T getNewInstanceIfPossible(Binder.Context context, ResolvableType type) {
|
private <T> T getNewDefaultValueInstanceIfPossible(Binder.Context context, ResolvableType type) {
|
||||||
Class<T> resolved = (Class<T>) type.resolve();
|
Class<T> resolved = (Class<T>) type.resolve();
|
||||||
Assert.state(resolved == null || isEmptyDefaultValueAllowed(resolved),
|
Assert.state(resolved == null || isEmptyDefaultValueAllowed(resolved),
|
||||||
() -> "Parameter of type " + type + " must have a non-empty default value.");
|
() -> "Parameter of type " + type + " must have a non-empty default value.");
|
||||||
|
@ -132,14 +135,27 @@ class ValueObjectBinder implements DataObjectBinder {
|
||||||
if (instance != null) {
|
if (instance != null) {
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
return (resolved != null) ? BeanUtils.instantiateClass(resolved) : null;
|
if (resolved != null) {
|
||||||
|
if (Optional.class == resolved) {
|
||||||
|
return (T) Optional.empty();
|
||||||
|
}
|
||||||
|
if (Collection.class.isAssignableFrom(resolved)) {
|
||||||
|
return (T) CollectionFactory.createCollection(resolved, 0);
|
||||||
|
}
|
||||||
|
if (Map.class.isAssignableFrom(resolved)) {
|
||||||
|
return (T) CollectionFactory.createMap(resolved, 0);
|
||||||
|
}
|
||||||
|
if (resolved.isArray()) {
|
||||||
|
return (T) Array.newInstance(resolved.getComponentType(), 0);
|
||||||
|
}
|
||||||
|
return BeanUtils.instantiateClass(resolved);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isEmptyDefaultValueAllowed(Class<?> type) {
|
private boolean isEmptyDefaultValueAllowed(Class<?> type) {
|
||||||
if (type.isPrimitive() || type.isEnum() || isAggregate(type) || type.getName().startsWith("java.lang")) {
|
return (Optional.class == type || isAggregate(type))
|
||||||
return false;
|
|| !(type.isPrimitive() || type.isEnum() || type.getName().startsWith("java.lang"));
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isAggregate(Class<?> type) {
|
private boolean isAggregate(Class<?> type) {
|
||||||
|
|
|
@ -29,6 +29,7 @@ import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
import java.util.Properties;
|
import java.util.Properties;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
|
@ -853,6 +854,17 @@ class ConfigurationPropertiesTests {
|
||||||
assertThat(bean.getBar()).isEqualTo(0);
|
assertThat(bean.getBar()).isEqualTo(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void loadWhenBindingToConstructorParametersWithEmptyDefaultValueShouldBind() {
|
||||||
|
load(ConstructorParameterEmptyDefaultValueConfiguration.class);
|
||||||
|
ConstructorParameterEmptyDefaultValueProperties bean = this.context
|
||||||
|
.getBean(ConstructorParameterEmptyDefaultValueProperties.class);
|
||||||
|
assertThat(bean.getSet()).isEmpty();
|
||||||
|
assertThat(bean.getMap()).isEmpty();
|
||||||
|
assertThat(bean.getArray()).isEmpty();
|
||||||
|
assertThat(bean.getOptional()).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void loadWhenBindingToConstructorParametersWithDefaultDataUnitShouldBind() {
|
void loadWhenBindingToConstructorParametersWithDefaultDataUnitShouldBind() {
|
||||||
load(ConstructorParameterWithUnitConfiguration.class);
|
load(ConstructorParameterWithUnitConfiguration.class);
|
||||||
|
@ -2145,6 +2157,45 @@ class ConfigurationPropertiesTests {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ConstructorBinding
|
||||||
|
@ConfigurationProperties(prefix = "test")
|
||||||
|
static class ConstructorParameterEmptyDefaultValueProperties {
|
||||||
|
|
||||||
|
private final Set<String> set;
|
||||||
|
|
||||||
|
private final Map<String, String> map;
|
||||||
|
|
||||||
|
private final int[] array;
|
||||||
|
|
||||||
|
private final Optional<String> optional;
|
||||||
|
|
||||||
|
ConstructorParameterEmptyDefaultValueProperties(@DefaultValue Set<String> set,
|
||||||
|
@DefaultValue Map<String, String> map, @DefaultValue int[] array,
|
||||||
|
@DefaultValue Optional<String> optional) {
|
||||||
|
this.set = set;
|
||||||
|
this.map = map;
|
||||||
|
this.array = array;
|
||||||
|
this.optional = optional;
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<String> getSet() {
|
||||||
|
return this.set;
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, String> getMap() {
|
||||||
|
return this.map;
|
||||||
|
}
|
||||||
|
|
||||||
|
int[] getArray() {
|
||||||
|
return this.array;
|
||||||
|
}
|
||||||
|
|
||||||
|
Optional<String> getOptional() {
|
||||||
|
return this.optional;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@ConstructorBinding
|
@ConstructorBinding
|
||||||
@ConfigurationProperties(prefix = "test")
|
@ConfigurationProperties(prefix = "test")
|
||||||
static class ConstructorParameterWithUnitProperties {
|
static class ConstructorParameterWithUnitProperties {
|
||||||
|
@ -2225,6 +2276,11 @@ class ConfigurationPropertiesTests {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@EnableConfigurationProperties(ConstructorParameterEmptyDefaultValueProperties.class)
|
||||||
|
static class ConstructorParameterEmptyDefaultValueConfiguration {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@EnableConfigurationProperties(ConstructorParameterWithUnitProperties.class)
|
@EnableConfigurationProperties(ConstructorParameterWithUnitProperties.class)
|
||||||
static class ConstructorParameterWithUnitConfiguration {
|
static class ConstructorParameterWithUnitConfiguration {
|
||||||
|
|
||||||
|
|
|
@ -31,6 +31,7 @@ import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.condition.EnabledForJreRange;
|
import org.junit.jupiter.api.condition.EnabledForJreRange;
|
||||||
|
@ -293,29 +294,31 @@ class ValueObjectBinderTests {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void bindWhenCollectionParameterWithEmptyDefaultValueShouldThrowException() {
|
void bindWhenCollectionParameterWithEmptyDefaultValueShouldReturnEmptyInstance() {
|
||||||
assertThatExceptionOfType(BindException.class)
|
NestedConstructorBeanWithEmptyDefaultValueForCollectionTypes bound = this.binder.bindOrCreate("foo",
|
||||||
.isThrownBy(() -> this.binder.bindOrCreate("foo",
|
Bindable.of(NestedConstructorBeanWithEmptyDefaultValueForCollectionTypes.class));
|
||||||
Bindable.of(NestedConstructorBeanWithEmptyDefaultValueForCollectionTypes.class)))
|
assertThat(bound.getListValue()).isEmpty();
|
||||||
.withStackTraceContaining(
|
|
||||||
"Parameter of type java.util.List<java.lang.String> must have a non-empty default value.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void bindWhenMapParametersWithEmptyDefaultValueShouldThrowException() {
|
void bindWhenMapParametersWithEmptyDefaultValueShouldReturnEmptyInstance() {
|
||||||
assertThatExceptionOfType(BindException.class)
|
NestedConstructorBeanWithEmptyDefaultValueForMapTypes bound = this.binder.bindOrCreate("foo",
|
||||||
.isThrownBy(() -> this.binder.bindOrCreate("foo",
|
Bindable.of(NestedConstructorBeanWithEmptyDefaultValueForMapTypes.class));
|
||||||
Bindable.of(NestedConstructorBeanWithEmptyDefaultValueForMapTypes.class)))
|
assertThat(bound.getMapValue()).isEmpty();
|
||||||
.withStackTraceContaining(
|
|
||||||
"Parameter of type java.util.Map<java.lang.String, java.lang.String> must have a non-empty default value.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void bindWhenArrayParameterWithEmptyDefaultValueShouldThrowException() {
|
void bindWhenArrayParameterWithEmptyDefaultValueShouldReturnEmptyInstance() {
|
||||||
assertThatExceptionOfType(BindException.class)
|
NestedConstructorBeanWithEmptyDefaultValueForArrayTypes bound = this.binder.bindOrCreate("foo",
|
||||||
.isThrownBy(() -> this.binder.bindOrCreate("foo",
|
Bindable.of(NestedConstructorBeanWithEmptyDefaultValueForArrayTypes.class));
|
||||||
Bindable.of(NestedConstructorBeanWithEmptyDefaultValueForArrayTypes.class)))
|
assertThat(bound.getArrayValue()).isEmpty();
|
||||||
.withStackTraceContaining("Parameter of type java.lang.String[] must have a non-empty default value.");
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void bindWhenOptionalParameterWithEmptyDefaultValueShouldReturnEmptyInstance() {
|
||||||
|
NestedConstructorBeanWithEmptyDefaultValueForOptionalTypes bound = this.binder.bindOrCreate("foo",
|
||||||
|
Bindable.of(NestedConstructorBeanWithEmptyDefaultValueForOptionalTypes.class));
|
||||||
|
assertThat(bound.getOptionalValue()).isEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -753,8 +756,7 @@ class ValueObjectBinderTests {
|
||||||
|
|
||||||
private final String[] arrayValue;
|
private final String[] arrayValue;
|
||||||
|
|
||||||
NestedConstructorBeanWithEmptyDefaultValueForArrayTypes(@DefaultValue String[] arrayValue,
|
NestedConstructorBeanWithEmptyDefaultValueForArrayTypes(@DefaultValue String[] arrayValue) {
|
||||||
@DefaultValue Integer intValue) {
|
|
||||||
this.arrayValue = arrayValue;
|
this.arrayValue = arrayValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -764,6 +766,20 @@ class ValueObjectBinderTests {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static class NestedConstructorBeanWithEmptyDefaultValueForOptionalTypes {
|
||||||
|
|
||||||
|
private final Optional<String> optionalValue;
|
||||||
|
|
||||||
|
NestedConstructorBeanWithEmptyDefaultValueForOptionalTypes(@DefaultValue Optional<String> optionalValue) {
|
||||||
|
this.optionalValue = optionalValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Optional<String> getOptionalValue() {
|
||||||
|
return this.optionalValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
static class NestedConstructorBeanWithEmptyDefaultValueForEnumTypes {
|
static class NestedConstructorBeanWithEmptyDefaultValueForEnumTypes {
|
||||||
|
|
||||||
private Foo foo;
|
private Foo foo;
|
||||||
|
|
Loading…
Reference in New Issue