序列化原理及技术实现

序列化

理解

序列化并不是说要一次性把对象或者消息直接编码为二进制串,也可以转化为本地某一种可以持久化或者传输的结构。对该转化后结构进行二进制编码即可。

Java API相关

Serializable 接口

  • 是一个标记接口,不用实现任何方法。一旦实现了此接口,该类的对象就是可序列化的。
  • Serializable接口的反序列化并不会调用构造方法。对象是由JVM自己生成的对象,不通过构造方法生成。因此,它存在一定的安全隐患。一个精心构造的byte[]数组被反序列化后可以执行特定的Java代码,从而导致严重的安全漏洞。

序列化步骤:

  • 步骤一:创建一个ObjectOutputStream输出流;

  • 步骤二:调用ObjectOutputStream对象的writeObject输出可序列化对象。

  • 注意事项:

    • 如果一个可序列化的类的某个属性不是基本类型,也不是String类型,那这个引用类型也必须是可序列化的;否则,会导致此类不能序列化。
    • 在同一次ObjectOutputStream写出时,如果一个对象写出多次,则从第二次开始,都是引用的第一次写出的对象。潜在的问题是如果序列化一个可变对象(对象内的内容可更改)后,更改了对象内容,再次序列化,并不会再次将此对象转换为字节序列,而只是保存该对象第一次序列化的编号。

反序列化步骤:

  • 步骤一:创建一个ObjectInputStream输入流;
  • 步骤二:调用ObjectInputStream对象的readObject()得到序列化的对象。

自定义序列化时属性

(可以自己选择哪些属性需要序列化,或者对序列化数据进行编码加密等。)

  • 向ObjectOutputStream写出对象的哪些属性

    private void writeObject(ObjectOutputStream out) throws IOException

  • 向ObjectIutputStream读取哪些属性,然后给对象赋值

    private void readObject(ObjectIutputStream in) throws IOException,ClassNotFoundException;

  • 不同类接收反序列化对象,或者序列化流被篡改时,系统都会调用readObjectNoData()

    private void readObjectNoData() throws ObjectStreamException

public class Person implements Serializable {   
	private String name;   
	private int age;   //省略构造方法,get及set方法   
	
	//将名字反转写入二进制流       
	private void writeObject(ObjectOutputStream out) throws IOException {       
		out.writeObject(new StringBuffer(this.name).reverse());       
		out.writeInt(age);   
	}   
	
	// 将读出的字符串反转恢复回来
    private void readObject(ObjectInputStream ins) throws IOException,
  												ClassNotFoundException{             
		this.name = ((StringBuffer)ins.readObject()).reverse().toString();       
		this.age = ins.readInt();   
	}
}

自定义序列化的对象

  • 决定序列化的对象,会先调用此方法,再调用writeObject方法。

    private Object writeReplace() throws ObjectStreamException

  • 决定反序列化的对象,反序列化出来的对象被立即丢弃,在readeObject后调用(常用来反序列单例类,保证单例类的唯一性)

    private Object readResolve() throws ObjectStreamException

Externalizable接口

  • 强制自定义序列化,必须实现writeExternal、readExternal方法,效果等同writeObject和readOject。
  • 必须提供pulic的无参构造器,因为在反序列化的时候需要反射创建对象

对比

  • Serializable是由系统自动存储必要的信息,Externalizable由程序员实现两个方法决定
  • Externalizable接口带来了一定性能提升,但复杂度也提高了,所以一般通过实现Serializable进行序列化。

SerialVersionUID

  • 保证升级前后的兼容性,如果反序列化使用的class的版本号与序列化时使用的不一致,反序列化会报InvalidClassException异常。
  • 序列化版本号可自由指定,如果不指定,每次JVM会根据类信息给类计算一个版本号,这样每次重新运行程序都无法反序列化之前生成的对象。
  • 必须要指定为static才会被jvm使用,因为是类的信息:private static final long serialVersionUID = 42L;
    • 不指定版本号另一个明显隐患是,不利于jvm间的移植,可能class文件没有更改,但不同jvm可能计算的规则不一样,这样也会导致无法反序列化。
  • 什么情况下需要修改serialVersionUID呢?
    • 如果只是修改了方法,反序列化不容影响,则无需修改版本号;
    • 如果只是修改了静态变量,瞬态变量(transient修饰的变量),反序列化不受影响,无需修改版本号;
    • 如果只是新增了实例变量,则反序列化回来新增的是默认值;如果减少了实例变量,反序列化时会忽略掉减少的实例变量,此种也无需修改(即反序列化,就算基本类型对应的值是空值,也不会反序列化异常)
    • 如果修改了非瞬态变量,则可能导致反序列化失败。如果新类中实例变量的类型与序列化时类的类型不一致,则会反序列化失败,这时候需要更改serialVersionUID。

Transient

使用transient修饰的属性,java序列化时,会忽略掉此字段,所以反序列化出的对象,被transient修饰的属性是默认值。对于引用类型,值是null;基本类型,值是0;boolean类型,值是false;char是’ '。

序列化协议

特性

1、通用性

2、强健性/鲁棒性

3、可调试性/可读性

4、性能

5、可扩展性/兼容性

6、安全性/访问限制

序列化和反序列化的组件

  • IDL(Interface description language)文件:参与通讯的各方需要对通讯的内容需要做相关的约定(Specifications)。为了建立一个与语言和平台无关的约定,这个约定需要采用与具体开发语言、平台无关的语言来进行描述。这种语言被称为接口描述语言(IDL),采用IDL撰写的协议约定称之为IDL文件。
  • IDL Compiler:IDL文件中约定的内容为了在各语言和平台可见,需要有一个编译器,将IDL文件转换成各语言对应的动态库(即由IDL文件,根据不同语言生成相应的类文件,比如.java文件)
  • Stub/Skeleton Lib:负责序列化和反序列化的工作代码。实现对象的序列化与反序列化。
  • Client/Server:指的是应用层程序代码,他们面对的是IDL所生存的特定语言的class或struct。
  • 底层协议栈和互联网:序列化之后的数据通过底层的传输层、网络层、链路层以及物理层协议转换成数字信号在互联网中传递。

XML&SOAP

XML是一种常用的序列化和反序列化协议,具有跨机器,跨语言等优点。

Soap(消息传递协议,规定的消息结构,并不是七层模型中应用层的协议,因为序列化后不是二进制串,所以一般依托HTTP协议传输)

  • SOAP (Simple Object Access Protocol,简单对象访问协议) 是HTTP POST的一个专用版本,遵循一种特殊的XML消息格式Content-type设置为: text/xml任何数据都可以xml化。

  • SOAP:简单对象访问协议,简单对象访问协议(SOAP)是一种轻量的、简单的、基于 XML 的协议,它的IDL是WSDL。它被设计成在 WEB 上交换结构化的和固化的信息

    • SOAP 可以和现存的许多因特网协议和格式结合使用,包括超文本传输协议( HTTP),简单邮件传输协议(SMTP),多用途网际邮件扩充协议(MIME)。它还支持从消息系统到远程过程调用(RPC)等大量的应用程序。
    • SOAP是一个基于XML的交换消息协议,可以使用HTTP来传输这些信息。事实上HTTP是SOAP消息的最常见的传输工具。
  • SOAP将信息进行XML的序列化后,再用HTTP协议的方式再打包(即作为请求体)进行传送,传送的方式还是TCP或者UDP。SOAP也可以绑定到TCP和UDP协议上。

  • Webservice就是使用Soap协议得到你想要的东西,相比Httpservice能处理些更加复杂的数据类型

SOAP优势

  • SOAP协议具有广泛的群众基础,基于HTTP的传输协议使得其在穿越防火墙时具有良好安全特性
  • XML所具有的人眼可读(Human-readable)特性使得其具有出众的可调试性
  • 互联网带宽的日益剧增也大大弥补了其空间开销大(Verbose)的缺点
  • 对于在公司之间传输数据量相对小或者实时性要求相对低(例如秒级别)的服务是一个好的选择。

SOAP劣势

  • 由于XML的额外空间开销大,序列化之后的数据量剧增,对于数据量巨大序列持久化应用常景,这意味着巨大的内存和磁盘开销,不太适合XML。
  • 另外,XML的序列化和反序列化的空间和时间开销都比较大,对于对性能要求在ms级别的服务,不推荐用。
  • WSDL虽然具备了描述对象的能力,SOAP的S代表的也是simple,但是SOAP的使用绝对不简单。对于习惯于面向对象编程的用户,WSDL文件不直观。

JSON

优势

  • 这种Associative array格式非常符合工程师对对象的理解。
  • 它保持了XML的人眼可读(Human-readable)的优点。
  • 相对于XML而言,序列化后的数据更加简洁。 来自于的以下链接的研究表明:XML所产生序列化之后文件的大小接近JSON的两倍。
  • 它具备Javascript的先天性支持,所以被广泛应用于Web browser的应用常景中,是Ajax的事实标准协议。
  • 与XML相比,其协议比较简单,解析速度比较快。
  • 松散的Associative array使得其具有良好的可扩展性和兼容性(松散是指对json序列化和反序列化时,均没有IDL文件对属性的约定,而是全由该对象目前有哪些属性决定)。

应用场景包括:

  • 公司之间传输数据量相对小,实时性要求相对低(例如秒级别)的服务。
  • 基于Web browser的Ajax请求。
  • 由于JSON具有非常强的前后兼容性(序列化和反序列化时对属性的有无没任何要求),对于接口经常发生变化,并对可调式性要求高的场景,例如Mobile app与服务端的通讯。
  • 由于JSON的典型应用场景是JSON+HTTP,适合跨防火墙访问。

不适合的场景

  • 总的来说,采用JSON进行序列化的额外空间开销比较大,对于大数据量服务或持久化,这意味着巨大的内存和磁盘开销,这种场景不适合。
  • 没有统一可用的IDL降低了对参与方的约束,实际操作中往往只能采用文档方式来进行约定,这可能会给调试带来一些不便,延长开发周期。
  • 由于JSON在一些语言中的序列化和反序列化需要采用反射机制,所以在性能要求为ms级别,不建议使用。

Protobuf

典型特征:

  • 标准的IDL和IDL编译器,这使得其对工程师非常友好。

  • 序列化数据非常简洁,紧凑,与XML相比,其序列化之后的数据量约为1/3到1/10。

  • 解析速度非常快,比对应的XML快约20-100倍。

  • 提供了非常友好的动态库,使用非常简介,反序列化只需要一行代码。

    protoc -I=$SRC_DIR --java_out=$DST_DIR $SRC_DIR/addressbook.proto

劣势

  • Protobuf主要问题在于其所支持的语言相对较少
  • 另外由于没有绑定的标准底层传输层协议,在公司间进行传输层协议的调试工作相对麻烦。

应用场景

  • 空间开销小以及高解析性能(只需要简单的位运算),非常适合于公司内部的对性能要求高的RPC传输使用。
  • 由于Protobuf提供了标准的IDL以及对应的编译器,其IDL文件是参与各方的非常强的业务约束,
  • 另外,Protobuf与传输层无关,所以也可以采用HTTP具有良好的跨防火墙的访问属性,所以Protobuf也适用于公司间对性能要求比较高的场景。
  • 由于其解析性能高,序列化后数据量相对少,非常适合应用层对象的持久化场景。
  • 传输数据量较大的需求场景下,比XML、Json 更小、更快、使用 & 维护更简单!

IDL文件举例(会编译成.java文件)

syntax = "proto2";

package tutorial;

option java_package = "com.example.tutorial";
option java_outer_classname = "AddressBookProtos";

message Person { // 生成静态内部类com.example.tutorial.AddressBookProtos.Person;
  required string name = 1; // 每个'='后的数字是指在二进制文件里排在属性的第几个位置
  required int32 id = 2;
  optional string email = 3;

  enum PhoneType { //生成静态内部枚举类 ...AddressBookProtos.Person,PhoneType
    MOBILE = 0;
    HOME = 1;
    WORK = 2;
  }

  message PhoneNumber { // 静态内部类 ...AddressBookProtos.Person,PhoneNumber
    required string number = 1;
    optional PhoneType type = 2 [default = HOME];
  }

  repeated PhoneNumber phones = 4; // repeated类型会生成List类型。
}

message AddressBook { // 生成静态内部类com.example.tutorial.AddressBookProtos.AddressBook;
  repeated Person people = 1;
}

Avro

典型特征:

  • Avro的产生解决了JSON的冗长和没有IDL的问题,使用JSON格式的Schema作为其IDL。
  • Avro提供两种序列化格式:JSON格式或者Binary格式。
    • Binary格式在空间开销和解析性能方面可以和Protobuf媲美
    • JSON格式方便测试阶段的调试。
  • Avro支持的数据类型非常丰富,包括C++语言里面的union类型。
  • Schema可以在传输数据的同时发送,加上JSON的自我描述属性,这使得Avro非常适合动态类型语言。同时服务端和客户端可以在握手阶段对Schema进行互相确认,大大提高了最终的数据解析速度。
  • Avro在做文件持久化的时候,一般会和Schema一起存储,所以Avro序列化文件自身具有自我描述属性(其他协议序列化后的文件只有属性的值,但没有属性相关信息),所以非常适合于做Hive、Pig和MapReduce的持久化数据格式。

应用场景

  • Avro解析性能高并且序列化之后的数据非常简洁,比较适合于高性能的序列化服务。
  • 由于Avro目前非JSON格式的IDL处于实验阶段,而JSON格式的IDL对于习惯于静态类型语言的工程师不直观。

IDL格式

protocol Userservice {
  record Address {
   string city;
   string postcode;
   string street;
  }  
  record UserInfo {
   string name;
   int userid;
   array<Address> address = [];
  }
} 

该IDL对应的JSON Schema格式

{
  "protocol" : "Userservice", // 该Schema描述对象
  "namespace" : "org.apache.avro.ipc.specific", // 包名
  "version" : "1.0.5", // 版本
  "types" : [ {
  	"name" : "Address", // 类
    "type" : "record"
    "fields" : [ { // 字段
      "name" : "city",
      "type" : "string"
    }, {
      "name" : "postcode",
      "type" : "string"
    }, {
      "name" : "street",
      "type" : "string"
    } ]
  }, {
  	"name" : "UserInfo", 
    "type" : "record"
    "fields" : [ {
      "name" : "name",
      "type" : "string"
    }, {
      "name" : "userid",
      "type" : "int"
    }, {
      "name" : "address",
      "type" : {
        "type" : "array",
        "items" : "Address"
      },
      "default" : [ ]
    } ]
  } ],
  "messages" : { }
}

Thrift

典型特征

  • Thrift是Facebook开源提供的一个高性能,轻量级RPC服务框架,其产生正是为了满足当前大数据量、分布式、跨语言、跨平台数据通讯的需求。
  • Thrift并不仅仅是序列化协议,而是一个RPC框架。
  • 相对于JSON和XML而言,Thrift在空间开销和解析性能上有了比较大的提升,对于对性能要求比较高的分布式系统,它是一个优秀的RPC解决方案
  • 但是由于Thrift的序列化被嵌入到Thrift框架里面,Thrift框架本身并没有透出序列化和反序列化接口,这导致其很难和其他传输层协议共同使用(例如HTTP)。

应用场景

  • 对于需求为高性能,分布式的RPC服务,Thrift是一个优秀的解决方案。
  • 它支持众多语言和丰富的数据类型,并对于数据字段的增删具有较强的兼容性。所以非常适用于作为公司内部的面向服务构建(SOA)的标准RPC框架。

劣势

  • Thrift的文档相对比较缺乏,目前使用的群众基础相对较少。
  • 另外由于其Server是基于自身的Socket服务,所以在跨防火墙访问时,安全是一个顾虑,所以在公司间进行通讯时需要谨慎。
  • 另外Thrift序列化之后的数据是Binary数组,不具有可读性,调试代码时相对困难。
  • 最后,由于Thrift的序列化和框架紧耦合,无法支持向持久层直接读写数据,所以不适合做数据持久化序列化协议。

IDL文件举例

struct Address
{ 
    1: required string city;
    2: optional string postcode;
    3: optional string street;
} 
struct UserInfo
{ 
    1: required string userid;
    2: required i32 name;
    3: optional list<Address> address;
}

性能与空间对比

  • XML序列化(Xstream)无论在性能和简洁性上比较差。
  • Thrift与Protobuf相比在时空开销方面都有一定的劣势。
  • Protobuf和Avro在两方面表现都非常优越。

选型建议

以上描述的五种序列化和反序列化协议都各自具有相应的特点,适用于不同的场景:

1、对于公司间的系统调用,如果性能可以在100ms以上的服务,基于XML的SOAP协议是一个值得考虑的方案。

2、基于Web browser的Ajax,以及Mobile app与服务端之间的通讯,JSON协议是首选。对于性能要求不太高,或者以动态类型语言为主,或者传输数据载荷很小的的运用场景,JSON也是非常不错的选择。

3、对于调试环境比较恶劣的场景,采用JSON或XML能够极大的提高调试效率,降低系统开发成本。

4、当对性能和简洁性有极高要求的场景,Protobuf,Thrift,Avro之间具有一定的竞争关系。

5、对于T级别的数据的持久化应用场景,Protobuf和Avro是首要选择。如果持久化后的数据存储在Hadoop子项目里,Avro会是更好的选择。

6、由于Avro的设计理念偏向于动态类型语言,对于动态语言为主的应用场景,Avro是更好的选择。

7、对于持久层非Hadoop项目,以静态类型语言为主的应用场景,Protobuf会更符合静态类型语言工程师的开发习惯。

8、如果需要提供一个完整的RPC解决方案,Thrift是一个好的选择。

9、如果序列化之后需要支持不同的传输层协议,或者需要跨防火墙访问的高性能场景,Protobuf可以优先考虑。

已标记关键词 清除标记
相关推荐
©️2020 CSDN 皮肤主题: 撸撸猫 设计师:C马雯娟 返回首页