Original Title

Default RowMapper converters are different between JVM and Native builds

Issue Description

When creating a Spring JDBC application with GraalVM native support it seems that the Converters that normally are available by default, are not registered automatically.

If I run my application using ./mvnw spring-boot:run, everything executes fine. When running the application using ./mvnw -Pnative native:compile && ./target/native-jdbc-converters, I get the following error: org.springframework.core.convert.ConverterNotFoundException: No converter found capable of converting from type [java.sql.Timestamp] to type [java.time.Instant].

I would expect this to work the same for a basic application. Does the native image build require extra configuration to register the JSR310 converters?

A repository with reproduction code can be found here: https://github.com/SanderKnauff/reproduction-spring-native-jdbc-converters

Reproduction steps

  1. Clone the repository above.
  2. Start the PostgreSQL database using docker-compose up.
  3. Run the application once with ./mvnw spring-boot:run. This time it should work fine.
  4. Run the native application by running ./mvnw native:compile && ./target/native-jdbc-converters. This will result in the aforementioned exception.

Environment

  • Spring Boot 3.5.4
  • OpenJDK 64-Bit Server VM GraalVM CE 21.0.2+13.1 (build 21.0.2+13-jvmci-23.1-b30, mixed mode, sharing)
  • Linux Mint

Comment From: wilkinsona

Thanks for the easy-to-use sample.

You're relying on ObjectToObjectConverter which can convert a Timestamp to an Instant by calling Timestamp#toInstant. This doesn't work in a native image as the toInstant method is not available for reflection.

Registering all possible methods that can be used by ObjectToObjectConverter would have a prohibitively large impact on the native image size so the Framework team chose not to do so. They do have a few selected hints for Object-to-Object conversion. We'll transfer this issue to the Framework team to see if they want to add Timestamp's toInstant method to those.

Comment From: SanderKnauff

Thank you, for now I will check if I can get the reflection hints in my own project, but I think this would be a really nice one to have by default.

Comment From: SanderKnauff

For anyone who is running into this while this is being triaged, the following code snippet, which is based on the link that @wilkinsona provided, will add the RuntimeHints.

First create a class that implements RunTimeHintsRegistrar:

class TimeStampToInstantReflectionHints implements RuntimeHintsRegistrar {
    @Override
    public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
        final var reflectionHints = hints.reflection();
        final var sqlTimestampReference = TypeReference.of(Timestamp.class);
        reflectionHints.registerTypeIfPresent(classLoader, sqlTimestampReference.getName(), hint -> hint
            .withMethod("toInstant", Collections.emptyList(), ExecutableMode.INVOKE)
            .onReachableType(sqlTimestampReference)
            .withMethod("from", List.of(TypeReference.of(Instant.class)), ExecutableMode.INVOKE)
            .onReachableType(sqlTimestampReference));
    }
}

Then add the @ImportRuntimeHints(TimeStampToInstantReflectionHints.class) to any Spring component or Bean method. I personally added the annotation to the Repository that performed the offending conversion.

See this documentation page for more information: https://docs.spring.io/spring-framework/reference/core/aot.html#aot.hints.import-runtime-hints

Comment From: sbrannen

Hi @SanderKnauff,

Thanks for reporting the issue.

I introduced Date-to-Instant and Instant-to-Date converters in #35175, and I registered runtime hints for Instant-to-Timestamp conversion in conjunction with this issue.

Please try out 6.2.9 snapshots and let us know if the changes allow you to remove your custom TimeStampToInstantReflectionHints.

Thanks

Comment From: wilkinsona

I registered runtime hints for Instant-to-Timestamp conversion in conjunction with this issue.

I think this is the wrong way around. From my debugging, I'm pretty sure that the sample was relying on Timestamp-to-Instant conversion using Timestamp.toInstant().

Comment From: sbrannen

I've also created an issue to improve the documentation.

  • 35178

Comment From: sbrannen

I registered runtime hints for Instant-to-Timestamp conversion in conjunction with this issue.

I think this is the wrong way around. From my debugging, I'm pretty sure that the sample was relying on Timestamp-to-Instant conversion using Timestamp.toInstant().

The custom TimeStampToInstantReflectionHints that @SanderKnauff provided handles both Timestamp#toInstant and Timestamp#from(Instant).

The new DateToInstantConverter introduced in conjunction with #35175 handles the Timestamp-to-Instant conversion.

And the runtime hint registration handles the Instant-to-Timestamp conversion.

So, we wanted to make sure we have both use cases covered.

Comment From: wilkinsona

Thanks, Sam. I'd overlooked the conversion in that direction being handled by the new DateToInstantConverter.

Comment From: SanderKnauff

Hi @SanderKnauff,

Thanks for reporting the issue.

I introduced Date-to-Instant and Instant-to-Date converters in #35175, and I registered runtime hints for Instant-to-Timestamp conversion in conjunction with this issue.

Please try out 6.2.9 snapshots and let us know if the changes allow you to remove your custom TimeStampToInstantReflectionHints.

Thanks

Hi @sbrannen,

After editing my sample application to use spring-core:6.2.9-SNAPSHOT and removing the reflection hint, I can confirm that my test case works as expected.

Thank you for implementing this change!

Comment From: sbrannen

Thanks, Sam. I'd overlooked the conversion in that direction being handled by the new DateToInstantConverter.

No worries, Andy. I'm always glad to see you follow up on stuff like this, in case we miss something. 👍

And admittedly, it got a bit confusing with two converters plus the runtime hint registration.

Comment From: sbrannen

After editing my sample application to use spring-core:6.2.9-SNAPSHOT and removing the reflection hint, I can confirm that my test case works as expected.

Awesome!

Thanks so much for trying out the snapshot and letting us know it works.

Thank you for implementing this change!

You're welcome.