聊聊序列化–选型
前文我们介绍的是JDK自带的序列化框架,如果是java系的代码,都用这个序列化框架实现简单,使用方便。但是在实际生成环境中,难免会有跨语言的情况,而这种情况,jdk自带的序列化框架是不满足的。另外,jdk进行序列化编码之后产生的字节数组过大,占用的存储内存空间也较高,这就导致了相应的流在网络传输的时候带宽占用较高,性能相比较为低下的情况。
除了原生的序列化之外,常见的还有protobuf、json、xml、hessian等
-
JSON:不多说了,用途广泛,序列化方式还衍生了阿里的fastjson,美团的MSON,谷歌的GSON等更加优秀的转码工具。优点:使用方便。缺点:数据冗长,转码性能一般。 -
XML:很久之前的转码方法了,现在用的不多。优点:暂时没发现。缺点:数据冗长,转码性能一般。 -
Serialzable:JDK自带的序列化。优点:使用方便。缺点:转码性能低下。 -
hessian:基于 binary-RPC实现的远程通讯library,使用二进制传输数据。优点:数据长度小。缺点:性能低下。 -
protobuf:谷歌公司出的一款开源项目,性能好,效率高,并且支持多种语言,例如:java,C++,python等。 优点:转码性能高,支持多语言。 缺点:中文文档少,使用相对复杂。
Hessian
Hessian是一款支持多种语言进行序列化操作的框架技术,同时在进行序列化之后产生的码流也较小,处理数据的性能方面远超于java内置的jdk序列化方式。
hessian序列化比Java序列化高效很多,而且生成的字节流也要短很多。
为了与jdk原生的进行区别,这边新建一个HUser对象,但是内容基本是一致的,而且也需要实现Serializable接口
引入依赖:
<dependency>
<groupId>com.caucho</groupId>
<artifactId>hessian</artifactId>
<version>4.0.7</version>
</dependency>
public class HUser implements Serializable{
private int id;
private String name;
private transient int age;
public HUser(String name, int id, int age) {
this.name = name;
this.id=id;
this.age=age;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public HUser(String name, int id) {
this.name = name;
this.id = id;
}
public HUser() {
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "HUser{" +
"id=" + id +
", name='" + name + ''' +
", age=" + age +
'}';
}
}
测试类,与原先的测试类类似,将序列化后的对象存储在H.txt
public class HessianTest {
public static void main(String[] args) throws IOException {
SerializeUser();// 序列化User对象
HUser user = DeserializeUser();// 反序列User对象
System.out.println("序列化之前:user:"+user);
// List list = new ArrayList();
}
private static HUser DeserializeUser() throws IOException {
FileInputStream fileInputStream = new FileInputStream("D:/H.txt");
byte[] data = new byte[1024];
int len = fileInputStream.read(data);
// 从流中读出对象
ByteArrayInputStream is = new ByteArrayInputStream(data);
Hessian2Input input = new Hessian2Input(is);
HUser user = (HUser) input.readObject();
System.out.println("User对象反序列化成功!");
return user;
}
private static void SerializeUser() throws IOException {
HUser user = new HUser("Jason",25,22);
System.out.println("序列化之前:user:"+user);
FileOutputStream fileOutputStream = new FileOutputStream("D:/H.txt");
// 从对象中获取字节流
ByteArrayOutputStream os = new ByteArrayOutputStream();
Hessian2Output output = new Hessian2Output(os);
output.writeObject(user);
output.getBytesOutputStream().flush();
output.completeMessage();
output.close();
fileOutputStream.write(os.toByteArray());
System.out.println("User对象序列化成功!");
}
}
输出:
序列化之前:user:HUser{id=25, name='Jason', age=22}
User对象序列化成功!
User对象反序列化成功!
序列化之前:user:HUser{id=25, name='Jason', age=0}
我们可以发现生成的文件,原先的T.txt是228字节,而H.txt只有112字节,确实是要更加的小。
需要注意的是:
需要序列化的对象还是需要实现java原生的Serializable 接口,如果不这么做的话会报错,参考如下源码:
// com.caucho.hessian.io.SerializerFactory 序列化时会获取默认序列化器
protected Serializer getDefaultSerializer(Class cl) {
if (_defaultSerializer != null)
return _defaultSerializer;
// 若序列化对象没有实现 Serializable 接口,则会抛出IllegalStateException
if (! Serializable.class.isAssignableFrom(cl)
&& ! _isAllowNonSerializable) {
throw new IllegalStateException("Serialized class " + cl.getName() + " must implement java.io.Serializable");
}
if (_isEnableUnsafeSerializer
&& JavaSerializer.getWriteReplace(cl) == null) {
return UnsafeSerializer.create(cl);
}
else
return JavaSerializer.create(cl);
}
若序列化对象经hessian序列化后,序列化对象中不加serialVersionUID时,再改变(增加对象属性、删除对象属性)都不会产生反序列化异常,即hessian序列化对象不再需要serialVersionUID。hessian会把复杂对象所有属性存储在一个 Map 中进行序列化。所以在父类、子类存在同名成员变量的情况下, Hessian 序列化时,先序列化子类,然后序列化父类,因此反序列化结果会导致子类同名成员变量被父类的值覆盖。hessian中的writeReplace方法与readResolve方法的作用一样,如果序列化的对象具有此方法,会利用此方法返回的实例来代替序列化后实例,用以保证对象的单例性。
Protobuf
Protobuf 是由 Google 推出且支持多语言的序列化框架,目前在主流网站上的序列化框架性能对比测试报告中,Protobuf 无论是编解码耗时,还是二进制流压缩大小,都名列前茅。
Protobuf 以一个 .proto 后缀的文件为基础,这个文件描述了字段以及字段类型,通过工具可以生成不同语言的数据结构文件。在序列化该数据对象的时候,Protobuf 通过.proto 文件描述来生成 Protocol Buffers 格式的编码。
Protocol Buffers 是一种轻便高效的结构化数据存储格式。它使用 T-L-V(标识 – 长度 – 字段值)的数据格式来存储数据,T 代表字段的正数序列 (tag),Protocol Buffers 将对象中的每个字段和正数序列对应起来,对应关系的信息是由生成的代码来保证的。在序列化的时候用整数值来代替字段名称,于是传输流量就可以大幅缩减;L 代表 Value 的字节长度,一般也只占一个字节;V 则代表字段值经过编码后的值。这种数据格式不需要分隔符,也不需要空格,同时减少了冗余字段名。
Protobuf 定义了一套自己的编码方式,几乎可以映射 Java/Python 等语言的所有基础数据类型。不同的编码方式对应不同的数据类型,还能采用不同的存储格式。如下图所示:
对于存储 Varint 编码数据,由于数据占用的存储空间是固定的,就不需要存储字节长度 Length,所以实际上 Protocol Buffers 的存储方式是 T – V,这样就又减少了一个字节的存储空间。
Protobuf 定义的 Varint 编码方式是一种变长的编码方式,每个字节的最后一位 (即最高位) 是一个标志位 (msb),用 0 和 1 来表示,0 表示当前字节已经是最后一个字节,1 表示这个数字后面还有一个字节。
对于 int32 类型数字,一般需要 4 个字节表示,若采用 Varint 编码方式,对于很小的 int32 类型数字,就可以用 1 个字节来表示。对于大部分整数类型数据来说,一般都是小于 256,所以这种操作可以起到很好地压缩数据的效果。
我们知道 int32 代表正负数,所以一般最后一位是用来表示正负值,现在 Varint 编码方式将最后一位用作了标志位,那还如何去表示正负整数呢?如果使用 int32/int64 表示负数就需要多个字节来表示,在 Varint 编码类型中,通过 Zigzag 编码进行转换,将负数转换成无符号数,再采用 sint32/sint64 来表示负数,这样就可以大大地减少编码后的字节数。
Protobuf 的这种数据存储格式,不仅压缩存储数据的效果好, 在编码和解码的性能方面也很高效。Protobuf 的编码和解码过程结合.proto 文件格式,加上 Protocol Buffer 独特的编码格式,只需要简单的数据运算以及位移等操作就可以完成编码与解码。可以说 Protobuf 的整体性能非常优秀。
原始的ProtoBuff需要自己写.proto文件,通过编译器将其转换为java文件,显得比较繁琐。百度研发的jprotobuf框架将Google原始的protobuf进行了封装,对其进行简化,仅提供序列化和反序列化方法。其实用上也比较简洁,通过对JavaBean中的字段进行注解就行,不需要撰写.proto文件和实用编译器将其生成.java文件,百度的jprotobuf都替我们做了这些事情了。
引入依赖:
<dependency>
<groupId>com.baidu</groupId>
<artifactId>jprotobuf</artifactId>
<version>2.4.9</version>
</dependency>
实体类改造:
@ProtobufClass
public class PUser{
@Protobuf(fieldType = FieldType.INT32, order = 1, required = true)
private int id;
@Protobuf(fieldType = FieldType.STRING, order = 2, required = true)
private String name;
@Protobuf(fieldType = FieldType.INT32, order = 3, required = true)
private int age;
public PUser(String name, int id, int age) {
this.name = name;
this.id=id;
this.age=age;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public PUser(String name, int id) {
this.name = name;
this.id = id;
}
public PUser() {
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "PUser{" +
"id=" + id +
", name='" + name + ''' +
", age=" + age +
'}';
}
}
测试代码:
public class ProtobufTest {
public static final Codec<PUser> simpleTypeCodec = ProtobufProxy
.create(PUser.class);
public static void main(String[] args) throws IOException {
SerializeUser();// 序列化User对象
PUser user = DeserializeUser();// 反序列User对象
System.out.println("序列化之后:puser:"+user);
}
private static PUser DeserializeUser() throws IOException {
FileInputStream fileInputStream = new FileInputStream("D:/P.txt");
int dataNum = 0;
StringBuilder sb = new StringBuilder();
for (;;) {
int n = fileInputStream.read(); // 反复调用read()方法,直到返回-1
if (n == -1) {
break;
}
sb.append((char)n);
dataNum++;
System.out.println(n); // 打印byte的值
}
byte[] data = sb.toString().getBytes();
System.out.println("PUser对象 data[len]:"+ Arrays.toString(data));
PUser user =simpleTypeCodec.decode(data);
System.out.println("PUser对象反序列化成功!");
return user;
}
private static void SerializeUser() throws IOException {
PUser user = new PUser("Jason",25,22);
System.out.println("序列化之前:Puser:"+user);
FileOutputStream fileOutputStream = new FileOutputStream("D:/P.txt");
// 序列化
byte[] bb = simpleTypeCodec.encode(user);
System.out.println("PUser对象 byte[len]:"+Arrays.toString(bb));
fileOutputStream.write(bb);
System.out.println("PUser对象序列化成功!");
}
}
输出:
序列化之前:Puser:PUser{id=25, name='Jason', age=22}
PUser对象 byte[len]:[8, 25, 18, 5, 74, 97, 115, 111, 110, 24, 22]
PUser对象序列化成功!
PUser对象 data[len]:[8, 25, 18, 5, 74, 97, 115, 111, 110, 24, 22]
PUser对象反序列化成功!
序列化之后:puser:PUser{id=25, name='Jason', age=22}
原文始发于微信公众号(云户):聊聊序列化–选型
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/25867.html