序列化

一、什么是序列化

简单来说,序列化就是将对象转换为字节流,反序列化就是将字节流转化为对象。

Java的对象序列化将那些实现了Serializable接口的对象转换为一个字节序列,并能够在以后将这个字节序列完全恢复为原来的对象。这一过程甚至可通过网络进行;这意味着序列化机制能自动弥补不同操作系统之间的差异。也也就是说,可以在运行Windows系统的计算机上创建一个对象,将其序列化,通过网络将它发送个一台运行Unix系统的计算机,然后在那里准确地重新组装,而却不必担心数据在不同机器上的表示会不同,也不必关心字节的顺序或者其他的任何细节。

对象的序列化可以利用它实现轻量级持久化。“持久性”意味着一个对象的生存周期并不取决于程序是否正在执行;它可以生存于程序的调用之间。通过将一个序列化对象写入磁盘,然后在重新调用程序时恢复该对象,就能够实现持久性的效果。之前写到的Session的持久化策略就是使用了序列化的方式进行了实现(保持对象的状态)。

二、序列化的应用场景

  1. 序列化输出到文件,读取文件反序列化为对象。如tomcat实现session持久化
  2. 网络传输,发送方序列化对象为字节流,接收方反序列化为对象。如dubbo的hessian

三、JAVA原生序列化

使用方式

  1. 如果类的字段表示的就是类的逻辑信息,如基本的POJO类,那就可以使用默认序列化机制,只要声明实现Serializable接口即可
  2. 否则的话,如LinkedList,那就可以使用transient关键字,实现writeObject和readObject自定义序列化过程。
  3. Java的序列化机制可以自动处理如引用同一个对象、循环引用等情况。

序列化原理

序列化到底是如何发生的?关键在ObjectInputStream的readObject和ObjectOutputStream的writeObject
方法中,它们的实现都非常复杂,正因为这些复杂的实现才使得序列化看上去很神奇,我们简单介绍其基本逻辑。

writeObject的基本逻辑是:

  1. 如果对象没有实现Serializable,抛出异常NotSerializableException
  2. 每个对象都有一个编号,如果之前已经写过该对象了,则本次只会写该对象的引用,这可以解决对象引用和循环引用的问题。
  3. 如果对象实现了writeObject方法,调用它的自定义方法
  4. 默认是利用反射机制,遍历对象结构图,对每个没有标记为transient的字段,根据其类型,分别进行处理,写出到流,流中的信息包括字段的类型,即完整类型、字段名、字段值等。

readObject的基本逻辑是:

  1. 不调用任何的构造方法
  2. 它自己就相当于是一个独立的构造方法,根据字节流初始化对象,利用的也是反射机制
  3. 在解析字节流时,对于引用到的类型信息,会动态加载,如果找不到类,会抛出ClassNotFoundException

简单实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
/**
* @author lemon
* @date 2019-08-04
* @desc java Bean
*/
public class Dog implements Serializable{ //此处若不实现Serializable将会报错,详见报错1
//对于已发行版本若serialVersionUID改动 在反序列化的时候会报错2
private static final long serialVersionUID = 4115425178199685592L;
public String name;
}

/**
* @author lemon
* @date 2019-08-04
* @desc 测试类
*/
public class TestSer {
@Test
public void testInput() throws IOException{
ObjectInputStream inputStream = new ObjectInputStream(
new BufferedInputStream(
new FileInputStream("/Users/lvxiran/dev/input/dog.text")
)
);
try {

int size = inputStream.readInt();
List<Dog> dogs = new ArrayList<Dog>(size);
for (int i = 0; i < size; i++) {
try {
dogs.add((Dog) inputStream.readObject());
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}

System.out.println(JSON.toJSONString(dogs));


} catch (IOException e) {
e.printStackTrace();
} finally {
inputStream.close();
}
}

@Test
public void testOutPut() {
try {

List<Dog> dogs = new ArrayList<Dog>();
Dog dog = new Dog();
dog.name = "123";
dogs.add(dog);
Dog dog2 = new Dog();
dog2.name = "234";
dogs.add(dog2);

//输出文本详见下方dog.txt 字节流
ObjectOutputStream outputStream = new ObjectOutputStream(
new BufferedOutputStream(new FileOutputStream("/Users/lvxiran/dev/input/dog.text"))
);

outputStream.writeInt(dogs.size());
for (Dog oneDog : dogs) {
outputStream.writeObject(oneDog);
}

outputStream.flush();
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}

保存文件dog.txt 字节流信息

1
2
3
4
5
6
7
aced 0005 7372 0024 636f 6d2e 6a61 7661
6c65 6d6f 6e2e 6d6f 6465 6c2e 7365 7269
616c 697a 6162 6c65 2e44 6f67 46fd a418
cdae 79d8 0200 014c 0004 6e61 6d65 7400
124c 6a61 7661 2f6c 616e 672f 5374 7269
6e67 3b78 7074 0003 3132 3373 7100 7e00
0074 0003 3233 34

错误信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
报错1:没有实现序列化
java.io.NotSerializableException: com.javalemon.model.serializable.Dog
at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1184)
at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:348)
at com.javalemon.model.serializable.TestSer.testOutPut(TestSer.java:65)
报错2:修改了序列版本id,导致反序列化失败
java.io.InvalidClassException: com.javalemon.model.serializable.Dog; local class incompatible: stream classdesc serialVersionUID = 5115425178199685592, local class serialVersionUID = 4115425178199685592
at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:687)
at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1876)
at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1745)
at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2033)
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1567)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:427)
at com.javalemon.model.serializable.TestSer.testInput(TestSer.java:31)

特点分析

优点:

  1. 使用简单
  2. 可自动处理对象引用和循环引用
  3. 方便处理版本问题

缺点:

  1. 不能实现跨语言的数据交换
  2. java在序列化字节中保存了很多描述信息,使得序列化格式比较大
  3. 使用反射分析遍历对象结构,性能 比较低
  4. 序列化格式是二进制的,不方便查看和修改

四、常见的序列化及其效率对比

dubbo序列化:阿里尚未开发成熟的高效java序列化实现,阿里不建议在生产环境使用它

hessian2序列化:hessian是一种跨语言的高效二进制序列化方式。但这里实际不是原生的hessian2序列化,而是阿里修改过的hessian lite,它是dubbo RPC默认启用的序列化方式

json序列化:目前有两种实现,一种是采用的阿里的fastjson库,另一种是采用dubbo中自己实现的简单json库,但其实现都不是特别成熟,而且json这种文本序列化性能一般不如上面两种二进制序列化。

java序列化:主要是采用JDK自带的Java序列化实现,性能很不理想 在通常情况下,这四种主要序列化方式的性能从上到下依次递减。 但hessian是一个比较老的序列化实现了,而且它是跨语言的,所以不是单独针对java进行优化的。而dubbo RPC实际上完全是一种Java to Java的远程调用,其实没有必要采用跨语言的序列化方式(当然肯定也不排斥跨语言的序列化)。

高效序列化方式: 专门针对Java语言的:Kryo,FST等等 跨语言的:Protostuff,ProtoBuf,Thrift,Avro,MsgPack等等 这些序列化方式的性能多数都显著优于hessian2(甚至包括尚未成熟的dubbo序列化)。 其中Kryo是一种非常成熟的序列化实现,已经在Twitter、Groupon、Yahoo以及多个著名开源项目(如Hive、Storm)中广泛的使用。而FST是一种较新的序列化实现,目前还缺乏足够多的成熟使用案例,但我认为它还是非常有前途的。

五、 序列化接口及无参构造函数

如果被序列化的类中不包含无参的构造函数,则在Kryo的序列化中,性能将会大打折扣,因为此时我们在底层将用Java的序列化来透明的取代Kryo序列 化。所以,尽可能为每一个被序列化的类添加无参构造函数是一种最佳实践(当然一个java类如果不自定义构造函数,默认就有无参构造函数)。

Java原生序列化、hessian都需实现Serializable接口,另外,Kryo和FST本来都不需要被序列化都类实现Serializable接口,但我们还是建议每个被序列化类都去实现它,因为这样可以保持和Java序列化以及dubbo序列化的兼容性,另外也使我们未来采用上述某些自动注册机制带来可能。

友情链接:

源代码地址
序列化性能对比
SpringMvc中反序列化与序列化的原理

lemon wechat
欢迎大家关注我的订阅号 SeeMoonUp
写的不错?鼓励一下?不差钱?