Add dynamic ObjectToObjectConverter hints

This commit refines BindingReflectionHintsRegistrar with additional
dynamic hints for application-defined types, main core Java conversion
ones being already covered by ObjectToObjectConverterRuntimeHints.

Closes gh-35847
This commit is contained in:
Sébastien Deleuze
2025-11-26 16:17:41 +01:00
parent 6504177e7b
commit 8647c44364
3 changed files with 61 additions and 0 deletions
@@ -20,6 +20,7 @@ import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.RecordComponent;
import java.lang.reflect.Type;
import java.util.HashSet;
@@ -116,6 +117,7 @@ public class BindingReflectionHintsRegistrar {
if (JACKSON_ANNOTATION_PRESENT) {
registerJacksonHints(hints, clazz);
}
registerObjectToObjectConverterHints(hints, clazz);
}
if (KotlinDetector.isKotlinType(clazz)) {
KotlinDelegate.registerComponentHints(hints, clazz);
@@ -156,6 +158,25 @@ public class BindingReflectionHintsRegistrar {
}
}
// See also the static hints registered by ObjectToObjectConverterRuntimeHints
private void registerObjectToObjectConverterHints(ReflectionHints hints, Class<?> clazz) {
for (Method method : clazz.getMethods()) {
String name = method.getName();
boolean isStatic = Modifier.isStatic(method.getModifiers());
if (isStatic && (clazz != String.class) && areRelatedTypes(method.getReturnType(), clazz) &&
(name.equals("valueOf") || name.equals("of") || name.equals("from"))) {
hints.registerMethod(method, ExecutableMode.INVOKE);
}
if (!isStatic && (method.getReturnType() != String.class) && name.equals("to" + method.getReturnType().getSimpleName())) {
hints.registerMethod(method, ExecutableMode.INVOKE);
}
}
}
private static boolean areRelatedTypes(Class<?> type1, Class<?> type2) {
return (ClassUtils.isAssignable(type1, type2) || ClassUtils.isAssignable(type2, type1));
}
private void collectReferencedTypes(Set<Class<?>> types, ResolvableType resolvableType) {
Class<?> clazz = resolvableType.resolve();
if (clazz != null && !types.contains(clazz)) {
@@ -23,6 +23,7 @@ import java.util.List;
import org.jspecify.annotations.Nullable;
import org.springframework.aot.hint.BindingReflectionHintsRegistrar;
import org.springframework.aot.hint.ExecutableMode;
import org.springframework.aot.hint.MemberCategory;
import org.springframework.aot.hint.ReflectionHints;
@@ -33,6 +34,7 @@ import org.springframework.aot.hint.TypeReference;
/**
* {@link RuntimeHintsRegistrar} to register hints for popular conventions in
* {@code org.springframework.core.convert.support.ObjectToObjectConverter}.
* Some dynamic hints registered by {@link BindingReflectionHintsRegistrar}.
*
* @author Sebastien Deleuze
* @author Sam Brannen
@@ -31,6 +31,7 @@ import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import org.junit.jupiter.api.Test;
import org.springframework.aot.hint.predicate.ReflectionHintsPredicates;
import org.springframework.aot.hint.predicate.RuntimeHintsPredicates;
import org.springframework.core.ResolvableType;
@@ -301,6 +302,16 @@ class BindingReflectionHintsRegistrarTests {
.accepts(this.hints);
}
@Test
void registerTypeForObjectToObjectConverter() {
bindingRegistrar.registerReflectionHints(this.hints.reflection(), Source.class);
ReflectionHintsPredicates reflection = RuntimeHintsPredicates.reflection();
assertThat(reflection.onMethodInvocation(Source.class, "valueOf")).accepts(this.hints);
assertThat(reflection.onMethodInvocation(Source.class, "of")).accepts(this.hints);
assertThat(reflection.onMethodInvocation(Source.class, "from")).accepts(this.hints);
assertThat(reflection.onMethodInvocation(Source.class, "toData")).accepts(this.hints);
}
static class SampleEmptyClass {
}
@@ -460,4 +471,31 @@ class BindingReflectionHintsRegistrarTests {
}
}
static class Source {
private final String value;
private Source(String value) {
this.value = value;
}
public static Source valueOf(String value) {
return new Source(value);
}
public static Source of(String value) {
return new Source(value);
}
public static Source from(String value) {
return new Source(value);
}
public Data toData() {
return new Data(this.value);
}
}
record Data(String value) { }
}