多模块Spring项目Hibernate XML映射Kotlin不可变类报错求助
解决Hibernate XML映射外部不可变复合ID类的构造器找不到问题
问题场景
我维护一个多模块Spring项目,部分类属于外部依赖(无法修改源码),必须通过Hibernate XML映射为实体。示例实体和复合ID类采用Kotlin data class定义:
data class GeoLocationEntity( val id: GeoLocationId, ) data class GeoLocationId( val id: String, val timestamp: Instant, )
对应的Hibernate XML映射文件如下:
<?xml version = "1.0" encoding = "utf-8"?> <!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD//EN" "http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd"> <hibernate-mapping> <class entity-name="GeoLocationEntity" name="com.core.api.GeoLocationEntity" table="geo_location" mutable="false"> <composite-id name="id" class="com.core.api.GeoLocationId" access="field"> <key-property column="id" name="id"/> <key-property column="timestamp" name="timestamp"/> </composite-id> </class> </hibernate-mapping>
运行时抛出异常:
org.hibernate.InstantiationException: Unable to locate constructor for embeddable 'com.core.api.GeoLocationId' at org.hibernate.metamodel.internal.EmbeddableInstantiatorPojoStandard.instantiate(EmbeddableInstantiatorPojoStandard.java:58) at org.hibernate.type.ComponentType.deepCopy(ComponentType.java:483) at org.hibernate.event.internal.DefaultMergeEventListener.copyCompositeTypeId(DefaultMergeEventListener.java:246) ...(省略后续栈跟踪)
注:相同类用注解映射可正常运行,尝试替换为Java Record也仅能作为DTO使用,无法作为实体映射,求可行解决方案。
可行解决方案
1. 给Kotlin Data Class生成无参构造器(适用于可添加编译注解的场景)
Hibernate XML映射默认会尝试调用类的无参构造器,但Kotlin data class默认只生成带全参数的构造器。可以通过Kotlin的noarg编译插件为目标类生成无参构造器:
- 在
build.gradle.kts中引入插件:
plugins { kotlin("plugin.noarg") version "与项目匹配的Kotlin版本" } noArg { // 指定要生成无参构造器的注解,给GeoLocationId添加该注解 annotation("javax.persistence.Embeddable") }
然后给外部类GeoLocationId添加@Embeddable注解(如果有权限修改外部类源码),或者自定义一个注解并配置插件,通过编译时增强生成无参构造器。
2. 自定义Embeddable实例化器(适用于完全无法修改外部类的场景)
通过实现Hibernate的EmbeddableInstantiator接口,手动控制复合ID类的实例化逻辑:
- 编写自定义实例化器:
public class GeoLocationIdInstantiator implements EmbeddableInstantiator { @Override public Object instantiate(ValueAccess valueAccess, SessionFactoryImplementor sessionFactory) { // 按XML映射中key-property的顺序获取参数 String id = valueAccess.getValue(0, String.class); Instant timestamp = valueAccess.getValue(1, Instant.class); return new GeoLocationId(id, timestamp); } @Override public boolean isInstance(Object object, SessionFactoryImplementor sessionFactory) { return object instanceof GeoLocationId; } @Override public boolean isSameClass(Object object, SessionFactoryImplementor sessionFactory) { return object.getClass().equals(GeoLocationId.class); } }
- 在XML映射中指定该实例化器:
<composite-id name="id" class="com.core.api.GeoLocationId" access="field"> <meta attribute="instantiator">com.your.project.GeoLocationIdInstantiator</meta> <key-property column="id" name="id"/> <key-property column="timestamp" name="timestamp"/> </composite-id>
3. 升级到Hibernate 6.x版本
Hibernate 6对Kotlin data class和Java Record的兼容性大幅提升,尤其是在XML映射场景下,它能自动识别类的带参构造器并完成实例化,无需额外配置。如果项目依赖允许,升级Hibernate版本是最省心的方案。
4. 本地实体类包装外部类
如果以上方法都无法生效,可以创建本地实体类映射到数据库表,在本地类和外部类之间做转换:
@Entity @Table(name = "geo_location") class LocalGeoLocationEntity( @EmbeddedId val id: LocalGeoLocationId ) { // 转换为外部类 fun toExternal(): GeoLocationEntity { return GeoLocationEntity(GeoLocationId(id.id, id.timestamp)) } companion object { // 从外部类转换为本地实体 fun fromExternal(external: GeoLocationEntity): LocalGeoLocationEntity { return LocalGeoLocationEntity(LocalGeoLocationId(external.id.id, external.id.timestamp)) } } } @Embeddable data class LocalGeoLocationId( val id: String, val timestamp: Instant )
Repository层操作本地实体类,业务层负责与外部类的转换,隔离Hibernate映射与外部类的依赖。
内容的提问来源于stack exchange,提问作者c0ld
相关产品推荐
相关产品推荐

