序列化框架的选型和比对

简介: 序列化框架的选型和比对

序列化通信


将对象转换为字节数组,方便在网络中进行对象的传输。在网络通信中,不同的计算机进行相互通信主要的方式就是将数据流从一台机器传输给另外一台计算机,常见的传输协议包括了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));
    }
}


jdk序列化的缺点


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,因此相应的类会多很多)



image.png



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


image.png


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无法支持)


不同序列化框架的总结


image.png


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


相关文章
|
6月前
|
存储 消息中间件 缓存
RocketMQ 5.0 分级存储背后技术优化包含那几个方面
RocketMQ 5.0 分级存储背后的技术优化是一个综合性的系统工程,需要考虑多个方面,包括存储介质的选择、数据读写策略的设计、数据压缩和解压缩技术的引入、自动的数据分级和迁移机制的实现,以及高可用性和容错性的保证等。这些技术优化的目的是为了实现存储成本和性能的最优平衡,提高系统的可靠性和可用性,满足大规模数据处理的需求。
179 1
|
6月前
|
存储 缓存 NoSQL
Redis缓存设计典型问题
缓存穿透 缓存穿透是指查询一个根本不存在的数据, 缓存层和存储层都不会命中, 通常出于容错的考虑, 如果从存储层查不到数据则不写入缓存层。缓存穿透将导致不存在的数据每次请求都要到存储层去查询, 失去了缓存保护后端存储的意义。
|
开发框架 前端开发 网络协议
使用 DataAnnotations(数据注解)实现模型的通用数据校验
在实际项目开发中,无论任何方式、任何规模的开发模式,项目中都离不开对接入数据模型参数的合法性校验,目前普片的开发模式基本是前后端分离,当用户在前端页面中输入一些表单数据时,点击提交按钮,触发请求目标服务器的一系列后续操作,在这中间的执行过程中(标准做法推荐)无论...
43447 1
使用 DataAnnotations(数据注解)实现模型的通用数据校验
|
3月前
|
存储 C# 数据库
解决C#对Firebase数据序列化失败的难题
在游戏开发中,Unity结合Firebase实时数据库为开发者提供强大支持,但在C#中进行数据序列化和反序列化时常遇难题。文章剖析了数据丢失或反序列化失败的原因,并给出解决方案,包括使用`JsonUtility`、确保字段标记为`[Serializable]`以及正确配置网络请求。示例代码演示了如何在Unity环境中实现Firebase数据的序列化和反序列化,并通过设置代理IP、Cookies和User-Agent来增强网络请求的安全性。这些技巧有助于确保数据完整传输,提升开发效率。
解决C#对Firebase数据序列化失败的难题
|
4月前
|
存储 数据格式 运维
开发与运维C++问题之更改数据模型为通用数据结构如何解决
开发与运维C++问题之更改数据模型为通用数据结构如何解决
29 1
|
4月前
|
存储 JSON 数据格式
通用研发提效问题之存储模型选择存储方式,如何解决
通用研发提效问题之存储模型选择存储方式,如何解决
|
4月前
|
缓存 Java
浅析JAVA日志中的性能实践与原理解释问题之AsyncAppender的配置方式的问题是如何解决的
浅析JAVA日志中的性能实践与原理解释问题之AsyncAppender的配置方式的问题是如何解决的
|
6月前
|
XML JSON 分布式计算
如何选择序列化协议:关键因素与场景分析
如何选择序列化协议:关键因素与场景分析
64 0
|
存储 SQL 分布式计算
从源码看Velox如何做序列化
从源码角度分析Velox做序列化和反序列化的过程
905 0
|
存储 缓存 安全
通用缓存存储设计实践
通用缓存存储设计实践
4684 3
通用缓存存储设计实践
下一篇
无影云桌面