为什么要序列化?
众所周知,计算机底层存储或网络传输的数据都必须是二进制数据;所谓底层,就是最下面最接近硬件的部分,为了方便大家理解,我先整理出一张图供大家对应着看,如果有不对,欢迎指正:
在Java这门语言中,一切皆对象,这是基于开发人员理解抽象出来的一种“思想”;对于底层设备来说对象是什么玩意儿跟他没关系,要想通过底层,就必须要尊重底层定义把数据转换二进制。
Java中序列化方式
2.1 实现Serializable接口—— 这种方式可能是比较多的同学在使用的,有的IDE在实现该接口后会提示开发人员要生成一个Long类型的标识,本人曾经遇到过的一个坑就是为了避免DB被过多的查询搞死,将查询结果放置的缓存,但定义的这个对象因新功能被其它同学修改过,上线后将缓存结果反序列化成对象时出错;还有的(低版本)RPC框架通信时如果一个对象没有实现该接口可能会报错;特别说明这种方式只会序列化所有非static和transient关键字修饰的成员变量。
2.2 实现Externalizable接口—— 这种方式本人没用过;但查看源码得知有如下限制:实现该接口必须实现writeExternal()方法和readExternal()方法且必须有一个无参的构造函数,支持手动选择哪些部分序列化。
2.3 实现Serializable接口,添加writeObject()和readObject()方法;该方式结合了2.1、2.2两种方式的优点,限制有 a:) 方法必须是private关键字修饰 b:) 第一行调用默认的defaultWriteObject() / defaultReadObject() 完成非static和transient修饰的变量的序列化与反序列化 c:) 调用writeObject() / readObject()方法对“指定的”成员变量进行序列化与反序列化;这种方式在源码ArrayList中可以看见
其它序列化方式
alibaba的fastJson、fasterxml的jackson、facebook的Thrift、Google的Protobuf等等。其中前2个本人都用过引入相关的maven坐标就好,但要提醒的是使用者要注意关注相关的技术栈,时常有漏洞提示,为避免给公司造成损失,请按官方说明进行升级;thrift暂未接触,因本人当前所在公司属于汽车的智能辅助驾驶,考虑到车机客户端的情况,当前使用protobuf进行通信
认识Protobuf(Google Protocol Buffers)
官方文档对 Protobuf的定义:protocol buffers 是一种语言无关、平台无关、可扩展的序列化结构数据的方法,可用于数据通信协议和数据存储等,它是 Google 提供的一个具有高效协议数据交换格式工具库,是一种灵活、高效和自动化机制的结构数据序列化方法。相比XML,有编码后体积更小,编、解码速度更快的优势;相比于 Json,Protobuf 有更高的转化效率,时间效率和空间效率都是 JSON 的 3-5 倍。
优点:
a. 性能好/效率高:无论时间、空间上都是比较低的
b. 有代码生成机制:可以将.proto格式的文件转换成指定开发语言的文件
c. 支持多种编程语言:C,C++,Java,Go,Dart,Python,Kotlin.....
d. 支持向前或向后兼容:在不同的协议中对多出来的字段定义可以“忽略”或变成“可选”
缺点:
a. 二进制格式文件导致可读性差
b. 缺乏自释性描述:如xml的节点名称,json的key
c. 通用性差:仍然是相对json和xml来的,对不同项目需要做适配工作
举例说明
因本人工作中的proto文件(业务)定义太过繁杂,为方便大家理解和遵守公司规章制度,我将通过一个简单的(通讯录)例子向给大家展示:
//定义4个相关联的Java类 然后 初始化并且写文件
@Data
public class AddressBook implements Serializable {
private List<Person> people; //通讯录
}
@Data
public class Person implements Serializable {
private Integer id; //标识
private String name; //姓名
private String email; //邮箱
private List<PhoneNumber> phones; //联系方式
}
@Data
public class PhoneNumber implements Serializable {
private String number;
private PhoneType type;
}
public enum PhoneType {
MOBILE,HOME,WORK
}
//定义proto格式文件并且初始化
syntax = "proto2";
package your.package;
将嵌套的文件拆开生成在不同的当中 false:类似Java的嵌套类
option java_multiple_files = true;
会自动放置在package目录下(没有则创建),反之则创建目录
option java_package = "proto";
//生成指定语言的文件名
option java_outer_classname = "AddressBook";
message Person {
optional string name = 1;
optional int32 id = 2;
optional string email = 3;
enum PhoneType {
MOBILE = 0;
HOME = 1;
WORK = 2;
}
message PhoneNumber {
optional string number = 1;
optional PhoneType type = 2 [default = HOME];
}
repeated PhoneNumber phones = 4;
}
message AddressBook {
repeated Person people = 1;
}
数据对比
通过右键查看文件可以看到proto序列化后比Java原生序列化后占用空间一样,因为磁盘是以页为单位(默认4k),但实际大小上还是有区别的(该结果受操作系统和数据量影响),感兴趣的可以试一下
数据读取
Java的序列化读取文件这里就不用举例了,本篇只为举例完成Proto的反序列化,能正确取出放进去的值即可。
public static void main(String[] args) throws Exception {
proto.AddressBook read = proto.AddressBook.parseFrom(new FileInputStream(new File("d:/proto/address_book_proto.txt")));
for (proto.Person per : read.getPeopleList()) {
System.out.println("id:"+per.getId());
System.out.println("name:"+per.getName());
System.out.println("email:"+per.getEmail());
for (proto.Person.PhoneNumber phoneNumber : per.getPhonesList()) {
System.out.println("type:"+phoneNumber.getType());
System.out.println("number:"+phoneNumber.getNumber());
}
}
}
如何生成Java类
a. 通过命令行
protoc -I=$src_dir --java_out=$dst_dir $src_dir/address_boo.proto
$src_dir:protoc可执行文件所在目录,如果没有则为当前目录
$dst_dir:生成Java文件存放的目录,记得带文件名,支持多目录
更多参数选项可通过protoc help查看
b. 通过插件
如果你用的是idea可安装插件后通过:Tools -> Configure GenProtoBuf完成配置后直接生成相应的文件
写在最后
文章只是我在某车企这段工作中的一点小收获,为避免遗忘特记录在此。不代表适合任何公司业务,就像前面所述,当前业务是跑在车机上,相对于PC来说,用户车端网络、存储等资源是比较昂贵的,所以我们选择了Proto协议。参考链接(微信不支持外链,辛苦copy一下了)
proto可执行文件下载地址:https://developers.google.com/protocol-buffers/docs/downloads
性能对比:https://github.com/eishay/jvm-serializers/wiki
学习文档:https://developers.google.com/protocol-buffers
其实通过proto生成的Java文件还有很多值得去讲解,比如使用了builder模式、生成了更多的方法方便各种场景,这些在打.(点)的时候通过提示就可以看到,有兴趣可以试一试。