聊聊序列化–选型


聊聊序列化–选型


前文我们介绍的是JDK自带的序列化框架,如果是Java系的代码,都用这个序列化框架实现简单,使用方便。但是在实际生成环境中,难免会有跨语言的情况,而这种情况,jdk自带的序列化框架是不满足的。另外,jdk进行序列化编码之后产生的字节数组过大,占用的存储内存空间也较高,这就导致了相应的流在网络传输的时候带宽占用较高,性能相比较为低下的情况。

除了原生的序列化之外,常见的还有protobuf、json、xml、hessian等

  • JSON:不多说了,用途广泛,序列化方式还衍生了阿里的fastjson,美团的MSON,谷歌的GSON等更加优秀的转码工具。优点:使用方便。缺点:数据冗长,转码性能一般。
  • XML:很久之前的转码方法了,现在用的不多。优点:暂时没发现。缺点:数据冗长,转码性能一般。
  • Serialzable:JDK自带的序列化。优点:使用方便。缺点:转码性能低下。
  • hessian:基于 binary-RPC实现的远程通讯library,使用二进制传输数据。优点:数据长度小。缺点:性能低下。
  • protobuf:谷歌公司出的一款开源项目,性能好,效率高,并且支持多种语言,例如:java,C++,Python等。 优点:转码性能高,支持多语言。 缺点:中文文档少,使用相对复杂。

Hessian

Hessian是一款支持多种语言进行序列化操作的框架技术,同时在进行序列化之后产生的码流也较小,处理数据的性能方面远超于java内置的jdk序列化方式。

hessian序列化比Java序列化高效很多,而且生成的字节流也要短很多。

为了与jdk原生的进行区别,这边新建一个HUser对象,但是内容基本是一致的,而且也需要实现Serializable接口

引入依赖:

        <dependency>
            <groupId>com.caucho</groupId>
            <artifactId>hessian</artifactId>
            <version>4.0.7</version>
        </dependency>
public class HUser implements Serializable{

    private int id;

    private String name;
    private transient int age;

    public HUser(String name, int id, int age) {
        this.name = name;
        this.id=id;
        this.age=age;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public HUser(String name, int id) {
        this.name = name;
        this.id = id;
    }

    public HUser() {

    }

    public int getId() {
        return id;
    }
    public void setId(int id) {
        this.id = id;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "HUser{" +
                "id=" + id +
                ", name='" + name + ''' +
                ", age=" + age +
                '}';
    }
}

测试类,与原先的测试类类似,将序列化后的对象存储在H.txt

public class HessianTest {
    public static void main(String[] args) throws IOException {
        SerializeUser();// 序列化User对象
        HUser user = DeserializeUser();// 反序列User对象
        System.out.println("序列化之前:user:"+user);
//        List list = new ArrayList();
    }

    private static HUser DeserializeUser() throws IOException {
        FileInputStream fileInputStream = new FileInputStream("D:/H.txt");
        byte[] data = new byte[1024];
        int len = fileInputStream.read(data);
        // 从流中读出对象
        ByteArrayInputStream is = new ByteArrayInputStream(data);
        Hessian2Input input = new Hessian2Input(is);
        HUser user = (HUser) input.readObject();
        System.out.println("User对象反序列化成功!");
        return user;
    }

    private static void SerializeUser() throws IOException {
        HUser user = new HUser("Jason",25,22);
        System.out.println("序列化之前:user:"+user);
        FileOutputStream fileOutputStream = new FileOutputStream("D:/H.txt");
        // 从对象中获取字节流
        ByteArrayOutputStream os = new ByteArrayOutputStream();
        Hessian2Output output = new Hessian2Output(os);
        output.writeObject(user);
        output.getBytesOutputStream().flush();
        output.completeMessage();
        output.close();
        fileOutputStream.write(os.toByteArray());
        System.out.println("User对象序列化成功!");
    }
}

输出:

序列化之前:user:HUser{id=25, name='Jason', age=22}
User对象序列化成功!
User对象反序列化成功!
序列化之前:user:HUser{id=25, name='Jason', age=0}

我们可以发现生成的文件,原先的T.txt是228字节,而H.txt只有112字节,确实是要更加的小。

需要注意的是:

需要序列化的对象还是需要实现java原生的Serializable 接口,如果不这么做的话会报错,参考如下源码:

// com.caucho.hessian.io.SerializerFactory 序列化时会获取默认序列化器
protected Serializer getDefaultSerializer(Class cl) {
  if (_defaultSerializer != null)
    return _defaultSerializer;

  // 若序列化对象没有实现 Serializable 接口,则会抛出IllegalStateException
  if (! Serializable.class.isAssignableFrom(cl)
      && ! _isAllowNonSerializable
{
    throw new IllegalStateException("Serialized class " + cl.getName() + " must implement java.io.Serializable");
  }

  if (_isEnableUnsafeSerializer
      && JavaSerializer.getWriteReplace(cl) == null) {
    return UnsafeSerializer.create(cl);
  }
  else
    return JavaSerializer.create(cl);
}

若序列化对象经hessian序列化后,序列化对象中不加serialVersionUID时,再改变(增加对象属性、删除对象属性)都不会产生反序列化异常,即hessian序列化对象不再需要serialVersionUID。hessian会把复杂对象所有属性存储在一个 Map 中进行序列化。所以在父类、子类存在同名成员变量的情况下, Hessian 序列化时,先序列化子类,然后序列化父类,因此反序列化结果会导致子类同名成员变量被父类的值覆盖。hessian中的writeReplace方法与readResolve方法的作用一样,如果序列化的对象具有此方法,会利用此方法返回的实例来代替序列化后实例,用以保证对象的单例性。

Protobuf

Protobuf 是由 Google 推出且支持多语言的序列化框架,目前在主流网站上的序列化框架性能对比测试报告中,Protobuf 无论是编解码耗时,还是二进制流压缩大小,都名列前茅。

Protobuf 以一个 .proto 后缀的文件为基础,这个文件描述了字段以及字段类型,通过工具可以生成不同语言的数据结构文件。在序列化该数据对象的时候,Protobuf 通过.proto 文件描述来生成 Protocol Buffers 格式的编码。

Protocol Buffers 是一种轻便高效的结构化数据存储格式。它使用 T-L-V(标识 – 长度 – 字段值)的数据格式来存储数据,T 代表字段的正数序列 (tag),Protocol Buffers 将对象中的每个字段和正数序列对应起来,对应关系的信息是由生成的代码来保证的。在序列化的时候用整数值来代替字段名称,于是传输流量就可以大幅缩减;L 代表 Value 的字节长度,一般也只占一个字节;V 则代表字段值经过编码后的值。这种数据格式不需要分隔符,也不需要空格,同时减少了冗余字段名。

Protobuf 定义了一套自己的编码方式,几乎可以映射 Java/Python 等语言的所有基础数据类型。不同的编码方式对应不同的数据类型,还能采用不同的存储格式。如下图所示:

对于存储 Varint 编码数据,由于数据占用的存储空间是固定的,就不需要存储字节长度 Length,所以实际上 Protocol Buffers 的存储方式是 T – V,这样就又减少了一个字节的存储空间。

Protobuf 定义的 Varint 编码方式是一种变长的编码方式,每个字节的最后一位 (即最高位) 是一个标志位 (msb),用 0 和 1 来表示,0 表示当前字节已经是最后一个字节,1 表示这个数字后面还有一个字节。

对于 int32 类型数字,一般需要 4 个字节表示,若采用 Varint 编码方式,对于很小的 int32 类型数字,就可以用 1 个字节来表示。对于大部分整数类型数据来说,一般都是小于 256,所以这种操作可以起到很好地压缩数据的效果。

我们知道 int32 代表正负数,所以一般最后一位是用来表示正负值,现在 Varint 编码方式将最后一位用作了标志位,那还如何去表示正负整数呢?如果使用 int32/int64 表示负数就需要多个字节来表示,在 Varint 编码类型中,通过 Zigzag 编码进行转换,将负数转换成无符号数,再采用 sint32/sint64 来表示负数,这样就可以大大地减少编码后的字节数。

Protobuf 的这种数据存储格式,不仅压缩存储数据的效果好, 在编码和解码的性能方面也很高效。Protobuf 的编码和解码过程结合.proto 文件格式,加上 Protocol Buffer 独特的编码格式,只需要简单的数据运算以及位移等操作就可以完成编码与解码。可以说 Protobuf 的整体性能非常优秀。

原始的ProtoBuff需要自己写.proto文件,通过编译器将其转换为java文件,显得比较繁琐。百度研发的jprotobuf框架将Google原始的protobuf进行了封装,对其进行简化,仅提供序列化和反序列化方法。其实用上也比较简洁,通过对JavaBean中的字段进行注解就行,不需要撰写.proto文件和实用编译器将其生成.java文件,百度的jprotobuf都替我们做了这些事情了。

引入依赖:

        <dependency>
            <groupId>com.baidu</groupId>
            <artifactId>jprotobuf</artifactId>
            <version>2.4.9</version>
        </dependency>

实体类改造:

@ProtobufClass
public class PUser{
    @Protobuf(fieldType = FieldType.INT32, order = 1, required = true)
    private int id;

    @Protobuf(fieldType = FieldType.STRING, order = 2, required = true)
    private String name;
    @Protobuf(fieldType = FieldType.INT32, order = 3, required = true)
    private int age;

    public PUser(String name, int id, int age) {
        this.name = name;
        this.id=id;
        this.age=age;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public PUser(String name, int id) {
        this.name = name;
        this.id = id;
    }

    public PUser() {

    }

    public int getId() {
        return id;
    }
    public void setId(int id) {
        this.id = id;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "PUser{" +
                "id=" + id +
                ", name='" + name + ''' +
                ", age=" + age +
                '}';
    }

}

测试代码:

public class ProtobufTest {
    public static final Codec<PUser> simpleTypeCodec = ProtobufProxy
            .create(PUser.class);
    public static void main(String[] args) throws IOException {
        SerializeUser();// 序列化User对象
        PUser user = DeserializeUser();// 反序列User对象
        System.out.println("序列化之后:puser:"+user);

    }

    private static PUser DeserializeUser() throws IOException {
        FileInputStream fileInputStream = new FileInputStream("D:/P.txt");
        int dataNum = 0;
        StringBuilder sb = new StringBuilder();
        for (;;) {
            int n = fileInputStream.read(); // 反复调用read()方法,直到返回-1
            if (n == -1) {
                break;
            }
            sb.append((char)n);
            dataNum++;
            System.out.println(n); // 打印byte的值
        }
        byte[] data = sb.toString().getBytes();
        System.out.println("PUser对象 data[len]:"+ Arrays.toString(data));

        PUser user =simpleTypeCodec.decode(data);
        System.out.println("PUser对象反序列化成功!");
        return user;
    }

    private static void SerializeUser() throws IOException {
        PUser user = new PUser("Jason",25,22);
        System.out.println("序列化之前:Puser:"+user);
        FileOutputStream fileOutputStream = new FileOutputStream("D:/P.txt");

        // 序列化
        byte[] bb = simpleTypeCodec.encode(user);
        System.out.println("PUser对象 byte[len]:"+Arrays.toString(bb));
        fileOutputStream.write(bb);
        System.out.println("PUser对象序列化成功!");
    }
}

输出:

序列化之前:Puser:PUser{id=25, name='Jason', age=22}
PUser对象 byte[len]:[8, 25, 18, 5, 74, 97, 115, 111, 110, 24, 22]
PUser对象序列化成功!
PUser对象 data[len]:[8, 25, 18, 5, 74, 97, 115, 111, 110, 24, 22]
PUser对象反序列化成功!
序列化之后:puser:PUser{id=25, name='Jason', age=22}


原文始发于微信公众号(云户):聊聊序列化–选型

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/25867.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!