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 b88e03c906..4d8fbf0133 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 @@ -366,7 +366,13 @@ public class Binder { (dataObjectBinder) -> dataObjectBinder.create(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()); + if (result == null) { + IllegalStateException ex = new IllegalStateException( + "Unable to create instance for " + target.getType()); + this.dataObjectBinders.get(target.getBindMethod()) + .forEach((dataObjectBinder) -> dataObjectBinder.onUnableToCreateInstance(target, context, ex)); + throw ex; + } } handler.onFinish(name, target, context, result); return context.getConverter().convert(result, target); diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/DataObjectBinder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/DataObjectBinder.java index 6fdd230c6d..f57b1c4c44 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/DataObjectBinder.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/DataObjectBinder.java @@ -53,4 +53,15 @@ interface DataObjectBinder { */ T create(Bindable target, Context context); + /** + * Callback that can be used to add additional suppressed exceptions when an instance + * cannot be created. + * @param the source type + * @param target the bindable that was being created + * @param context the bind context + * @param exception the exception about to be thrown + */ + default void onUnableToCreateInstance(Bindable target, Binder.Context context, RuntimeException exception) { + } + } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/ValueObjectBinder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/ValueObjectBinder.java index 5f51e91f34..aecac3e612 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/ValueObjectBinder.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/ValueObjectBinder.java @@ -19,6 +19,7 @@ package org.springframework.boot.context.properties.bind; import java.lang.annotation.Annotation; import java.lang.reflect.Array; import java.lang.reflect.Constructor; +import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.lang.reflect.Parameter; import java.util.ArrayList; @@ -27,6 +28,7 @@ import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.function.Consumer; import kotlin.reflect.KFunction; import kotlin.reflect.KParameter; @@ -35,6 +37,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.beans.BeanUtils; +import org.springframework.boot.context.properties.bind.Binder.Context; import org.springframework.boot.context.properties.source.ConfigurationPropertyName; import org.springframework.core.CollectionFactory; import org.springframework.core.DefaultParameterNameDiscoverer; @@ -69,7 +72,7 @@ class ValueObjectBinder implements DataObjectBinder { @Override public T bind(ConfigurationPropertyName name, Bindable target, Binder.Context context, DataObjectPropertyBinder propertyBinder) { - ValueObject valueObject = ValueObject.get(target, this.constructorProvider, context); + ValueObject valueObject = ValueObject.get(target, this.constructorProvider, context, Discoverer.LENIENT); if (valueObject == null) { return null; } @@ -90,7 +93,7 @@ class ValueObjectBinder implements DataObjectBinder { @Override public T create(Bindable target, Binder.Context context) { - ValueObject valueObject = ValueObject.get(target, this.constructorProvider, context); + ValueObject valueObject = ValueObject.get(target, this.constructorProvider, context, Discoverer.LENIENT); if (valueObject == null) { return null; } @@ -102,6 +105,16 @@ class ValueObjectBinder implements DataObjectBinder { return valueObject.instantiate(args); } + @Override + public void onUnableToCreateInstance(Bindable target, Context context, RuntimeException exception) { + try { + ValueObject.get(target, this.constructorProvider, context, Discoverer.STRICT); + } + catch (Exception ex) { + exception.addSuppressed(ex); + } + } + private T getDefaultValue(Binder.Context context, ConstructorParameter parameter) { ResolvableType type = parameter.getType(); Annotation[] annotations = parameter.getAnnotations(); @@ -187,7 +200,7 @@ class ValueObjectBinder implements DataObjectBinder { @SuppressWarnings("unchecked") static ValueObject get(Bindable bindable, BindConstructorProvider constructorProvider, - Binder.Context context) { + Binder.Context context, ParameterNameDiscoverer parameterNameDiscoverer) { Class type = (Class) bindable.getType().resolve(); if (type == null || type.isEnum() || Modifier.isAbstract(type.getModifiers())) { return null; @@ -198,9 +211,10 @@ class ValueObjectBinder implements DataObjectBinder { return null; } if (KotlinDetector.isKotlinType(type)) { - return KotlinValueObject.get((Constructor) bindConstructor, bindable.getType()); + return KotlinValueObject.get((Constructor) bindConstructor, bindable.getType(), + parameterNameDiscoverer); } - return DefaultValueObject.get(bindConstructor, bindable.getType()); + return DefaultValueObject.get(bindConstructor, bindable.getType(), parameterNameDiscoverer); } } @@ -246,12 +260,13 @@ class ValueObjectBinder implements DataObjectBinder { return this.constructorParameters; } - static ValueObject get(Constructor bindConstructor, ResolvableType type) { + static ValueObject get(Constructor bindConstructor, ResolvableType type, + ParameterNameDiscoverer parameterNameDiscoverer) { KFunction kotlinConstructor = ReflectJvmMapping.getKotlinFunction(bindConstructor); if (kotlinConstructor != null) { return new KotlinValueObject<>(bindConstructor, kotlinConstructor, type); } - return DefaultValueObject.get(bindConstructor, type); + return DefaultValueObject.get(bindConstructor, type, parameterNameDiscoverer); } } @@ -262,8 +277,6 @@ class ValueObjectBinder implements DataObjectBinder { */ private static final class DefaultValueObject extends ValueObject { - private static final ParameterNameDiscoverer PARAMETER_NAME_DISCOVERER = new DefaultParameterNameDiscoverer(); - private final List constructorParameters; private DefaultValueObject(Constructor constructor, List constructorParameters) { @@ -277,12 +290,10 @@ class ValueObjectBinder implements DataObjectBinder { } @SuppressWarnings("unchecked") - static ValueObject get(Constructor bindConstructor, ResolvableType type) { - String[] names = PARAMETER_NAME_DISCOVERER.getParameterNames(bindConstructor); + static ValueObject get(Constructor bindConstructor, ResolvableType type, + ParameterNameDiscoverer parameterNameDiscoverer) { + String[] names = parameterNameDiscoverer.getParameterNames(bindConstructor); if (names == null) { - logger.debug(LogMessage.format( - "Unable to use value object binding with %s as parameter names cannot be discovered", - bindConstructor)); return null; } List constructorParameters = parseConstructorParameters(bindConstructor, type, names); @@ -339,4 +350,49 @@ class ValueObjectBinder implements DataObjectBinder { } + /** + * {@link ParameterNameDiscoverer} used for value data object binding. + */ + static final class Discoverer implements ParameterNameDiscoverer { + + private static final ParameterNameDiscoverer DEFAULT_DELEGATE = new DefaultParameterNameDiscoverer(); + + private static final ParameterNameDiscoverer LENIENT = new Discoverer(DEFAULT_DELEGATE, (message) -> { + }); + + private static final ParameterNameDiscoverer STRICT = new Discoverer(DEFAULT_DELEGATE, (message) -> { + throw new IllegalStateException(message.toString()); + }); + + private final ParameterNameDiscoverer delegate; + + private final Consumer noParameterNamesHandler; + + private Discoverer(ParameterNameDiscoverer delegate, Consumer noParameterNamesHandler) { + this.delegate = delegate; + this.noParameterNamesHandler = noParameterNamesHandler; + } + + @Override + public String[] getParameterNames(Method method) { + throw new UnsupportedOperationException(); + } + + @Override + public String[] getParameterNames(Constructor constructor) { + String[] names = this.delegate.getParameterNames(constructor); + if (names != null) { + return names; + } + LogMessage message = LogMessage.format( + "Unable to use value object binding with constructor [%s] as parameter names cannot be discovered. " + + "Ensure that the compiler uses the '-parameters' flag", + constructor); + this.noParameterNamesHandler.accept(message); + logger.debug(message); + return null; + } + + } + } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/ValueObjectBinderTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/ValueObjectBinderTests.java index a47f30aa0f..ac95a9d1d6 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/ValueObjectBinderTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/ValueObjectBinderTests.java @@ -27,6 +27,7 @@ import java.util.Objects; import java.util.Optional; import com.jayway.jsonpath.JsonPath; +import com.jayway.jsonpath.internal.CharacterIndex; import org.junit.jupiter.api.Test; import org.springframework.boot.context.properties.source.ConfigurationPropertyName; @@ -394,7 +395,7 @@ class ValueObjectBinderTests { } @Test // gh-38201 - void bindWithNonExtractableParameterNamesAndNonIterablePropertySource() throws Exception { + void bindWhenNonExtractableParameterNamesOnPropertyAndNonIterablePropertySource() throws Exception { verifyJsonPathParametersCannotBeResolved(); MockConfigurationPropertySource source = new MockConfigurationPropertySource(); source.put("test.value", "test"); @@ -404,6 +405,17 @@ class ValueObjectBinderTests { assertThat(bound.getValue()).isEqualTo("test"); } + @Test + void createWhenNonExtractableParameterNamesOnPropertyAndNonIterablePropertySource() throws Exception { + assertThat(new DefaultParameterNameDiscoverer() + .getParameterNames(CharacterIndex.class.getDeclaredConstructor(CharSequence.class))).isNull(); + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + this.sources.add(source.nonIterable()); + Bindable target = Bindable.of(CharacterIndex.class).withBindMethod(BindMethod.VALUE_OBJECT); + assertThatExceptionOfType(BindException.class).isThrownBy(() -> this.binder.bindOrCreate("test", target)) + .withStackTraceContaining("Ensure that the compiler uses the '-parameters' flag"); + } + private void verifyJsonPathParametersCannotBeResolved() throws NoSuchFieldException { Class jsonPathClass = NonExtractableParameterName.class.getDeclaredField("jsonPath").getType(); Constructor[] constructors = jsonPathClass.getDeclaredConstructors();