将Java对象序列化为固定宽度字节数组的库

5

我想要将一个非常简单的pojo对象以二进制格式存储:

public class SampleDataClass {
    private long field1;
    private long field2;
    private long field3;
}

为了实现这个功能,我编写了一对简单的序列化/反序列化方法:
public class SampleDataClass {

    // ... Fields as above        

    public static void deserialize(ByteBuffer buffer, SampleDataClass into) {
        into.field1 = buffer.getLong();
        into.field2 = buffer.getLong();
        into.field3 = buffer.getLong();
    }

    public static void serialize(ByteBuffer buffer, SampleDataClass from) {
        buffer.putLong(from.field1);
        buffer.putLong(from.field2);
        buffer.putLong(from.field3);
    }
}

简单高效,最重要的是二进制格式中对象的大小是固定的。我知道每个记录序列化后的大小将是3 x long,即3 x 8字节=24字节。
这一点非常关键,因为我将按顺序记录它们,并且稍后需要通过索引找到它们,例如“找到第127条记录”。
对我来说,这很有效,但我讨厌样板代码 - 而且在某些时候,我会犯错误,导致写入无法读取的数据,因为我的序列化/反序列化方法之间存在不一致性。
是否有一个库可以为我生成类似protobuf的东西?
理想情况下,我正在寻找像protobuf一样具有固定长度编码方案的东西。稍后,我还想编码字符串。这些也将具有固定长度。如果字符串超过长度,则将其截断为n个字节。如果字符串太短,我将以null结尾(或类似方式)。
最后,protobuf支持协议的不同版本。我迟早需要做到这一点。
我希望在开始自己编写之前,有人能提出建议。

我已经查看了Cap'n Proto,但是(A)它还没有准备好用于生产环境,(B)目前它只对C++有可靠的支持。 - jwa
3个回答

0
这里最困难的部分是对字符串或集合进行限制。您可以通过覆盖默认序列化程序,使用Kryo来对字符串进行限制。将字符串放入自定义缓冲类(即FixedSerializableBuffer),该类存储或带有要截取的长度也是一种明智的选择。
public class KryoDemo {
    static class Foo{
        String s;
        long v;

        Foo() {
        }

        Foo(String s, long v) {
            this.s = s;
            this.v = v;
        }

        @Override
        public String toString() {
            final StringBuilder sb = new StringBuilder("Foo{");
            sb.append("s='").append(s).append('\'');
            sb.append(", v=").append(v);
            sb.append('}');
            return sb.toString();
        }
    }

    public static void main(String[] args) {
        Kryo kryo = new Kryo();

        Foo foo = new Foo("test string", 1);

        kryo.register(String.class, new Serializer<String>() {
            {
                setImmutable(true);
                setAcceptsNull(true);
            }

            public void write(Kryo kryo, Output output, String s) {
                if (s.length() > 4) {
                    s = s.substring(0, 4);
                }

                output.writeString(s);
            }

            public String read(Kryo kryo, Input input, Class<String> type) {
                return input.readString();
            }
        });

        // serialization part, data is binary inside this output
        ByteBufferOutput output = new ByteBufferOutput(100);

        kryo.writeObject(output, foo);

        System.out.println("before: " + foo);
        System.out.println("after: " + kryo.readObject(new Input(output.toBytes()), Foo.class));
    }
}

这将打印:

before: Foo{s='test string', v=1}
after: Foo{s='test', v=1}

0
让你的类继承java.io.Serializable接口。然后,您可以使用java.io.ObjectOutputStreamjava.io.ObjectInputStream将对象序列化/反序列化到/从流中。 writeread方法将byte数组作为参数。为了使其长度固定,请标准化所使用的byte[]数组的大小。

1
要使其成为固定大小,我该如何计算它需要的字节数?这是否必须是硬编码的?如果是的话,我仍然需要管理那个样板文件。这可能比手工编码一切都更进一步,但我仍然需要一个处理版本控制的方法。此外,根据您的建议,我假设ObjectInputStream将能够处理由“较短”对象留下的尾随零吗? - jwa
1
现在我想起来了,你也可以使用反射来确定对象中字段的大小,从而避免在序列化器中硬编码值。 - Bizmarck
最后一点,我假设这依赖于反射?因此它会比硬编码的编码慢?我将每秒记录数千个数据点,速度是一个问题。 - jwa
不确定类定义是否更改会导致对象大小改变的情况。但是反射只需要一次,之后可以重复使用读取的值。 - Bizmarck
Java内置序列化的问题在于它非常慢:jvm-serializers - Andrey Chaschev
显示剩余4条评论

0
如果除了标准序列化之外的唯一附加要求是对第n个条目进行有效的随机访问,则存在替代固定大小条目的方案。您将存储可变长度条目(例如字符串),这使我认为这些替代方案值得考虑。
其中一种替代方案是使用具有固定长度条目的“目录”,每个条目指向可变长度内容。然后通过从目录中读取相应的指针(可以使用随机访问进行,因为目录条目具有固定大小)并读取其指向的块来实现对条目的随机访问。此方法的缺点是需要额外的I/O访问才能访问数据,但允许更紧凑的数据表示,因为您不必填充可变长度内容,从而加快了顺序读取速度。当然,问题和上述解决方案都不是新颖的 - 文件系统已经存在很长时间了...

我明白你的意思。我的需求实际上需要顺序读取,即读取第20-50个元素。固定宽度的优点在于它们(硬盘片碎片化除外)理论上是按顺序存储在磁盘上的。针对表进行查找至少需要一定量的寻道操作,返回目录,然后返回数据块。 - jwa
不一定。您可以将目录保存在单独的文件中,或使用单独的文件描述符从目录中读取。这将导致操作系统缓存当前目录和当前数据块,加快顺序读取速度。 - meriton
或者 - 如果您事先知道范围 - 您可以首先读取目录的相关部分,然后按顺序读取数据条目。 - meriton
好的建议,感谢澄清。 - jwa

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接