Java Record作为Apache Ignite Cache键的序列化异常问题
问题描述
我尝试将Java Record类型作为Apache Ignite Cache的键使用时触发了错误,当前环境是Java 17.0.12(openjdk version "17.0.12" 2024-07-16)和GridGain Ignite 8.8.39。想知道有没有其他开发者遇到过同样的问题,以及是否必须自定义序列化处理才能使用Record类型?
错误日志
java.lang.UnsupportedOperationException: can't get field offset on a
record class: private final long
***.TestIgniteCache$TestKey.ttlInMillis at
jdk.unsupported/sun.misc.Unsafe.objectFieldOffset(Unknown Source) at
org.apache.ignite.internal.util.GridUnsafe.objectFieldOffset(GridUnsafe.java:1193)
at
org.apache.ignite.internal.binary.BinaryFieldAccessor$AbstractPrimitiveAccessor.(BinaryFieldAccessor.java:223)
at
org.apache.ignite.internal.binary.BinaryFieldAccessor$LongPrimitiveAccessor.(BinaryFieldAccessor.java:387)
at
org.apache.ignite.internal.binary.BinaryFieldAccessor.create(BinaryFieldAccessor.java:75)
at
org.apache.ignite.internal.binary.BinaryClassDescriptor.(BinaryClassDescriptor.java:368)
at
org.apache.ignite.internal.binary.BinaryClassDescriptor.(BinaryClassDescriptor.java:155)
at
org.apache.ignite.internal.binary.BinaryContext.createDescriptorForClass(BinaryContext.java:648)
at
org.apache.ignite.internal.binary.BinaryContext.descriptorForClass(BinaryContext.java:606)
at
org.apache.ignite.internal.binary.BinaryWriterExImpl.marshal0(BinaryWriterExImpl.java:181)
at
org.apache.ignite.internal.binary.BinaryWriterExImpl.marshal(BinaryWriterExImpl.java:164)
at
org.apache.ignite.internal.binary.BinaryWriterExImpl.marshal(BinaryWriterExImpl.java:151)
at
org.apache.ignite.internal.binary.GridBinaryMarshaller.marshal(GridBinaryMarshaller.java:250)
at
org.apache.ignite.internal.processors.cache.binary.CacheObjectBinaryProcessorImpl.marshalToBinary(CacheObjectBinaryProcessorImpl.java:506)
at
org.apache.ignite.internal.processors.cache.binary.CacheObjectBinaryProcessorImpl.toBinary(CacheObjectBinaryProcessorImpl.java:1360)
at
org.apache.ignite.internal.processors.cache.binary.CacheObjectBinaryProcessorImpl.toCacheKeyObject(CacheObjectBinaryProcessorImpl.java:1137)
at
org.apache.ignite.internal.processors.cache.GridCacheContext.toCacheKeyObject(GridCacheContext.java:1841)
at
org.apache.ignite.internal.processors.cache.distributed.dht.atomic.GridDhtAtomicCache$12.apply(GridDhtAtomicCache.java:476)
at
org.apache.ignite.internal.processors.cache.distributed.dht.atomic.GridDhtAtomicCache$12.apply(GridDhtAtomicCache.java:474)
at
org.apache.ignite.internal.processors.cache.distributed.dht.atomic.GridDhtAtomicCache.asyncOp(GridDhtAtomicCache.java:766)
at
org.apache.ignite.internal.processors.cache.distributed.dht.atomic.GridDhtAtomicCache.getAsync(GridDhtAtomicCache.java:474)
at
org.apache.ignite.internal.processors.cache.GridCacheAdapter.get0(GridCacheAdapter.java:4376)
at
org.apache.ignite.internal.processors.cache.GridCacheAdapter.get(GridCacheAdapter.java:4357)
at
org.apache.ignite.internal.processors.cache.GridCacheAdapter.get(GridCacheAdapter.java:1432)
at
org.apache.ignite.internal.processors.cache.IgniteCacheProxyImpl.get(IgniteCacheProxyImpl.java:1116)
at
org.apache.ignite.internal.processors.cache.GatewayProtectedCacheProxy.get(GatewayProtectedCacheProxy.java:637)
at ***.cache.TestIgniteCache.get(TestIgniteCache.java:123)
Record类定义
public static record TestKey<K>(K k, long ttlInMillis) { // 默认TTL为5分钟 private static final long defaultTTLInMillis = 300000L; /** * 指定TTL的构造方法 * @param k 键值 * @param ttlInMillis 毫秒级TTL */ public TestKey { if (Objects.isNull(k)) { throw new IllegalArgumentException("Key must not be null!"); } if (0 >= ttlInMillis) { throw new IllegalArgumentException("ttlInMillis must not be zero or negative!"); } } /** * 使用默认TTL的构造方法 * @param k 键值 */ public TestKey (K k) { this(k, defaultTTLInMillis); } /** * 获取默认TTL值 * @return 默认毫秒级TTL */ public static final long defaultTTLInMillis() { return defaultTTLInMillis; } }
问题原因
Ignite的默认二进制序列化器依赖sun.misc.Unsafe.objectFieldOffset()方法获取字段偏移量,但Java Record的字段是隐式定义的,Unsafe API不支持直接获取Record字段的偏移量,这直接导致了报错。
解决方案
有两种可行的处理方式:
1. 自定义序列化器
为Record类实现BinarySerializer接口,手动控制序列化和反序列化逻辑,绕过Unsafe的字段偏移量获取逻辑。示例代码如下:
public class TestKeyBinarySerializer implements BinarySerializer { @Override public void writeBinary(Object obj, BinaryWriter writer) throws BinaryObjectException { TestKey<?> key = (TestKey<?>) obj; writer.writeObject("k", key.k()); writer.writeLong("ttlInMillis", key.ttlInMillis()); } @Override public void readBinary(Object obj, BinaryReader reader) throws BinaryObjectException { // Record是不可变类,反序列化时通过构造方法初始化,只需按写入顺序读取所有字段 reader.readObject("k"); reader.readLong("ttlInMillis"); } }
然后在Ignite配置中注册这个序列化器:
IgniteConfiguration cfg = new IgniteConfiguration(); BinaryConfiguration binaryCfg = new BinaryConfiguration(); BinaryTypeConfiguration typeCfg = new BinaryTypeConfiguration(TestKey.class.getName()); typeCfg.setSerializer(new TestKeyBinarySerializer()); binaryCfg.setTypeConfigurations(List.of(typeCfg)); cfg.setBinaryConfiguration(binaryCfg);
2. 改用普通不可变类替代Record
将TestKey改为普通类,手动实现equals()和hashCode()方法(Ignite缓存键必须实现这两个方法保证正确性):
public static class TestKey<K> { private final K k; private final long ttlInMillis; private static final long defaultTTLInMillis = 300000L; public TestKey(K k, long ttlInMillis) { if (Objects.isNull(k)) { throw new IllegalArgumentException("Key must not be null!"); } if (0 >= ttlInMillis) { throw new IllegalArgumentException("ttlInMillis must not be zero or negative!"); } this.k = k; this.ttlInMillis = ttlInMillis; } public TestKey(K k) { this(k, defaultTTLInMillis); } public static long defaultTTLInMillis() { return defaultTTLInMillis; } public K k() { return k; } public long ttlInMillis() { return ttlInMillis; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; TestKey<?> testKey = (TestKey<?>) o; return ttlInMillis == testKey.ttlInMillis && Objects.equals(k, testKey.k); } @Override public int hashCode() { return Objects.hash(k, ttlInMillis); } }
补充说明
GridGain Ignite 8.8.x版本确实存在对Java Record的支持缺陷,后续版本可能会修复该问题。如果坚持使用Record,自定义序列化器是最直接的解决方式。
内容的提问来源于stack exchange,提问作者lmk

