序列化框架的选型和比对

3,807 阅读8分钟

序列化通信

将对象转换为字节数组,方便在网络中进行对象的传输。在网络通信中,不同的计算机进行相互通信主要的方式就是将数据流从一台机器传输给另外一台计算机,常见的传输协议包括了TCP,UDP,HTTP等,网络io的方式主要包括有了aio,bio,nio三种方式。

当客户端将需要请求的数据封装好了之后就需要进行转换为二进制格式再转换为流进行传输,当服务端接收到流之后再将数据解析为二进制格式的内容,再按照约定好的协议进行处理解析。最常见的场景就是rpc远程调用的时候,对发送数据和接收数据时候的处理。

下边我们来一一介绍一下现在比较常见的几款序列化技术框架。

jdk序列化

jdk自身便带有序列化的功能,Java序列化API允许我们将一个对象转换为流,并通过网络发送,或将其存入文件或数据库以便未来使用,反序列化则是将对象流转换为实际程序中使用的Java对象的过程。

先来看看实际的代码案例

首先我们创建一个基础的测试Person类

package com.sise.test;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;

/**
 * @author linhao
 * @date 2019/8/15
 * @Version V1.0
 */
@AllArgsConstructor
@NoArgsConstructor
@Data
public class Person implements Serializable {
    private static final long serialVersionUID = 3829252771168681281L;

    private Integer id;

    private String username;

    private String tel;
}

如果某些特殊字段不希望被序列化该如何处理?

这里面如果有相应的属性不希望被序列化操作的话,可以使用transient关键字进行修饰,例如希望tel属性不希望被序列化,可以改成这样:

  private transient String tel;

这样的话,该对象在反序列化出来结果之后,相应的属性就会为null值。

为什么要定义serialVersionUID?

序列化操作时,系统会把当前类声明的serialVersionUID写入到序列化文件中,用于反序列化时系统会去检测文件中的serialVersionUID,判断它是否与当前类的serialVersionUID一致,如果一致就说明序列化类的版本与当前类版本是一样的,可以反序列化成功,否则失败。

如果没有定义serialVersionUID时

当实现当前类没有显式地定义一个serialVersionUID变量时候,Java序列化机制会根据编译的Class自动生成一个serialVersionUID作序列化版本比较用,这种情况下,如果类信息进行修改,会导致反序列化时serialVersionUID与原先值无法match,反序列化失败。

通过jdk提升的序列化对其进行相应的序列化和反序列化的代码案例

package com.sise.test.jdk;


import com.sise.test.Person;

import java.io.IOException;

/**
 * @author idea
 * @date 2019/8/15
 * @Version V1.0
 */
public class SerializationTest {

    /**
     *
     * @param args
     * @throws IOException
     */
    public static void main(String[] args) throws IOException, ClassNotFoundException {

        long begin = System.currentTimeMillis();
        for (int i = 0; i < 2000; i++) {
            String fileName = "test-person.txt";
            Person person = new Person();
            person.setId(1);
            person.setTel("99562352");
            person.setUsername("idea");
            SerializationUtil.serialize(person, fileName);
            Person newPerson = (Person) SerializationUtil.deserialize(fileName);
        }
        long end = System.currentTimeMillis();
        System.out.println("耗时:" + (end - begin));
    }
}

1、无法跨语言

这一缺点几乎是致命伤害,对于跨进程的服务调用,通常都需要考虑到不同语言的相互调用时候的兼容性,而这一点对于jdk序列化操作来说却无法做到。这是因为jdk序列化操作时是使用了java语言内部的私有协议,在对其他语言进行反序列化的时候会有严重的阻碍。

2、序列化之后的码流过大

jdk进行序列化编码之后产生的字节数组过大,占用的存储内存空间也较高,这就导致了相应的流在网络传输的时候带宽占用较高,性能相比较为低下的情况。

Hessian序列化框架

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

相关的代码案例:

package com.sise.test.hessian;

import com.caucho.hessian.io.HessianInput;
import com.caucho.hessian.io.HessianOutput;
import com.sise.test.Person;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;

/**
 * @author idea
 * @date 2019/8/15
 * @Version V1.0
 */
public class HessianTest {

    /**
     *
     * @param args
     * @throws IOException
     */
    public static void main(String[] args) throws IOException {
        long begin = System.currentTimeMillis();
        for (int i = 0; i < 2000; i++) {
            Person person = new Person();
            person.setId(1);
            person.setUsername("idea");
            person.setTel("99562352");
            ByteArrayOutputStream os = new ByteArrayOutputStream();
            HessianOutput ho = new HessianOutput(os);
            ho.writeObject(person);
            byte[] userByte = os.toByteArray();
            ByteArrayInputStream is = new ByteArrayInputStream(userByte);
            //Hessian的反序列化读取对象
            HessianInput hi = new HessianInput(is);
            Person newPerson = (Person) hi.readObject();
        }
        long end = System.currentTimeMillis();
        System.out.println("耗时:" + (end - begin));
    }
}

Hessian的源码里面,核心主要还是com.caucho.hessian.io里面的代码,AbstractSerializer是Hessian里面的核心序列化类,当我们仔细查看源码的时候就会发现hessian提供了许多种序列化和反序列化的类进行不同类型数据的处理。(我使用的是hessian4.0,因此相应的类会多很多)

序列化框架的选型和比对


在SerializerFactory里面有getSerializer和getDefaultSerializer的函数,专门用于提取这些序列化和反序列化的工具类,这样可以避免在使用该工具类的时候又要重新实例化,这些工具类都会被存储到不同的ConcurrentHashMap里面去。


序列化框架的选型和比对


ps:对于hessian3.0时候的Serializer/Derializer实现功能没有考虑到对于异常信息进行序列化处理,因此如果遇到相应问题的朋友可以考虑将hessian的版本提升到3.1.5以上。

Kryo序列化技术

Kryo是一种非常成熟的序列化实现,已经在Twitter、Groupon、 Yahoo以及多个著名开源项目(如Hive、Storm)中广泛的使用,它的性能在各个方面都比hessian2要优秀些,因此dubbo后期也开始渐渐引入了使用Kryo进行序列化的方式。

对于kryo的使用,我们来看看相应代码:

首先我们引入相应的依赖:

<dependency>
            <groupId>com.esotericsoftware</groupId>
            <artifactId>kryo-shaded</artifactId>
            <version>3.0.3</version>
        </dependency>

然后就是基础的序列化和反序列化代码操作了

package com.sise.test.kryo;

import com.esotericsoftware.kryo.Kryo;
import com.esotericsoftware.kryo.io.Input;
import com.esotericsoftware.kryo.io.Output;
import com.sise.test.Person;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;

/**
 * @author idea
 * @date 2019/8/15
 * @Version V1.0
 */
public class KryoTest {

    public static void main(String[] args) throws FileNotFoundException {
        Kryo kryo=new Kryo();
        Output output = new Output(new FileOutputStream("person.txt"));
        Person person=new Person();
        person.setId(1);
        person.setUsername("idea");
        kryo.writeObject(output, person);
        output.close();
        Input input = new Input(new FileInputStream("person.txt"));
        Person person1 = kryo.readObject(input, Person.class);
        input.close();
        System.out.println(person1.toString());
        assert "idea".equals(person1.getUsername());
    }
}

ps:

这里我们需要注意,Kryo不支持没有无参构造函数的对象进行反序列化,因此如果某个对象希望使用Kryo来进行序列化操作的话,需要有相应的无参构造函数才可以。

由于Kryo不是线程安全,因此当我们希望使用Kryo构建的工具类时候,需要在实例化的时候注意线程安全的问题。代码案例:

package com.sise.test.kryo;

import com.esotericsoftware.kryo.Kryo;
import com.esotericsoftware.kryo.io.Input;
import com.esotericsoftware.kryo.io.Output;
import com.sise.test.Person;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;

/**
 * @author idea
 * @data 2019/8/17
 */
public class KryoUtils {


    public byte[] serialize(Object obj){
        Kryo kryo = kryos.get();
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        Output output = new Output(byteArrayOutputStream);
        kryo.writeClassAndObject(output, obj);
        output.close();
        return byteArrayOutputStream.toByteArray();
    }

    public <T> T deserialize(byte[] bytes) {
        Kryo kryo = kryos.get();
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
        Input input = new Input(byteArrayInputStream);
        input.close();
        return (T) kryo.readClassAndObject(input);
    }


    private static final ThreadLocal<Kryo> kryos=new ThreadLocal<Kryo>(){
        @Override
        protected Kryo initialValue(){
            Kryo kryo=new Kryo();
            return kryo;
        }

    };


    public static void main(String[] args) {
        KryoUtils kryoUtils=new KryoUtils();
        for(int i=0;i<1000;i++){
            Person person=new Person(1,"idea");
            byte[] bytes=kryoUtils.serialize(person);
            Person newPerson=kryoUtils.deserialize(bytes);
            System.out.println(newPerson.toString());
        }
    }
}

XStream实现对象的序列化

在使用XStream进行序列化技术的实现过程中,类中的字符串组成了 XML 中的元素内容,而且该对象还不需要实现 Serializable 接口。XStream不关心被序列化/反序列化的类字段的可见性,该对象也不需要有getter/setter方法和默认的构造函数。

引入的依赖:

<dependency>
            <groupId>com.thoughtworks.xstream</groupId>
            <artifactId>xstream</artifactId>
            <version>1.4.9</version>
        </dependency>

通过使用XStream来对对象进行序列化和反序列化操作:

package com.sise.test.xstream;


import com.sise.test.Person;
import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.io.xml.DomDriver;

/**
 * @author idea
 * @date 2019/8/15
 * @Version V1.0
 */
public class XStreamTest {


    private static XStream xStream;

    static {
        xStream = new XStream(new DomDriver());
        /*
         * 使用xStream.alias(String name, Class Type)为任何一个自定义类创建到类到元素的别名
         * 如果不使用别名,则生成的标签名为类全名
         */
        xStream.alias("person", Person.class);
    }

    //xml转java对象
    public static Object xmlToBean(String xml) {
        return xStream.fromXML(xml);
    }

    //java对象转xml
    public static String beanToXml(Object obj) {
        return "<?xml version="1.0" encoding="UTF-8"?>n" + xStream.toXML(obj);
    }

    /**
     *
     * @param args
     */
    public static void main(String[] args) {
        long begin = System.currentTimeMillis();
        for (int i = 0; i < 2000; i++) {
            Person person = new Person();
            person.setId(1);
            person.setUsername("idea");
            String xml = XStreamTest.beanToXml(person);
            Person newPerson = (Person) XStreamTest.xmlToBean(xml);
        }
        long end = System.currentTimeMillis();
        System.out.println("耗时:" + (end - begin));
    }
}

google的Protobuf

google protobuf是一个灵活的、高效的用于序列化数据的协议。相比较XML和JSON格式,protobuf更小、更快、更便捷。google protobuf是跨语言的,并且自带了一个编译器(protoc),只需要用它进行编译,可以编译成Java、python、C++、C#、Go等代码,然后就可以直接使用,不需要再写其他代码,自带有解析的代码。
protobuf相对于kryo来说具有更加高效的性能和灵活性,能够在实际使用中,当对象序列化之后新增了字段,在反序列化出来的时候依旧可以正常使用。(这一点kryo无法支持)

不同序列化框架的总结

序列化框架的选型和比对

目前已有的序列化框架还有很多在文中没有提到,日后假若在开发中遇到的时候可以适当的进行归纳总结,比对各种不同的序列化框架之间的特点。


推荐阅读(点击即可跳转阅读)

1. SpringBoot内容聚合

2. 面试题内容聚合

3. 设计模式内容聚合

4. Mybatis内容聚合

5. 多线程内容聚合