You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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 marshaller
    
    原因是build()操作会尝试反序列化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在处理包含特殊序列化逻辑的第三方类时,会触发强制反序列化:

  1. ISOChronology实现了writeReplace()和readResolve(),这类方法会改变对象的序列化形态,Ignite的二进制序列化机制无法直接保留其原始二进制数据。
  2. 当调用BinaryObject.toBuilder()时,构建器需要解析原BinaryObject的所有字段内容。对于带有自定义序列化逻辑的类,Ignite无法仅通过二进制元数据完成字段复制,必须先将其反序列化为Java对象,才能在build()时重新序列化。
  3. 由于服务端未部署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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.23 08:45:01