RPC 框架的常用序列化方案详解

数据在计算机中的用途分为存储、运输和运算三种,序列化技术关注的重点就是数据的存储和运输。

  • 序列化是指将数据按照规定的格式进行重组,在保持数据语义不变的情况下,方便数据的存储和运输。
  • 反序列化就是将序列化后的数据还原成具有语义的数据。

1 序列化方案分类

1.1 文本格式 & 二进制格式

从数据的整体格式上看,序列化实现方案分为文本格式二进制格式两种。

1.2 二进制格式:自描述 & 非自描述

对于二进制格式的序列化方案,还可以根据是否依赖外部描述文件划分为自描述型方案非自描述型方案两种。

大多数编程语言是有数据类型的概念的,以 Java 为例,整形数据类型为 Integer,字符串类型为 String,反序列化还原语义的时候,这些内容是必不可缺的,所以在序列化过程中不止需要编排数据,还需要编排数据的类型。

  • 自述型序列化方案的解决办法是在编码过程中添加数据类型的元数据,例如 Hessian 序列化方案会将 Integer a = 2 编排成 I 2,用元数据 I 表示 Integer 数据类型。
  • 非自述型序列化方案的解决办法是引入外部描述文件。例如 Protocol Buffer 需要用户事先定义 *.protp 文件指导序列化和反序列化过程。

2 文本格式序列化方案

文本格式的序列化方案是指将数据用某种文本格式呈现的方案,目前应用最广的文本格式有 XML 和 JSON 两种。

2.1 XML 序列化方案

2.1.1 XML 简介

XML (Extensible Markup Language) 是由 W3C 定义的一种可展标记语音。它是经人们几十年的探索才最终形成的通用编码语言。自从人们开始关注电子数据的文档结构和后,通用编码就变得非常重要,因为通用编码可以保证文档类型的通用性,使用电子数据变得可检索。

继图形通信协会 (Graphic Communications Association, GCA) 研发了用通用标签标记数据的方案来实现通用编码后,IBM 研发了广义标记语言 GML (Generalized Markup Language),美国国家标准协会 (American National Standards Institute, ANSI) 又基于 GML 制定了广义标记语言标准 SGML (Standard Generalized Markup Language),并且 SGML 被飞速推广。但由于 SGML 过于灵活,所以开发一个能处理 SGML 的程序非常复杂和昂贵。20 世纪 90 年代早期,一种非常精简并且支持超级文本的 SGML 文档被研发出来,它就是 HTML。虽然 HTML 非常精简但是它在设计中抛弃了通用编码的一些基本原则,并且 HTML 设计的许多标签都侧重于展示效果,并没有起到非常好的标记作用。由于 Web 的兴起,急需一种既能够保证 SGML 的灵活性,又足够精简的标记语言,很幸运,XML 及时出现了。

下边是一个 XML 描述的示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<Person-array>
    <Person>
        <name>小明</name>
        <age>18</age>
    </Person>
    <Person>
        <name>小红</name>
        <age>17</age>
    </Person>
</Person-array>

XML 数据的基本单元是元素,元素是由起始标签元素内容结束标签组成的。用户把要描述的数据对象放在起始标签和结束标签之间。它的语法格式如下:<标签名称>文本内容</标签名称>。无论文本内容有多长或者多么复杂,XML 元素中都可以再嵌套别的元素,这样使相关信息具有关联性。这种方法定义了 XML 文档数据和数据结构。除了元素,XML 文档的数据中还有声明、注释、根元素、子元素和属性等内容,这部分内容不再具体展开介绍。

2.1.2 XML 序列化框架

在 Java 领域中,常见的 XML 格式序列化框架有以下几种:

  • 第一种是 JDK 自带的 XML 格式的序列化 API,分别是 java.beans.XMLEncoderjava.beans.XMLDecoder,这两个类提供了 XML 格式数据的序列化能力;
  • 第二种是 Digester,Digester 框架是从 Struts 发展而来的,它只能从 XML 文件解析成 Java Bean 对象,功能比较单一;
  • 第三种是 XStream,XStream 是一个功能丰富的框架,它不仅支持将 Java Bean 对象和 XML 互转,还支持 JSON 格式的转化,而且其 API 十分简单易用。

这里我们重点介绍下 XStream。

2.1.3 XStream 使用示例

首先在 Maven 中引入以下依赖:

1
2
3
4
5
<dependency>
    <groupId>com.thoughtworks.xstream</groupId>
    <artifactId>xstream</artifactId>
    <version>1.4.19</version>
</dependency>

然后创建 Person 类:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public class Person {
    private String name;
    private Integer age;
    private BigDecimal salary;

    public Person(String name, Integer age, BigDecimal salary) {
        this.name = name;
        this.age = age;
        this.salary = salary;
    }
}

最后编写测试用例,通过 XStream API 序列化 Person 实例:

1
2
3
4
5
XStream xStream = new XStream();
xStream.alias("person", Person.class);
Person xiaohong = new Person("xiaohong", 28, new BigDecimal(100));
String xml = xStream.toXML(xiaohong);
System.out.println(xml);

执行上述逻辑后可以看到控制台打印出了 XML 数据:

1
2
3
4
5
<person>
  <name>xiaohong</name>
  <age>28</age>
  <salary>100</salary>
</person>

通过 XStream 可以讲一个 Person 对象序列化成 XML 数据格式。序列化后的数据可以用于存储、传输等。通过以下代码还可以将序列化后的数据反序列化,还原原有语义:

1
Person np = (Person) xstream.fromXML(xml);

2.2 JSON 序列化方案

2.2.1 JSON 简介

JSON 是 JavaScript Object Notation 的缩写,可以理解为是 JavaScript 对象的表示法。JSON 格式的数据也是文本格式的一种。JSON 格式出现的比 XML 晚一些,可以说 JSON 是 XML 的替代方案。下面来看一个简单的 JSON 数据:

1
2
3
4
5
6
{
    "pensonlist": [
        {"name": "小明", "age":18},
        {"name": "小红", "age":18}
    ]
}

JSON 格式整体看起来每个数据都是 key-value 形式。JSON 数据本身也有自己的一套规范,比如数组就是中括号 “[]” 包裹起来的内容,数组中的每个元素都用 “,” 隔开,每个对象都 “{}” 包裹起来。JSON 与 XML 相比,序列化后的数据包更小,反序列化 JSON 格式的数要比反序列化 XML 格式的数据更容易,速度也会更快。

JSON 格式的序列化方案最大的优势与 XML 几乎一致,就是数据可读性强和天然支持异构语言。JSON 格式的序列化方案虽然相对于 XML 格式的序列化方案已经在数据包大小和性能上有所优化,但是作为文本格式的序列化方案,它并没有对数据进行压缩,而是原封不动地呈现了所有数据,并且还增加了一些标识符来帮助反序列化,数据包依旧有些大,在序列化和反序列化的性能上也还有很大的进步空间。

JSON 序列化框架

目前几种常见的支持 JSON 格式的序列化框架如下:

  • Fastjson:该框架是阿里巴巴开源的 JSON 解析库,它可以解析 JSON 格式的字符支持将 Java Bean 序列化为 JSON 字符串,也可以从 JSON 字符串反序列化为 Java Bean。它的优点是速度快、社区较为活跃、API 十分简单,不需要第三方库的依赖。
  • Jackson:简单易用并且性能相对高一些,流式解析 JSON 格式在解析数据量大的 JSON 格式文本时比较有优势。但该框架将复杂类型的 JSON 转换为 Bean 时会出现问题,比如一些集合 Map、List 的转换;还有将复杂类型的 Bean 转换为 JSON 时,转换的 JSON 格式不符合标准。
  • Gson:由 Google 开源的 JSON 解析库,功能最全,它没有依赖第三方类库,但是在性能上比 Jackson 和 Fastjson 稍微差一些。
  • JSON-lib:最古老的 JSON 解析工具,它在进行 Java Bean 和 JSON 字符串互相转化的时候,依赖了太多的第三方库,并且在转化的时候还有缺陷,比如数值溢出时不抛出错误。该工具库最近一次更新也是在很多年前了,已经满足不了现在的开发需求。

3 二进制序列化方案

二进制格式的序列化方案指数据按照某种编排规则通过二进制格式呈现。将对象序列化为二进制格式的数据能够大大减小数据包大小,从而提高传输速度和解析速度。二进制格式的序列化方案相对于文本格式的序列化方案而言,更容易提升性能,更容易对数据做加密,安全性更高。

二进制格式的序列化方案非常多样化,所以现在市面上开源的序列化框架也非常多,这些开源的序列化框架都做了不同程度的优化,提高了序列化框架的性能,比如做数据压缩等。现在互联网环境对性能要求极高,所以二进制格式的序列化方案被使用得更广泛。

二进制序列化方案分为两大类:

  • 第一类是强语言相关的,例如 Java 的原生序列化方案、Java 第三方序列化方案。
  • 第二类是异构语言序列化方案,其特点是跨语言,通用性更强。

3.1 Java 语言序列化方案

Java 语言强相关的序列化方案分为 JDK 原生序列化第三方框架两大类。

3.1.1 Java 原生序列化方案

Java 在 JDK 1.1 中引入了一套序列化 API,它可以帮助我们将 Java 对象和二进制流相互转化。API 的使用也很简单,只要在需要序列化的类定义上实现 java.io.Serializable 接口,然后通过 ObjectInputStreamObjectOutputStream 即可实现两者的转化。

该序列化方式将字段类型信息用字符串格式写到了二进制流中,这样反序列化方就可以根据字段信息实现反序列化。但是这种翻案生成的字节流太大,性能也不高,企业开发中很少使用。

3.1.2 Kryo 序列化框架

第二种 Java 语言序列化方案是使用第三方框架,这里我们重点介绍 Kryo。

3.1.2.1 Kryo 简介

Kryo 是一款优秀的 Java 序列化框架,它的序列化/反序列化性能很高,相比于 JDK 原生序列化方案,Kryo 使用了字节码生成机制(基于 ASM 库),因此性能提升明显。Kryo 还对数据做了压缩处理,减小了序列化后数据包的大小。

Kryo 之所以在序列化和反序列化的性能上有良好的表现,是因为它在时间消耗和空间消耗上都做了优化:

  • 时间消耗优化方面主要是预先缓存了元数据信息:Kyro 先加载类元数据信息,将加载后的二进制数据存入缓存,保证之后的序列化和反序列化过程都不需要重新加载该类的元数据。虽然在第一次加载时耗费性能,但是方便了后续的加载;
  • 在空间消耗上,为了降低序列化后数据的大小,做出了以下两点优化:
    1. 变长设计降低空间消耗:Kryo 对 longint 这两种数据类型采用变长字节存储来代替 Java 标准的固定长度存储模式。以 int 为例,Java 中需要 4 字节存储,而 Kryo 可以通过 1~5 个字节去存储,避免了较小的数字高位的 0 浪费的空间。
    2. 压缩元数据信息:Kryo 提供了单独序列化对象信息,以及将类的元数据信息与对象信息一起序列化两种方式。由于第二种方式携带的类的元数据信息太大,倒置序列化后的数据特别大,所以 Kryo 又提供了一种提前注册类的方式,这种方式是在 Kryo 中将 Java 类用唯一 Id 表示,当 Kryo 对 Class 进行序列化时只需要将对应的 ID 数值进行传递即可。反序列化时根据 ID 来找对应的类,然后通过类加载器进行加载,如此一来大大减少了数据包大小。

Kyro 除了在性能上表现良好,其提供的功能也非常全面。比如它支持分块编码:与 HTTP 中 Transfer-Encoding:chunked 的分块上传机制十分类似,它通过使用小的缓冲区来解决无法得知数据长度的问题。当缓冲区已满时,其长度和数据被写入。将长度与数据作为一个数据块,然后切分整个数据为很多块进行编码,直到没有更多的数据市,用长度为 0 的块表示结尾。Kyro 的 OutputChunked 用于写分块数据,其中 endChunks() 方法用于标记一组块的结尾。

Kryo 支持自动生成深拷贝和浅拷贝的对象副本:Kyro 的复制不会序列化为字节然后反转,它使用直接分配的机制。

Kryo 支持解决循环依赖问题:它可以序列化相同对象和循环图的多个引用,单开启解决循环依赖后会有少量的开销,如果确保没有发生循环引用,可以调用 kryo.setReferences(false) 关闭这个功能。一旦出现循环引用,容易导致内存溢出问题。

除了优点,Kryo 也有以下缺点:

  • 由于 Kryo 可以把类的元数据信息写到二进制流中,所以对类的匹配是完全对应的,如此一来就对类的升级的兼容性较差。在使用时要保证两边的 Class 结构一致。
  • Kryo 不是线程安全的:每个线程都应该有自己的 Kryo Input 和 Output 实例。此外,bytes[] Input 可能被修改,然后在反序列化期间回到初始状态,因此不应该在多线程中并发使用相同的 bytes[]。可以使用 ThreadLocal 将 Kryo 实例绑定到对应的线程上来解决 Kryo 线程安全问题。
  • Kryo 的实例的创建/初始化是相当昂贵的,如果每个线程都创建 Kryo 的实例,则会非常占用系统资源。所以在多线程的情况下,应该池化 Kryo 实例,Kryo 提供的 KryoPool 允许使用 SoftReferences 保留对 Kryo 实例的引用,这样当 JVM 耗尽内存时 Kryo 实例就可以被 GC 回收。
3.1.2.2 Kryo 使用示例

首先引入依赖:

1
2
3
4
5
<dependency>
    <groupId>com.esotericsoftware</groupId>
    <artifactId>kryo</artifactId>
    <version>5.3.0</version>
</dependency>

Kryo 的序列化方式根据是否保存对象所属类的元信息可以分为 writeObjectwriteClassAndObject 两种,如果对象所属类不是 Java 原生的类,也就是自定义的类,则需要执行注册操作。

第一种使用方法为调用 Kryo 的 writeObject 方法。使用这种方法时只会序列化对象的实例,不会记录对象所属类的元信息,因为不需要记录对象属性类的元信息,所以这种方法的优势是节省空间,劣势是需要提供序列化目标类作为反序列化的模板。示例如下:

  • 使用 writeObject 对 Java 原生类对象进行序列化的示例:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    
    public static void originalObjectSerializationTest() {
        Kryo kryo = new Kryo();
        String name = "xiaoming";
        //序列化
        try {
            Output output = new Output(new FileOutputStream("kryo.result"));
            kryo.writeObject(output, name);
            output.close();
        } catch (FileNotFoundException e) {
            throw new RuntimeException(e);
        }
        //反序列化
        try {
            Input input = new Input(new FileInputStream("kryo.result"));
            String s = kryo.readObject(input, String.class);
            System.out.println(s);
            input.close();
        } catch (FileNotFoundException e) {
            throw new RuntimeException(e);
        }
    }

    调用该方法后,工程根目录会生成 kryo.result 文件,里边内容为 xiaomin�,即序列化后的二进制数据。

  • 使用 writeObject 对自定义类型的对象进行序列化的示例:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    
    public static void customizeObjectSerializationTest() {
        Kryo kryo = new Kryo();
        kryo.register(Person.class);
        Person xiaoming = new Person("xiaoming", 18);
        //序列化
        try {
            Output output = new Output(new FileOutputStream("kryo.result"));
            kryo.writeObject(output, xiaoming);
            output.close();
        } catch (FileNotFoundException e) {
            throw new RuntimeException(e);
        }
        //反序列化
        try {
            Input input = new Input(new FileInputStream("kryo.result"));
            Person person = kryo.readObject(input, Person.class);
            System.out.println(person.getName());
            System.out.println(person.getAge());
            input.close();
        } catch (FileNotFoundException e) {
            throw new RuntimeException(e);
        }
    }

    调用该方法后,工程根目录会生成 kryo.result 文件,里边内容为 $xiaomin�,即序列化后的二进制数据,控制台也成功打印出了反序列化后的字段信息。

第二种方法是使用 kryo 的 writeClassAndObject 把类的元数据和对象的实例一同写入二进制流,这样做的好处是读取该二进制流的时候,无需再提供类的信息;缺点是占用带宽更大。Kryo 可以对类进行注册,对类绑定唯一数字 ID,以节省带宽:

  • 使用 writeClassAndObject 对 Java 原生类对象进行序列化的示例:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    
    public static void originalObjectSerializationWithObjectTest(){
        Kryo kryo = new Kryo();
        String name = "xiaoming";
        //序列化
        try {
            Output output = new Output(new FileOutputStream("kryo.result"));
            kryo.writeClassAndObject(output, name);
            output.close();
        } catch (FileNotFoundException e) {
            throw new RuntimeException(e);
        }
        //反序列化
        try {
            Input input = new Input(new FileInputStream("kryo.result"));
            String s = (String) kryo.readClassAndObject(input);
            System.out.println(s);
            input.close();
        } catch (FileNotFoundException e) {
            throw new RuntimeException(e);
        }
    }
  • 使用 writeClassAndObject 对自定义类型的对象进行序列化的示例:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    
    public static void customizeObjectSerializationWithObjectTest() {
        Kryo kryo = new Kryo();
        kryo.register(Person.class);
        Person xiaoming = new Person("xiaoming", 18);
        //序列化
        try {
            Output output = new Output(new FileOutputStream("kryo.result"));
            kryo.writeClassAndObject(output, xiaoming);
            output.close();
        } catch (FileNotFoundException e) {
            throw new RuntimeException(e);
        }
        //反序列化
        try {
            Input input = new Input(new FileInputStream("kryo.result"));
            Person person = (Person) kryo.readClassAndObject(input);
            System.out.println(person.getName());
            System.out.println(person.getAge());
            input.close();
        } catch (FileNotFoundException e) {
            throw new RuntimeException(e);
        }
    }

3.2 异构语言序列化方案

上文的 JDK 原生序列化方案或者 Kryo 方案都只能在 Java 语言中使用,对异构性语言并不友好。异构语言指的是不同的语言,在序列化技术中,可以理解为应用程序是用 Java 开发的,但是某个序列化框架并不是针对 Java 的语法设计的,可能是针对 Golang 设计的,并且它又是二进制的数据格式,那么该序列化框架不能为 Java 开发的应用程序提供序列化和反序列化能力,从而导致在解析二进制数据时不能解析出正确的内容。所以只支持单语言的序列化框架的应用市场减少了很多。而对异构语言的支持在数据传输中尤为重要,这就涉及异构语言应用之间的 RPC 调用。

目前有两种方案可以解决异构语言问题。

  • 第一种方案就是根据相同的机制重新实现对应语言的序列化框架。相同的机制是指对数据的编排都必须保持一致,比如 Kryo 只支持 Java,现在需要支持 Golang 的序列化,从而实现被 Java 版本的 Kryo 框架序列化后的数据能够被 Golang 版本的 Kryo 框架反序列化(该版本不存在)。而在 Golang 版本的 Kryo 框架中需要遵循 Java 版本的 Kryo 架的设计原则,才能正确地将 Java 的数据转化成 Golang 的数据。比如将 Java 中 int 类型的数据转化为 Golang 中 int32 类型的数据。目前市面上也有一些开源框架为了满足异构语言的需求而衍生出许多语言的版本,Hessian 就是非常典型的例子:

    Hession 目前已经支持多种编程语言,包括 Java、Python、C++、C#、Erlang、PHP、Ruby 和 C。它是一个高效序列化框架,其实现机制注重简化数据。Hessian 简化了数据类型的元数据表达,通过 Java 的反射机制,将对象的所有属性视为一个 Map 进行序列化。阿里巴巴在 Hession 的基础上,开源了 Hession-lite 版本,解决了原版 Hession 存在的一些问题,并提升了一些性能。该版本目前已捐献给 Apache。甚至如今,阿里巴巴内部的 HSF RPC 服务的默认序列化框架就是 Hession2。

  • 第二种方案就是通过与编程语言无关的 IDL 来解决异构语言的问题,这种方案目前较为常见的有以下两种:

    • Thrift:Thrift 是 Facebook 开源的一个高性能、轻量级 RPC 服务架,它针对序列也有自己独特的处理方式,在 Thrift 框架内部提供了对 Thrift 协议的序列化的工具,且减小了序列化后的数据包大小,以及提升了解析的性能,但是 Thrift 没有暴露化和反序列化的 Thrift 协议的序列化能力与 Thrift 框架强耦合,所以它支持其它协议的序列化和反序列化比较困难。由于 Thrift 有 IDL 的设计,支持多语言之间进远程调用,所以它同样支持多语言之间的序列化。
    • ProtoBuf:ProtoBuf 的全称为 Google Protocol Buffer,是 Google 公司内部的混合语言数据标准,它是一种轻便高效的结构化数据存储格式,可用于序列化。它解析速度快,序列化和反序列化 API 非常简单,文档也非常丰富,并且可以跟各种传输层协结合使用。它同样有 IDL 的设计,并且提供了 IDL 的编译器,它支持的语言也非常多比如 C++、C#、Dart、Golang、Java、Python、Rust 等。

4 序列化框架选型

在序列化框架选型阶段,需要从多个角度来考量各个序列化框架。在做选型之前,首先需要明确项目的需求,比如需要将序列化框架运用在数据的传输中,但是要明确整个传输过程中更侧重性能,还是更侧重可读性,仅明确这一点就可以在选型初期排除一大批序列化框架。通过初期筛选的序列化框架可以从以下几个角度综合考量:

  • 通用性:序列化框架的通用性主要体现在跨语言和跨平台两个方面。如果项目初期或者未来有跨语言或者跨平台的需求,则必须考虑序列化框架的通用性,确保所选的序列化框架满足项目需求。

    跨语言指的是支持异构语言,选择跨语言的序列化框架有两个方案:

    • 第一个方案是直接选择文本格式的序列化方案,因为文本格式的序列化方案天生就支持异构语言。无论是哪种编程语言,都能够反序列化文本格式的数据。
    • 第二个方案是选择二进制格式的序列化方案,但是必须选择支持异构语言的序列化方案,比如 Hessian 这种实现了多个编程语言版本的序列化方案,或者是 ProtoBuf 这种通过 IDL 文件支持异构语言的序列化方案。
  • 性能:性能是序列化框架非常重要的指标,序列化框架的性能可以从时间开销和空间开销两个方面来考量。

    空间开销可以分为两个方面:

    • 第一是序列化框架在序列化或者反序列化过程中对系统内存的开销。比如前面提到的 Fastjson 中使用符号表算法缓存关键字,避免创建重复的关键字,这样可以减少许多内存的开销。

    • 第二就是序列化后的数据大小。在文本序列化方案中,无论是 JSON 格式还是 XML 格式,都在原始的数据上添加了一些数据描述,比如 JSON 格式数据中的 {} 等,所以文本序列化方案中序列化得到的数据非常大。但是在二进制序列化方案中,可以对数据做压缩,并且还能简化数据类型描述,所以二进制序列化方案序列化后的数据远远小于文本序列化方案序列化后的数据。

      序列化后的数据大小对于 RPC 调用来说尤为重要,因为数据包越小,意味着一次 RPC 调用所需传输的数据包就越小,这样可以提升 RPC 调用的性能,增加服务端的吞吐量。

    时间开销则是序列化和反序列化的总耗时,也是就这两个过程的性能。复杂的序列化协议会导致序列化和反序列化耗时较长,这可能会使得序列化和反序列化阶段成为整个系统性能的瓶颈。

    上述指标只能排除一部分序列化框架,最终还需要通过对候选的序列化框架进行压测,得出压测结果才能下定论。

  • 可扩展性:序列化框架的核心是序列化协议,只要是协议,它的扩展性或者兼容性就很重要。现在需求迭代周期短,新需求涌现的速度快,如果序列化协议的可扩展性非常好,那么运用该序列化协议的业务系统就可以支撑需求的不断更迭。比如在 JSON 格式的序列化方案中,整个序列化协议仅限定了整体的数据格式必须按照 JSON 的规范进行编排,但是并没有限定数据的编排顺序,所以无论是扩展还是变更都及其灵活。

  • 安全性:序列化框架的安全性主要体现在序列化协议方面。

    比如 JSON 格式 XML 格式的序列化方案在安全性上并不理想,因为 XML 格式和 JSON 格式是业界都熟悉的数据格式,数据格式相对固定,如果没有对数据做加密处理,则非常容易被发现安全漏洞。近几年业界的序列化框架中 Fastjson 暴露的安全漏洞较多,比如远程代码执行漏洞等。

    在安全性方面,二进制格式的序列化方案相对来说较为安全。一是因为序列化为二进制后的数据格式并不像 JSON 和 XML 一样有标准的格式;二是因为对二进制数据更加容易加密。在二进制格式的序列化方案中,非自描述型的序列化方案的安全性又要高于自描述型的序列化方案。因为非自描述型的序列化方案如果没有描述文件,二进制流数据将毫无意义。而自描述型的序列化方案序列化后的数据将携带一定的数据描述,所以这部分二进制数据是具有含义的,安全性自然也略低。

  • 支持的数据类型的丰富程度:序列化的目标是数据,所以支持的数据类型对于序列化框架来说也是非常重要的一个指标。支持更多的数据类型,序列化框架的使用范围也会更广。

  • 可读性:可读性并不是选择序列化框架的关键,因为可读性高的序列化框架的价值更多体现在项目的联调阶段和排查问题的调试阶段。

    当接收数据方要将序列化后的数据反序列化时,出现了反序列化失败,为了确定序列化后的数据的正确性,需要查看序列化后的数据内容。比如在发起一次 RPC 调用时,客户端请求调用的数据被传输到服务端,但是服务端反序列化失败,此时最简单的方式就是重新发起一次 RPC 调用,然后抓一下请求包的数据或者记录序列化后的数据,观察数据是否正常。如果是文本格式的序列化方案,则一眼就可以看出数据的正确性,如果是二进制格式的序列化方案,就无法直观地检查数据的正确性。所以在满足别的需求的前提下,尽可能使用可读性较好的方案。良好的可读性可以提升开发效率,加快定位问题的速度。

  • 开源社区成熟度和活跃度:只要是开源的框架,开源社区的成熟度和活跃度永远都是需要考量的指标,这决定了该序列化框架的稳定性。使用它的人越多,往往说明该框架越优秀,开源的序列化框架的发展也会更快。


欢迎关注我的公众号,第一时间获取文章更新:

微信公众号

相关内容