Apache Ignite中BinaryObjectBuilder序列化时反序列化第三方类失败问题
在Ignite缓存中存储包含org.joda.time.chrono.ISOChronology类型字段的对象,该类是带有writeReplace()和readResolve()方法的第三方类。缓存采用二进制模式(IgniteCache.withKeepBinary())并使用EntryProcessor:
- 对象在客户端通过
BinaryObjectBuilder序列化后传入处理器 - 处理器接收
BinaryObject,调用toBuilder()修改非ISOChronology字段 - 调用
BinaryObjectBuilder.build()时失败,报错:
原因是org.apache.ignite.binary.BinaryObjectException: Failed to unmarshal object with optimized marshallerbuild()操作会尝试反序列化ISOChronology字段,但该类未在服务端部署。
疑问:为何BinaryObjectBuilder在重新序列化前要反序列化原BinaryObject?已知文档提到Externalizable对象会被反序列化,但仍不清楚该行为的根本原因及规避方案。
预期结果:调用包含BinaryObject字段的BinaryObjectBuilder.build()时,无需反序列化ISOChronology字段即可成功。
实际结果:因尝试反序列化ISOChronology值而失败。
处理器实现
public class TestBinaryProcessor implements EntryProcessor<String, BinaryObject, Boolean> { @IgniteInstanceResource private Ignite ignite; private static final String MAP_FIELD_NAME = "map"; private static final String BINARY_TYPE = TestBinaryProcessor.class.getName() + ".value"; @Override public Boolean process(MutableEntry<String, BinaryObject> entry, Object... arguments) throws EntryProcessorException { BinaryObject entryValue = entry.getValue(); String key = (String) arguments[0]; BinaryObject value = (BinaryObject) arguments[1]; Map<String, BinaryObject> map = entryValue != null ? entryValue.field(MAP_FIELD_NAME) : new HashMap<>(); map.put(key, value); BinaryObjectBuilder binaryObjectBuilder = entryValue != null ? entryValue.toBuilder() : ignite.binary().builder(BINARY_TYPE); binaryObjectBuilder.setField(MAP_FIELD_NAME, map); entry.setValue(binaryObjectBuilder.build()); return true; } }
处理器调用代码
Cache<String, BinaryObject> cache = ignite.cache("default").withKeepBinary(); return cache.invoke("testMap", new TestBinaryProcessor(), "testKey", ignite.binary().toBinary(org.joda.time.DateTime.now()));
Ignite的BinaryObjectBuilder在处理包含特殊序列化逻辑的第三方类时,会触发强制反序列化:
ISOChronology实现了writeReplace()和readResolve(),这类方法会改变对象的序列化形态,Ignite的二进制序列化机制无法直接保留其原始二进制数据。- 当调用
BinaryObject.toBuilder()时,构建器需要解析原BinaryObject的所有字段内容。对于带有自定义序列化逻辑的类,Ignite无法仅通过二进制元数据完成字段复制,必须先将其反序列化为Java对象,才能在build()时重新序列化。 - 由于服务端未部署
ISOChronology类,反序列化过程抛出异常。
方案1:服务端部署第三方依赖
直接在Ignite服务端节点添加Joda Time依赖包,让服务端能够完成ISOChronology的反序列化与重新序列化操作。这是最直接的解决方案,适合允许在服务端添加依赖的场景。
方案2:自定义Binary序列化器
为ISOChronology实现专属的Ignite BinarySerializer,绕过其writeReplace()/readResolve()机制,直接序列化类的核心字段:
public class ISOChronologyBinarySerializer implements BinarySerializer { @Override public void writeBinary(Object obj, BinaryWriter writer) throws BinaryObjectException { ISOChronology chrono = (ISOChronology) obj; // 序列化核心字段,比如时区信息 writer.writeString("zone", chrono.getZone().getID()); } @Override public Object readBinary(Object obj, BinaryReader reader) throws BinaryObjectException { // 从二进制数据重建ISOChronology String zoneId = reader.readString("zone"); return ISOChronology.getInstance(DateTimeZone.forID(zoneId)); } }
然后在Ignite配置中注册该序列化器:
IgniteConfiguration cfg = new IgniteConfiguration(); BinaryConfiguration binaryCfg = new BinaryConfiguration(); binaryCfg.setSerializers(Map.of(ISOChronology.class, new ISOChronologyBinarySerializer())); cfg.setBinaryConfiguration(binaryCfg);
这样Ignite就能直接处理ISOChronology的二进制数据,无需触发反序列化。
方案3:转换为Ignite原生支持的类型
在客户端将ISOChronology字段转换为Ignite原生支持的类型(如字符串、长整型),避免直接存储第三方类对象。例如:
- 存储时:将
ISOChronology转换为时区ID字符串 - 读取时:在客户端从时区ID重建
ISOChronology
这种方式无需服务端依赖第三方类,完全通过原生类型交互。
方案4:绕过toBuilder(),手动构建BinaryObject
修改EntryProcessor逻辑,不使用toBuilder(),而是手动创建新的BinaryObjectBuilder并复制所有字段,仅修改目标字段:
@Override public Boolean process(MutableEntry<String, BinaryObject> entry, Object... arguments) throws EntryProcessorException { BinaryObject entryValue = entry.getValue(); String key = (String) arguments[0]; BinaryObject value = (BinaryObject) arguments[1]; Map<String, BinaryObject> map = entryValue != null ? entryValue.field(MAP_FIELD_NAME) : new HashMap<>(); map.put(key, value); BinaryObjectBuilder builder = ignite.binary().builder(BINARY_TYPE); if (entryValue != null) { // 手动复制所有字段,跳过反序列化风险的字段 for (String fieldName : entryValue.type().fieldNames()) { if (!MAP_FIELD_NAME.equals(fieldName)) { // 直接以二进制形式复制字段 builder.setField(fieldName, entryValue.rawField(fieldName)); } } } builder.setField(MAP_FIELD_NAME, map); entry.setValue(builder.build()); return true; }
这种方式通过rawField()直接获取字段的二进制数据,避免触发反序列化,但需要明确所有字段名,扩展性较差。
内容的提问来源于stack exchange,提问作者Igal

