0、背景
当两个进程在进行远程通信时,彼此可以发送各种类型的数据。无论是何种类型的数据,都会以二进制序列的形式在网络上传送。发送方需要把这个Java对象转换为字节序列,才能在网络上传送;接收方则需要把字节序列再恢复为Java对象。
本文主要对几种常见Java序列化方式进行实现。包括Java原生以流的方法进行的序列化、Json序列化、FastJson序列化、Protobuff序列化、Hessian序列化和Kyro序列化。
1、Java原生序列化方式
这种方式只能将支持 java.io.Serializable 接口的对象写入流中。每个 serializable 对象的类都被编码,编码内容包括类名和类签名、对象的字段值和数组值,以及从初始对象中引用的其他所有对象的闭包。
java.io.ObjectOutputStream代表对象输出流,它的writeObject(Object obj)方法可对参数指定的obj对象进行序列化,把得到的字节序列写到一个目标输出流中。只有实现了Serializable和Externalizable接口的类的对象才能被序列化。
java.io.ObjectInputStream代表对象输入流,它的readObject()方法从一个源输入流中读取字节序列,再把它们反序列化为一个对象,并将其返回。
package temp;import java.io.*;import java.util.Date;public class ObjectSaver {public static void main(String[] args) throws Exception {/*其中的 ./objectFile.obj 表示存放序列化对象的文件*///序列化对象ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("./objectFile.obj"));Customer customer = new Customer("小韩", 24);out.writeObject("你好!"); //写入字面值常量out.writeObject(new Date()); //写入匿名Date对象out.writeObject(customer); //写入customer对象out.close();//反序列化对象ObjectInputStream in = new ObjectInputStream(new FileInputStream("./objectFile.obj"));System.out.println("obj1 " + (String) in.readObject()); //读取字面值常量System.out.println("obj2 " + (Date) in.readObject()); //读取匿名Date对象Customer obj3 = (Customer) in.readObject(); //读取customer对象System.out.println("obj3 " + obj3);in.close();}}class Customer implements Serializable {private String name;private int age;public Customer(String name, int age) {this.name = name;this.age = age;}public String toString() {return "name=" + name + ", age=" + age;}}
2、json序列化
Json序列化一般会使用jackson包,通过ObjectMapper类来进行一些操作,比如将对象转化为byte数组或者将json串转化为对象。现在的大多数公司都将json作为服务器端返回的数据格式。比如调用一个服务器接口,通常的请求为xxx.json?a=xxx&b=xxx的形式。Json序列化示例代码如下所示:
package temp;import java.io.IOException;import java.util.ArrayList;import java.util.List;import com.fasterxml.jackson.databind.ObjectMapper;/*** @author liqqc*/public class JsonSerialize {public static void main(String[] args) throws IOException {new JsonSerialize().start();}public void start() throws IOException {User u = new User();List<User> friends = new ArrayList<>();u.setUserName("张三");u.setPassWord("123456");u.setUserInfo("张三是一个很好的人");u.setFriends(friends);User f1 = new User();f1.setUserName("李四");f1.setPassWord("123456");f1.setUserInfo("李四是一个很坏的人");User f2 = new User();f2.setUserName("王五");f2.setPassWord("123456");f2.setUserInfo("王五是一个很好的人");friends.add(f1);friends.add(f2);ObjectMapper mapper = new ObjectMapper();Long t1 = System.currentTimeMillis();byte[] writeValueAsBytes = null;for (int i = 0; i < 10; i++) {writeValueAsBytes = mapper.writeValueAsBytes(u);}System.out.println("json serialize: " + (System.currentTimeMillis() - t1) + "ms; 总大小:" + writeValueAsBytes.length);Long t2 = System.currentTimeMillis();User user = mapper.readValue(writeValueAsBytes, User.class);System.out.println("json deserialize: " + (System.currentTimeMillis() - t2) + "ms; User: " + user);}}
3、Fastjson序列化
fastjson 是由阿里巴巴开发的一个性能很好的Java 语言实现的 Json解析器和生成器。特点:速度快,测试表明fastjson具有极快的性能,超越任其他的java json parser。功能强大,完全支持java bean、集合、Map、日期、Enum,支持范型和自省。无依赖,能够直接运行在Java SE 5.0以上版本
支持Android。使用时候需引入FastJson第三方jar包。FastJson序列化代码示例如下所示:
package temp;import java.util.ArrayList;import java.util.List;import com.alibaba.fastjson.JSON;/*** @author liqqc*/public class FastJsonSerialize {public static void main(String[] args) {new FastJsonSerialize().start();}public void start() {User u = new User();List<User> friends = new ArrayList<>();u.setUserName("张三");u.setPassWord("123456");u.setUserInfo("张三是一个很牛逼的人");u.setFriends(friends);User f1 = new User();f1.setUserName("李四");f1.setPassWord("123456");f1.setUserInfo("李四是一个很牛逼的人");User f2 = new User();f2.setUserName("王五");f2.setPassWord("123456");f2.setUserInfo("王五是一个很牛逼的人");friends.add(f1);friends.add(f2);//序列化Long t1 = System.currentTimeMillis();String text = null;for (int i = 0; i < 10; i++) {text = JSON.toJSONString(u);}System.out.println("fastJson serialize: " + (System.currentTimeMillis() - t1) + "ms; 总大小:" + text.getBytes().length);//反序列化Long t2 = System.currentTimeMillis();User user = JSON.parseObject(text, User.class);System.out.println("fastJson serialize: " + (System.currentTimeMillis() - t2) + "ms; User: " + user);}}
4、ProtoBuff序列化
ProtocolBuffer是一种轻便高效的结构化数据存储格式,可以用于结构化数据序列化。适合做数据存储或 RPC 数据交换格式。可用于通讯协议、数据存储等领域的语言无关、平台无关、可扩展的序列化结构数据格式。 优点:跨语言;序列化后数据占用空间比JSON小,JSON有一定的格式,在数据量上还有可以压缩的空间。 缺点:它以二进制的方式存储,无法直接读取编辑,除非你有 .proto 定义,否则无法直接读出 Protobuffer的任何内容。 其与thrift的对比:两者语法类似,都支持版本向后兼容和向前兼容,thrift侧重点是构建跨语言的可伸缩的服务,支持的语言多,同时提供了全套RPC解决方案,可以很方便的直接构建服务,不需要做太多其他的工作。 Protobuffer主要是一种序列化机制,在数据序列化上进行性能比较,Protobuffer相对较好。
ProtoBuff序列化对象可以很大程度上将其压缩,可以大大减少数据传输大小,提高系统性能。对于大量数据的缓存,也可以提高缓存中数据存储量。原始的ProtoBuff需要自己写.proto文件,通过编译器将其转换为java文件,显得比较繁琐。百度研发的jprotobuf框架将Google原始的protobuf进行了封装,对其进行简化,仅提供序列化和反序列化方法。其实用上也比较简洁,通过对JavaBean中的字段进行注解就行,不需要撰写.proto文件和实用编译器将其生成.java文件,百度的jprotobuf都替我们做了这些事情了。
第一步:使用注解的方式写User类为protoBuff序列化做准备。
第二步:写jprotobuf序列化代码。
5、Hessian序列化
Hessian是一个支持跨语言传输的二进制文本序列化协议,对比Java默认的序列化,Hessian的使用较简单,并且性能较高,现在的主流远程通讯框架几乎都支持Hessian,比如Dubbo,默认使用的就是Hessian,不过是Hessian的重构版。
和java原生的Serializable序列化比较,hessian序列化的效率更高,且序列化的数据更小,在基于RPC的调用方式中性能更好。
Hessian序列化是一种支持动态类型、跨语言、基于对象传输的网络协议,Java对象序列化的二进制流可以被其他语言(如,c++,python)。特性如下:
自描述序列化类型。不依赖外部描述文件或者接口定义,用一个字节表示常用的基础类型,极大缩短二进制流。 语言无关,支持脚本语言 协议简单,比Java原生序列化高效 相比hessian1,hessian2中增加了压缩编码,其序列化二进制流大小事Java序列化的50%,序列化耗时是Java序列化的30%,反序列化耗时是Java序列化的20%。 Hessian会把复杂的对象所有属性存储在一个Map中进行序列化。所以在父类、子类中存在同名成员变量的情况下,hessian序列化时,先序列化子类,然后序列化父类。因此,反序列化结果会导致子类同名成员变量被父类的值覆盖。 换个思路,既然你继承了一个父类,当然希望复用的越多越好,所以,使用hessian序列化的时候,避免开这一点就行了。
6、Kyro序列化
Kyro序列化是主流的比较成熟的序列化方案之一,目前广泛使用在大数据组件中,比如Hive**、**Storm等,性能比起Hessian还要优越,但是缺陷较明显,不支持跨语言交互,在dubbo2.6.x版本开始已经加入了Kyro序列化的支持。
kryo是一个高性能的序列化/反序列化工具,由于其变长存储特性并使用了字节码生成机制,拥有较高的运行速度和较小的体积。
kryo可以记录类型信息,这算是kryo的一大特点了,就是可以把对象信息直接写到序列化数据里,反序列化的时候可以精确地找到原始类信息,不会出错,这意味着在写readxxx方法时,无需传入Class或Type类信息。相应的,kryo提供两种读写方式。记录类型信息的writeClassAndObject/readClassAndObject方法,以及传统的writeObject/readObject方法。
kryo性能非常好。比kyro更高效的序列化库就只有google的protobuf了(而且两者性能很接近),protobuf有个缺点就是要传输的每一个类的结构都要生成对应的proto文件(也可以都放在同一个proto文件中,如果考虑到扩展性的话,不建议放在一个proto文件中),如果某个类发生修改,还得重新生成该类对应的proto文件;另外考虑到项目中用的全部是java技术栈,不存在不同编程语言间的兼容性问题,因此最终采用了kryo作为序列化库。
使用场景:(数据交换或数据持久化)比如使用kryo把对象序列化成字节数组发送给消息队列或者放到redis等Nosql中等等应用场景。
注意:由于kryo不是线程安全的,针对多线程情况下的使用,要对kryo进行一个简单的封装设计,从而可以多线程安全的使用序列化和反序列化
