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()方法从一个源输入流中读取字节序列,再把它们反序列化为一个对象,并将其返回。

  1. package temp;
  2. import java.io.*;
  3. import java.util.Date;
  4. public class ObjectSaver {
  5. public static void main(String[] args) throws Exception {
  6. /*其中的 ./objectFile.obj 表示存放序列化对象的文件*/
  7. //序列化对象
  8. ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("./objectFile.obj"));
  9. Customer customer = new Customer("小韩", 24);
  10. out.writeObject("你好!"); //写入字面值常量
  11. out.writeObject(new Date()); //写入匿名Date对象
  12. out.writeObject(customer); //写入customer对象
  13. out.close();
  14. //反序列化对象
  15. ObjectInputStream in = new ObjectInputStream(new FileInputStream("./objectFile.obj"));
  16. System.out.println("obj1 " + (String) in.readObject()); //读取字面值常量
  17. System.out.println("obj2 " + (Date) in.readObject()); //读取匿名Date对象
  18. Customer obj3 = (Customer) in.readObject(); //读取customer对象
  19. System.out.println("obj3 " + obj3);
  20. in.close();
  21. }
  22. }
  23. class Customer implements Serializable {
  24. private String name;
  25. private int age;
  26. public Customer(String name, int age) {
  27. this.name = name;
  28. this.age = age;
  29. }
  30. public String toString() {
  31. return "name=" + name + ", age=" + age;
  32. }
  33. }

2、json序列化

Json序列化一般会使用jackson包,通过ObjectMapper类来进行一些操作,比如将对象转化为byte数组或者将json串转化为对象。现在的大多数公司都将json作为服务器端返回的数据格式。比如调用一个服务器接口,通常的请求为xxx.json?a=xxx&b=xxx的形式。Json序列化示例代码如下所示:

  1. package temp;
  2. import java.io.IOException;
  3. import java.util.ArrayList;
  4. import java.util.List;
  5. import com.fasterxml.jackson.databind.ObjectMapper;
  6. /**
  7. * @author liqqc
  8. */
  9. public class JsonSerialize {
  10. public static void main(String[] args) throws IOException {
  11. new JsonSerialize().start();
  12. }
  13. public void start() throws IOException {
  14. User u = new User();
  15. List<User> friends = new ArrayList<>();
  16. u.setUserName("张三");
  17. u.setPassWord("123456");
  18. u.setUserInfo("张三是一个很好的人");
  19. u.setFriends(friends);
  20. User f1 = new User();
  21. f1.setUserName("李四");
  22. f1.setPassWord("123456");
  23. f1.setUserInfo("李四是一个很坏的人");
  24. User f2 = new User();
  25. f2.setUserName("王五");
  26. f2.setPassWord("123456");
  27. f2.setUserInfo("王五是一个很好的人");
  28. friends.add(f1);
  29. friends.add(f2);
  30. ObjectMapper mapper = new ObjectMapper();
  31. Long t1 = System.currentTimeMillis();
  32. byte[] writeValueAsBytes = null;
  33. for (int i = 0; i < 10; i++) {
  34. writeValueAsBytes = mapper.writeValueAsBytes(u);
  35. }
  36. System.out.println("json serialize: " + (System.currentTimeMillis() - t1) + "ms; 总大小:" + writeValueAsBytes.length);
  37. Long t2 = System.currentTimeMillis();
  38. User user = mapper.readValue(writeValueAsBytes, User.class);
  39. System.out.println("json deserialize: " + (System.currentTimeMillis() - t2) + "ms; User: " + user);
  40. }
  41. }

3、Fastjson序列化

fastjson 是由阿里巴巴开发的一个性能很好的Java 语言实现的 Json解析器和生成器。特点:速度快,测试表明fastjson具有极快的性能,超越任其他的java json parser。功能强大,完全支持java bean、集合、Map、日期、Enum,支持范型和自省。无依赖,能够直接运行在Java SE 5.0以上版本
支持Android。使用时候需引入FastJson第三方jar包。FastJson序列化代码示例如下所示:

  1. package temp;
  2. import java.util.ArrayList;
  3. import java.util.List;
  4. import com.alibaba.fastjson.JSON;
  5. /**
  6. * @author liqqc
  7. */
  8. public class FastJsonSerialize {
  9. public static void main(String[] args) {
  10. new FastJsonSerialize().start();
  11. }
  12. public void start() {
  13. User u = new User();
  14. List<User> friends = new ArrayList<>();
  15. u.setUserName("张三");
  16. u.setPassWord("123456");
  17. u.setUserInfo("张三是一个很牛逼的人");
  18. u.setFriends(friends);
  19. User f1 = new User();
  20. f1.setUserName("李四");
  21. f1.setPassWord("123456");
  22. f1.setUserInfo("李四是一个很牛逼的人");
  23. User f2 = new User();
  24. f2.setUserName("王五");
  25. f2.setPassWord("123456");
  26. f2.setUserInfo("王五是一个很牛逼的人");
  27. friends.add(f1);
  28. friends.add(f2);
  29. //序列化
  30. Long t1 = System.currentTimeMillis();
  31. String text = null;
  32. for (int i = 0; i < 10; i++) {
  33. text = JSON.toJSONString(u);
  34. }
  35. System.out.println("fastJson serialize: " + (System.currentTimeMillis() - t1) + "ms; 总大小:" + text.getBytes().length);
  36. //反序列化
  37. Long t2 = System.currentTimeMillis();
  38. User user = JSON.parseObject(text, User.class);
  39. System.out.println("fastJson serialize: " + (System.currentTimeMillis() - t2) + "ms; User: " + user);
  40. }
  41. }

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进行一个简单的封装设计,从而可以多线程安全的使用序列化和反序列化