Spring Boot 3 GraalVM Native镜像中DataClassRowMapper类型转换失败
问题背景
Spring Boot项目在JVM模式(./gradlew bootRun)下CRUD操作正常,但通过./gradlew clean nativeCompile生成Native镜像运行时,使用DataClassRowMapper从PostgreSQL查询数据时,出现Timestamp转Instant失败的错误:
org.postgresql.util.PSQLException: conversion to class java.time.Instant from timestamptz not supported ... Caused by: java.lang.IllegalStateException: Cannot convert value of type 'java.sql.Timestamp' to required type 'java.time.Instant' for property 'createdOn': no matching editors or conversion strategy found
当前使用的查询代码:
public Optional<ClientDto> findById(String tenantId, UUID id) { List<ClientDto> result = db.query( "SELECT * FROM clients WHERE tenant_id = ? AND id = ?", DataClassRowMapper.newInstance(ClientDto.class), tenantId, id); return result.size() == 1 ? Optional.ofNullable(result.get(0)) : Optional.empty(); }
ClientDto定义:
public record ClientDto( UUID id, String tenantId, String name, String description, Instant createdOn, Instant modifiedOn) { }
此前尝试过改用BeanPropertyRowMapper、将record改为普通类、自定义Timestamp转Instant转换器均无效,但使用ListCRUDRepository或自定义RowMapper可正常运行,目标是让DataClassRowMapper在Native镜像中正常工作。
问题根源
GraalVM Native镜像在构建时会做静态分析,未被显式注册的反射信息、类型转换器会被裁剪。DataClassRowMapper依赖Spring的类型转换系统和反射实例化DTO,但Native环境下默认没有包含Timestamp与Instant之间的转换元数据,也没有注册DTO类的反射信息,导致转换失败。
解决方案
方案1:注册类型转换器+反射元数据
创建配置类,显式注册Timestamp与Instant的转换器,并通过@RegisterReflectionForBinding注册相关类的反射信息:
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.convert.converter.Converter; import org.springframework.nativex.hint.RegisterReflectionForBinding; import java.sql.Timestamp; import java.time.Instant; @Configuration @RegisterReflectionForBinding({Timestamp.class, Instant.class, ClientDto.class}) public class NativeConversionConfig { @Bean public Converter<Timestamp, Instant> timestampToInstantConverter() { return Timestamp::toInstant; } @Bean public Converter<Instant, Timestamp> instantToTimestampConverter() { return Timestamp::from; } }
方案2:使用RuntimeHints注册Native镜像元数据
通过Spring Boot的RuntimeHintsRegistrar注册所需的反射和类型信息,确保GraalVM在构建镜像时保留这些元数据:
import org.springframework.aot.hint.MemberCategory; import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.RuntimeHintsRegistrar; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.ImportRuntimeHints; import java.sql.Timestamp; import java.time.Instant; @Configuration @ImportRuntimeHints(NativeDataClassRowMapperHints.class) public class NativeConfig { static class NativeDataClassRowMapperHints implements RuntimeHintsRegistrar { @Override public void registerHints(RuntimeHints hints, ClassLoader classLoader) { // 注册Timestamp、Instant的反射信息 hints.reflection().registerType(Timestamp.class, MemberCategory.values()); hints.reflection().registerType(Instant.class, MemberCategory.values()); // 注册ClientDto的反射信息,支持DataClassRowMapper实例化 hints.reflection().registerType(ClientDto.class, MemberCategory.values()); // 注册DataClassRowMapper的反射信息 hints.reflection().registerType(org.springframework.jdbc.core.DataClassRowMapper.class, MemberCategory.values()); } } }
方案3:确保PostgreSQL Native驱动依赖
在build.gradle中添加PostgreSQL Native驱动的运行时依赖,确保Native环境下数据库驱动正常工作:
dependencies { implementation 'org.postgresql:postgresql' runtimeOnly 'org.postgresql:postgresql-native' }
验证
重新执行./gradlew clean nativeCompile生成Native镜像,运行后测试findById操作,即可正常完成Timestamp到Instant的转换。
内容的提问来源于stack exchange,提问作者artespoon

