Register runtime hints for Instant-to-Timestamp conversion

If an application depends on automatic type conversion from
java.time.Instant to java.sql.Timestamp, the ObjectToObjectConverter
performs the conversion based on convention, by using reflection to
invoke Timestamp.from(Instant).

However, when running in a native image a user needs to explicitly
register runtime hints for that particular use of reflection.

To assist users who are running their applications in a native image,
this commit automatically registers the necessary runtime hints for
Timestamp.from(Instant) so that users do not have to.

See gh-35175
Closes gh-35156
This commit is contained in:
Sam Brannen 2025-07-09 12:35:10 +02:00
parent 7900315f23
commit 3dc22379a0
2 changed files with 24 additions and 8 deletions

View File

@ -16,6 +16,7 @@
package org.springframework.aot.hint.support;
import java.time.Instant;
import java.time.LocalDate;
import java.util.Collections;
import java.util.List;
@ -33,6 +34,7 @@ import org.springframework.lang.Nullable;
* {@code org.springframework.core.convert.support.ObjectToObjectConverter}.
*
* @author Sebastien Deleuze
* @author Sam Brannen
* @since 6.0
*/
class ObjectToObjectConverterRuntimeHints implements RuntimeHintsRegistrar {
@ -40,6 +42,7 @@ class ObjectToObjectConverterRuntimeHints implements RuntimeHintsRegistrar {
@Override
public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) {
ReflectionHints reflectionHints = hints.reflection();
TypeReference sqlDateTypeReference = TypeReference.of("java.sql.Date");
reflectionHints.registerTypeIfPresent(classLoader, sqlDateTypeReference.getName(), hint -> hint
.withMethod("toLocalDate", Collections.emptyList(), ExecutableMode.INVOKE)
@ -47,8 +50,14 @@ class ObjectToObjectConverterRuntimeHints implements RuntimeHintsRegistrar {
.withMethod("valueOf", List.of(TypeReference.of(LocalDate.class)), ExecutableMode.INVOKE)
.onReachableType(sqlDateTypeReference));
TypeReference sqlTimestampTypeReference = TypeReference.of("java.sql.Timestamp");
reflectionHints.registerTypeIfPresent(classLoader, sqlTimestampTypeReference.getName(), hint -> hint
.withMethod("from", List.of(TypeReference.of(Instant.class)), ExecutableMode.INVOKE)
.onReachableType(sqlTimestampTypeReference));
reflectionHints.registerTypeIfPresent(classLoader, "org.springframework.http.HttpMethod",
builder -> builder.withMethod("valueOf", List.of(TypeReference.of(String.class)), ExecutableMode.INVOKE));
reflectionHints.registerTypeIfPresent(classLoader, "java.net.URI", MemberCategory.INVOKE_DECLARED_CONSTRUCTORS);
}

View File

@ -17,6 +17,7 @@
package org.springframework.aot.hint.support;
import java.net.URI;
import java.time.Instant;
import java.time.LocalDate;
import org.junit.jupiter.api.BeforeEach;
@ -24,38 +25,44 @@ import org.junit.jupiter.api.Test;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.RuntimeHintsRegistrar;
import org.springframework.aot.hint.predicate.RuntimeHintsPredicates;
import org.springframework.core.io.support.SpringFactoriesLoader;
import org.springframework.util.ClassUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.aot.hint.predicate.RuntimeHintsPredicates.reflection;
/**
* Tests for {@link ObjectToObjectConverterRuntimeHints}.
*
* @author Sebastien Deleuze
* @author Sam Brannen
*/
class ObjectToObjectConverterRuntimeHintsTests {
private RuntimeHints hints;
private final RuntimeHints hints = new RuntimeHints();
@BeforeEach
void setup() {
this.hints = new RuntimeHints();
SpringFactoriesLoader.forResourceLocation("META-INF/spring/aot.factories")
.load(RuntimeHintsRegistrar.class).forEach(registrar -> registrar
.registerHints(this.hints, ClassUtils.getDefaultClassLoader()));
.load(RuntimeHintsRegistrar.class)
.forEach(registrar -> registrar.registerHints(this.hints, ClassUtils.getDefaultClassLoader()));
}
@Test
void javaSqlDateHasHints() throws NoSuchMethodException {
assertThat(RuntimeHintsPredicates.reflection().onMethod(java.sql.Date.class, "toLocalDate")).accepts(this.hints);
assertThat(RuntimeHintsPredicates.reflection().onMethod(java.sql.Date.class.getMethod("valueOf", LocalDate.class))).accepts(this.hints);
assertThat(reflection().onMethod(java.sql.Date.class, "toLocalDate")).accepts(this.hints);
assertThat(reflection().onMethod(java.sql.Date.class.getMethod("valueOf", LocalDate.class))).accepts(this.hints);
}
@Test // gh-35156
void javaSqlTimestampHasHints() throws NoSuchMethodException {
assertThat(reflection().onMethod(java.sql.Timestamp.class.getMethod("from", Instant.class))).accepts(this.hints);
}
@Test
void uriHasHints() throws NoSuchMethodException {
assertThat(RuntimeHintsPredicates.reflection().onConstructor(URI.class.getConstructor(String.class))).accepts(this.hints);
assertThat(reflection().onConstructor(URI.class.getConstructor(String.class))).accepts(this.hints);
}
}