Skip to content

Commit f609022

Browse files
committed
Add suppressed missing parameters exception from ValueObjectBinder
Update `DataObjectBinder` interface and `ValueObjectBinder` implementation so that suppressed exceptions are added whenever parameter names cannot be discovered. See gh-38603
1 parent 6b58051 commit f609022

File tree

4 files changed

+101
-16
lines changed

4 files changed

+101
-16
lines changed

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/Binder.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -366,7 +366,13 @@ private <T> T handleBindResult(ConfigurationPropertyName name, Bindable<T> targe
366366
(dataObjectBinder) -> dataObjectBinder.create(target, context));
367367
result = handler.onCreate(name, target, context, result);
368368
result = context.getConverter().convert(result, target);
369-
Assert.state(result != null, () -> "Unable to create instance for " + target.getType());
369+
if (result == null) {
370+
IllegalStateException ex = new IllegalStateException(
371+
"Unable to create instance for " + target.getType());
372+
this.dataObjectBinders.get(target.getBindMethod())
373+
.forEach((dataObjectBinder) -> dataObjectBinder.onUnableToCreateInstance(target, context, ex));
374+
throw ex;
375+
}
370376
}
371377
handler.onFinish(name, target, context, result);
372378
return context.getConverter().convert(result, target);

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/DataObjectBinder.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,4 +53,15 @@ <T> T bind(ConfigurationPropertyName name, Bindable<T> target, Context context,
5353
*/
5454
<T> T create(Bindable<T> target, Context context);
5555

56+
/**
57+
* Callback that can be used to add additional suppressed exceptions when an instance
58+
* cannot be created.
59+
* @param <T> the source type
60+
* @param target the bindable that was being created
61+
* @param context the bind context
62+
* @param exception the exception about to be thrown
63+
*/
64+
default <T> void onUnableToCreateInstance(Bindable<T> target, Binder.Context context, RuntimeException exception) {
65+
}
66+
5667
}

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/ValueObjectBinder.java

Lines changed: 70 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import java.lang.annotation.Annotation;
2020
import java.lang.reflect.Array;
2121
import java.lang.reflect.Constructor;
22+
import java.lang.reflect.Method;
2223
import java.lang.reflect.Modifier;
2324
import java.lang.reflect.Parameter;
2425
import java.util.ArrayList;
@@ -27,6 +28,7 @@
2728
import java.util.List;
2829
import java.util.Map;
2930
import java.util.Optional;
31+
import java.util.function.Consumer;
3032

3133
import kotlin.reflect.KFunction;
3234
import kotlin.reflect.KParameter;
@@ -35,6 +37,7 @@
3537
import org.apache.commons.logging.LogFactory;
3638

3739
import org.springframework.beans.BeanUtils;
40+
import org.springframework.boot.context.properties.bind.Binder.Context;
3841
import org.springframework.boot.context.properties.source.ConfigurationPropertyName;
3942
import org.springframework.core.CollectionFactory;
4043
import org.springframework.core.DefaultParameterNameDiscoverer;
@@ -69,7 +72,7 @@ class ValueObjectBinder implements DataObjectBinder {
6972
@Override
7073
public <T> T bind(ConfigurationPropertyName name, Bindable<T> target, Binder.Context context,
7174
DataObjectPropertyBinder propertyBinder) {
72-
ValueObject<T> valueObject = ValueObject.get(target, this.constructorProvider, context);
75+
ValueObject<T> valueObject = ValueObject.get(target, this.constructorProvider, context, Discoverer.LENIENT);
7376
if (valueObject == null) {
7477
return null;
7578
}
@@ -90,7 +93,7 @@ public <T> T bind(ConfigurationPropertyName name, Bindable<T> target, Binder.Con
9093

9194
@Override
9295
public <T> T create(Bindable<T> target, Binder.Context context) {
93-
ValueObject<T> valueObject = ValueObject.get(target, this.constructorProvider, context);
96+
ValueObject<T> valueObject = ValueObject.get(target, this.constructorProvider, context, Discoverer.LENIENT);
9497
if (valueObject == null) {
9598
return null;
9699
}
@@ -102,6 +105,16 @@ public <T> T create(Bindable<T> target, Binder.Context context) {
102105
return valueObject.instantiate(args);
103106
}
104107

108+
@Override
109+
public <T> void onUnableToCreateInstance(Bindable<T> target, Context context, RuntimeException exception) {
110+
try {
111+
ValueObject.get(target, this.constructorProvider, context, Discoverer.STRICT);
112+
}
113+
catch (Exception ex) {
114+
exception.addSuppressed(ex);
115+
}
116+
}
117+
105118
private <T> T getDefaultValue(Binder.Context context, ConstructorParameter parameter) {
106119
ResolvableType type = parameter.getType();
107120
Annotation[] annotations = parameter.getAnnotations();
@@ -187,7 +200,7 @@ T instantiate(List<Object> args) {
187200

188201
@SuppressWarnings("unchecked")
189202
static <T> ValueObject<T> get(Bindable<T> bindable, BindConstructorProvider constructorProvider,
190-
Binder.Context context) {
203+
Binder.Context context, ParameterNameDiscoverer parameterNameDiscoverer) {
191204
Class<T> type = (Class<T>) bindable.getType().resolve();
192205
if (type == null || type.isEnum() || Modifier.isAbstract(type.getModifiers())) {
193206
return null;
@@ -198,9 +211,10 @@ static <T> ValueObject<T> get(Bindable<T> bindable, BindConstructorProvider cons
198211
return null;
199212
}
200213
if (KotlinDetector.isKotlinType(type)) {
201-
return KotlinValueObject.get((Constructor<T>) bindConstructor, bindable.getType());
214+
return KotlinValueObject.get((Constructor<T>) bindConstructor, bindable.getType(),
215+
parameterNameDiscoverer);
202216
}
203-
return DefaultValueObject.get(bindConstructor, bindable.getType());
217+
return DefaultValueObject.get(bindConstructor, bindable.getType(), parameterNameDiscoverer);
204218
}
205219

206220
}
@@ -246,12 +260,13 @@ List<ConstructorParameter> getConstructorParameters() {
246260
return this.constructorParameters;
247261
}
248262

249-
static <T> ValueObject<T> get(Constructor<T> bindConstructor, ResolvableType type) {
263+
static <T> ValueObject<T> get(Constructor<T> bindConstructor, ResolvableType type,
264+
ParameterNameDiscoverer parameterNameDiscoverer) {
250265
KFunction<T> kotlinConstructor = ReflectJvmMapping.getKotlinFunction(bindConstructor);
251266
if (kotlinConstructor != null) {
252267
return new KotlinValueObject<>(bindConstructor, kotlinConstructor, type);
253268
}
254-
return DefaultValueObject.get(bindConstructor, type);
269+
return DefaultValueObject.get(bindConstructor, type, parameterNameDiscoverer);
255270
}
256271

257272
}
@@ -262,8 +277,6 @@ static <T> ValueObject<T> get(Constructor<T> bindConstructor, ResolvableType typ
262277
*/
263278
private static final class DefaultValueObject<T> extends ValueObject<T> {
264279

265-
private static final ParameterNameDiscoverer PARAMETER_NAME_DISCOVERER = new DefaultParameterNameDiscoverer();
266-
267280
private final List<ConstructorParameter> constructorParameters;
268281

269282
private DefaultValueObject(Constructor<T> constructor, List<ConstructorParameter> constructorParameters) {
@@ -277,12 +290,10 @@ List<ConstructorParameter> getConstructorParameters() {
277290
}
278291

279292
@SuppressWarnings("unchecked")
280-
static <T> ValueObject<T> get(Constructor<?> bindConstructor, ResolvableType type) {
281-
String[] names = PARAMETER_NAME_DISCOVERER.getParameterNames(bindConstructor);
293+
static <T> ValueObject<T> get(Constructor<?> bindConstructor, ResolvableType type,
294+
ParameterNameDiscoverer parameterNameDiscoverer) {
295+
String[] names = parameterNameDiscoverer.getParameterNames(bindConstructor);
282296
if (names == null) {
283-
logger.debug(LogMessage.format(
284-
"Unable to use value object binding with %s as parameter names cannot be discovered",
285-
bindConstructor));
286297
return null;
287298
}
288299
List<ConstructorParameter> constructorParameters = parseConstructorParameters(bindConstructor, type, names);
@@ -339,4 +350,49 @@ ResolvableType getType() {
339350

340351
}
341352

353+
/**
354+
* {@link ParameterNameDiscoverer} used for value data object binding.
355+
*/
356+
static final class Discoverer implements ParameterNameDiscoverer {
357+
358+
private static final ParameterNameDiscoverer DEFAULT_DELEGATE = new DefaultParameterNameDiscoverer();
359+
360+
private static final ParameterNameDiscoverer LENIENT = new Discoverer(DEFAULT_DELEGATE, (message) -> {
361+
});
362+
363+
private static final ParameterNameDiscoverer STRICT = new Discoverer(DEFAULT_DELEGATE, (message) -> {
364+
throw new IllegalStateException(message.toString());
365+
});
366+
367+
private final ParameterNameDiscoverer delegate;
368+
369+
private final Consumer<LogMessage> noParameterNamesHandler;
370+
371+
private Discoverer(ParameterNameDiscoverer delegate, Consumer<LogMessage> noParameterNamesHandler) {
372+
this.delegate = delegate;
373+
this.noParameterNamesHandler = noParameterNamesHandler;
374+
}
375+
376+
@Override
377+
public String[] getParameterNames(Method method) {
378+
throw new UnsupportedOperationException();
379+
}
380+
381+
@Override
382+
public String[] getParameterNames(Constructor<?> constructor) {
383+
String[] names = this.delegate.getParameterNames(constructor);
384+
if (names != null) {
385+
return names;
386+
}
387+
LogMessage message = LogMessage.format(
388+
"Unable to use value object binding with constructor [%s] as parameter names cannot be discovered. "
389+
+ "Ensure that the compiler uses the '-parameters' flag",
390+
constructor);
391+
this.noParameterNamesHandler.accept(message);
392+
logger.debug(message);
393+
return null;
394+
}
395+
396+
}
397+
342398
}

spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/ValueObjectBinderTests.java

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import java.util.Optional;
2828

2929
import com.jayway.jsonpath.JsonPath;
30+
import com.jayway.jsonpath.internal.CharacterIndex;
3031
import org.junit.jupiter.api.Test;
3132

3233
import org.springframework.boot.context.properties.source.ConfigurationPropertyName;
@@ -394,7 +395,7 @@ public record RecordProperties(
394395
}
395396

396397
@Test // gh-38201
397-
void bindWithNonExtractableParameterNamesAndNonIterablePropertySource() throws Exception {
398+
void bindWhenNonExtractableParameterNamesOnPropertyAndNonIterablePropertySource() throws Exception {
398399
verifyJsonPathParametersCannotBeResolved();
399400
MockConfigurationPropertySource source = new MockConfigurationPropertySource();
400401
source.put("test.value", "test");
@@ -404,6 +405,17 @@ void bindWithNonExtractableParameterNamesAndNonIterablePropertySource() throws E
404405
assertThat(bound.getValue()).isEqualTo("test");
405406
}
406407

408+
@Test
409+
void createWhenNonExtractableParameterNamesOnPropertyAndNonIterablePropertySource() throws Exception {
410+
assertThat(new DefaultParameterNameDiscoverer()
411+
.getParameterNames(CharacterIndex.class.getDeclaredConstructor(CharSequence.class))).isNull();
412+
MockConfigurationPropertySource source = new MockConfigurationPropertySource();
413+
this.sources.add(source.nonIterable());
414+
Bindable<CharacterIndex> target = Bindable.of(CharacterIndex.class).withBindMethod(BindMethod.VALUE_OBJECT);
415+
assertThatExceptionOfType(BindException.class).isThrownBy(() -> this.binder.bindOrCreate("test", target))
416+
.withStackTraceContaining("Ensure that the compiler uses the '-parameters' flag");
417+
}
418+
407419
private void verifyJsonPathParametersCannotBeResolved() throws NoSuchFieldException {
408420
Class<?> jsonPathClass = NonExtractableParameterName.class.getDeclaredField("jsonPath").getType();
409421
Constructor<?>[] constructors = jsonPathClass.getDeclaredConstructors();

0 commit comments

Comments
 (0)