-
Notifications
You must be signed in to change notification settings - Fork 32
/
Copy path序列化与其他序列化框架.md
430 lines (266 loc) · 18.5 KB
/
序列化与其他序列化框架.md
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
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
### 序列化
#### 理解
序列化并不是说要把对象或者消息直接编码为二进制串,而是转化为本地某一种可以持久化或者传输的结构。编码为二进制串是网络七层模型中第六层表现层完成(通过获得序列化后文件的输入流,然后进行编码即可)。
#### Java API相关
**Serializable 接口**
- 是一个标记接口,不用实现任何方法。一旦实现了此接口,该类的对象就是可序列化的。
- Serializable接口的反序列化并不会调用构造方法。对象是由JVM自己生成的对象,不通过构造方法生成。因此,它存在一定的安全隐患。一个精心构造的`byte[]`数组被反序列化后可以执行特定的Java代码,从而导致严重的安全漏洞。
序列化步骤:
- 步骤一:创建一个ObjectOutputStream输出流;
- 步骤二:调用ObjectOutputStream对象的writeObject输出可序列化对象。
- 注意事项:
- 如果一个可序列化的类的某个属性不是基本类型,也不是String类型,那这个引用类型也必须是可序列化的;否则,会导致此类不能序列化。
- 在同一次ObjectOutputStream写出时,如果一个对象写出多次,则从第二次开始,都是引用的第一次写出的对象。潜在的问题是如果序列化一个可变对象(对象内的内容可更改)后,更改了对象内容,再次序列化,并不会再次将此对象转换为字节序列,而只是保存该对象第一次序列化的编号。
反序列化步骤:
- 步骤一:创建一个ObjectInputStream输入流;
- 步骤二:调用ObjectInputStream对象的readObject()得到序列化的对象。
自定义序列化时属性
(可以自己选择哪些属性需要序列化,或者对序列化数据进行编码加密等。)
- 向ObjectOutputStream写出对象的哪些属性
```private void writeObject(ObjectOutputStream out) throws IOException ```
- 向ObjectIutputStream读取哪些属性,然后给对象赋值
```private void readObject(ObjectIutputStream in) throws IOException,ClassNotFoundException;```
- 不同类接收反序列化对象,或者序列化流被篡改时,系统都会调用readObjectNoData()
```private void readObjectNoData() throws ObjectStreamException```
```
public class Person implements Serializable {
private String name;
private int age; //省略构造方法,get及set方法
//将名字反转写入二进制流
private void writeObject(ObjectOutputStream out) throws IOException {
out.writeObject(new StringBuffer(this.name).reverse());
out.writeInt(age);
}
// 将读出的字符串反转恢复回来
private void readObject(ObjectInputStream ins) throws IOException,
ClassNotFoundException{
this.name = ((StringBuffer)ins.readObject()).reverse().toString();
this.age = ins.readInt();
}
}
```
自定义序列化的对象
- 决定序列化的对象,会先调用此方法,再调用writeObject方法。
```private Object writeReplace() throws ObjectStreamException```
- 决定反序列化的对象,反序列化出来的对象被立即丢弃,在readeObject后调用(常用来反序列单例类,保证单例类的唯一性)
```private Object readResolve() throws ObjectStreamException```
**Externalizable接口**
- 强制自定义序列化,必须实现writeExternal、readExternal方法,效果等同writeObject和readOject。
- 必须提供pulic的无参构造器,因为在反序列化的时候需要反射创建对象
**对比**
- Serializable是由系统自动存储必要的信息,Externalizable由程序员实现两个方法决定
- Externalizable接口带来了一定性能提升,但复杂度也提高了,所以一般通过实现Serializable进行序列化。
**SerialVersionUID**
- 保证升级前后的兼容性,如果反序列化使用的class的版本号与序列化时使用的不一致,反序列化会报InvalidClassException异常。
- 序列化版本号可自由指定,如果不指定,每次JVM会根据类信息给类计算一个版本号,这样每次重新运行程序都无法反序列化之前生成的对象。
- 必须要指定为static才会被jvm使用,因为是类的信息:```private static final long serialVersionUID = 42L;```
- 不指定版本号另一个明显隐患是,不利于jvm间的移植,可能class文件没有更改,但不同jvm可能计算的规则不一样,这样也会导致无法反序列化。
- 什么情况下需要修改serialVersionUID呢?
- 如果只是修改了方法,反序列化不容影响,则无需修改版本号;
- 如果只是修改了静态变量,瞬态变量(transient修饰的变量),反序列化不受影响,无需修改版本号;
- 如果只是新增了实例变量,则反序列化回来新增的是默认值;如果减少了实例变量,反序列化时会忽略掉减少的实例变量,此种也无需修改(即反序列化,就算基本类型对应的值是空值,也不会反序列化异常)
- 如果修改了非瞬态变量,则可能导致反序列化失败。如果新类中实例变量的类型与序列化时类的类型不一致,则会反序列化失败,这时候需要更改serialVersionUID。
**Transient**
使用transient修饰的属性,java序列化时,会忽略掉此字段,所以反序列化出的对象,被transient修饰的属性是默认值。对于引用类型,值是null;基本类型,值是0;boolean类型,值是false;char是' '。
#### 序列化协议
**特性**
1、通用性
2、强健性/鲁棒性
3、可调试性/可读性
4、性能
5、可扩展性/兼容性
6、安全性/访问限制
**序列化和反序列化的组件**
- IDL(Interface description language)文件:参与通讯的各方需要`对通讯的内容需要做相关的约定`(Specifications)。为了建立一个与语言和平台无关的约定,这个约定需要采用与具体开发语言、平台无关的语言来进行描述。这种语言被称为接口描述语言(IDL),采用IDL撰写的协议约定称之为IDL文件。
- IDL Compiler:IDL文件中约定的内容为了在各语言和平台可见,需要有一个编译器,`将IDL文件转换成各语言对应的动态库(即由IDL文件,根据不同语言生成相应的类文件,比如.java文件)`。
- Stub/Skeleton Lib:负责序列化和反序列化的工作代码。实现对象的序列化与反序列化。
- Client/Server:指的是应用层程序代码,他们面对的是IDL所生存的特定语言的class或struct。
- 底层协议栈和互联网:序列化之后的数据通过底层的传输层、网络层、链路层以及物理层协议转换成数字信号在互联网中传递。
**XML&SOAP**
XML是一种常用的序列化和反序列化协议,具有跨机器,跨语言等优点。
Soap(消息传递协议,规定的消息结构,并不是七层模型中应用层的协议,因为序列化后不是二进制串,所以一般依托HTTP协议传输)
- SOAP (Simple Object Access Protocol,简单对象访问协议) 是HTTP POST的一个专用版本,遵循一种特殊的XML消息格式Content-type设置为: text/xml任何数据都可以xml化。
- SOAP:简单对象访问协议,简单对象访问协议(SOAP)是一种轻量的、简单的、基于 XML 的协议,它的IDL是WSDL。它被设计成`在 WEB 上交换结构化的和固化的信息`。
- SOAP 可以和现存的许多因特网协议和格式结合使用,包括超文本传输协议( HTTP),简单邮件传输协议(SMTP),多用途网际邮件扩充协议(MIME)。它还支持从消息系统到远程过程调用(RPC)等大量的应用程序。
- SOAP是一个基于XML的交换消息协议,可以使用HTTP来传输这些信息。事实上HTTP是SOAP消息的最常见的传输工具。
- SOAP将信息进行XML的序列化后,再用HTTP协议的方式再打包(即作为请求体)进行传送,传送的方式还是TCP或者UDP。SOAP也可以绑定到TCP和UDP协议上。
- Webservice就是使用Soap协议得到你想要的东西,相比Httpservice`能处理些更加复杂的数据类型`
SOAP优势
- SOAP协议具有广泛的群众基础,基于HTTP的传输协议使得其在穿越防火墙时具有良好安全特性
- XML所具有的人眼可读(Human-readable)特性使得其具有出众的可调试性
- 互联网带宽的日益剧增也大大弥补了其空间开销大(Verbose)的缺点
- 对于在公司之间传输数据量相对小或者实时性要求相对低(例如秒级别)的服务是一个好的选择。
SOAP劣势
- 由于XML的额外空间开销大,序列化之后的数据量剧增,对于数据量巨大序列持久化应用常景,这意味着巨大的内存和磁盘开销,不太适合XML。
- 另外,XML的序列化和反序列化的空间和时间开销都比较大,对于对性能要求在ms级别的服务,不推荐用。
- WSDL虽然具备了描述对象的能力,SOAP的S代表的也是simple,但是SOAP的使用绝对不简单。对于习惯于面向对象编程的用户,WSDL文件不直观。
**JSON**
优势
- 这种Associative array格式非常符合工程师对对象的理解。
- 它保持了XML的人眼可读(Human-readable)的优点。
- 相对于XML而言,序列化后的数据更加简洁。 来自于的以下链接的研究表明:XML所产生序列化之后文件的大小接近JSON的两倍。
- 它具备Javascript的先天性支持,所以被广泛应用于Web browser的应用常景中,是Ajax的事实标准协议。
- 与XML相比,其协议比较简单,解析速度比较快。
- 松散的Associative array使得其具有良好的可扩展性和兼容性(松散是指对json序列化和反序列化时,均没有IDL文件对属性的约定,而是全由该对象目前有哪些属性决定)。
应用场景包括:
- 公司之间传输数据量相对小,实时性要求相对低(例如秒级别)的服务。
- 基于Web browser的Ajax请求。
- 由于JSON具有非常强的前后兼容性(序列化和反序列化时对属性的有无没任何要求),对于接口经常发生变化,并对可调式性要求高的场景,例如Mobile app与服务端的通讯。
- 由于JSON的典型应用场景是JSON+HTTP,适合跨防火墙访问。
不适合的场景
- 总的来说,采用JSON进行序列化的额外空间开销比较大,对于大数据量服务或持久化,这意味着巨大的内存和磁盘开销,这种场景不适合。
- 没有统一可用的IDL降低了对参与方的约束,实际操作中往往只能采用文档方式来进行约定,这可能会给调试带来一些不便,延长开发周期。
- 由于JSON在一些语言中的序列化和反序列化需要采用反射机制,所以在性能要求为ms级别,不建议使用。
**Protobuf**
典型特征:
- 标准的IDL和IDL编译器,这使得其对工程师非常友好。
- 序列化数据非常简洁,紧凑,与XML相比,其序列化之后的数据量约为1/3到1/10。
- 解析速度非常快,比对应的XML快约20-100倍。
- 提供了非常友好的动态库,使用非常简介,反序列化只需要一行代码。
```protoc -I=$SRC_DIR --java_out=$DST_DIR $SRC_DIR/addressbook.proto```
劣势
- Protobuf主要问题在于其所支持的语言相对较少
- 另外由于没有绑定的标准底层传输层协议,在公司间进行传输层协议的调试工作相对麻烦。
应用场景
- 空间开销小以及高解析性能(只需要简单的位运算),非常适合于公司内部的对性能要求高的RPC传输使用。
- 由于Protobuf提供了标准的IDL以及对应的编译器,其IDL文件是参与各方的非常强的业务约束,
- 另外,`Protobuf与传输层无关`,所以也可以采用HTTP具有良好的跨防火墙的访问属性,所以Protobuf也适用于公司间对性能要求比较高的场景。
- 由于其解析性能高,序列化后数据量相对少,非常适合应用层对象的持久化场景。
- 传输数据量较大的需求场景下,比XML、Json 更小、更快、使用 & 维护更简单!
IDL文件举例(会编译成.java文件)
```
syntax = "proto2";
package tutorial;
option java_package = "com.example.tutorial";
option java_outer_classname = "AddressBookProtos";
message Person { // 生成静态内部类com.example.tutorial.AddressBookProtos.Person;
required string name = 1; // 每个'='后的数字是指在二进制文件里排在属性的第几个位置
required int32 id = 2;
optional string email = 3;
enum PhoneType { //生成静态内部枚举类 ...AddressBookProtos.Person,PhoneType
MOBILE = 0;
HOME = 1;
WORK = 2;
}
message PhoneNumber { // 静态内部类 ...AddressBookProtos.Person,PhoneNumber
required string number = 1;
optional PhoneType type = 2 [default = HOME];
}
repeated PhoneNumber phones = 4; // repeated类型会生成List类型。
}
message AddressBook { // 生成静态内部类com.example.tutorial.AddressBookProtos.AddressBook;
repeated Person people = 1;
}
```
**Avro**
典型特征:
- Avro的产生解决了JSON的冗长和没有IDL的问题,使用JSON格式的Schema作为其IDL。
- Avro提供两种序列化格式:JSON格式或者Binary格式。
- Binary格式在空间开销和解析性能方面可以和Protobuf媲美
- JSON格式方便测试阶段的调试。
- Avro支持的数据类型非常丰富,包括C++语言里面的union类型。
- Schema可以在传输数据的同时发送,加上JSON的自我描述属性,这使得Avro非常适合动态类型语言。同时服务端和客户端可以在握手阶段对Schema进行互相确认,大大提高了最终的数据解析速度。
- Avro在做文件持久化的时候,一般会和Schema一起存储,所以Avro序列化文件自身具有自我描述属性(其他协议序列化后的文件只有属性的值,但没有属性相关信息),所以非常适合于做Hive、Pig和MapReduce的持久化数据格式。
应用场景
- Avro解析性能高并且序列化之后的数据非常简洁,比较适合于高性能的序列化服务。
- 由于Avro目前非JSON格式的IDL处于实验阶段,而JSON格式的IDL对于习惯于静态类型语言的工程师不直观。
IDL格式
```avrasm
protocol Userservice {
record Address {
string city;
string postcode;
string street;
}
record UserInfo {
string name;
int userid;
array<Address> address = [];
}
}
```
该IDL对应的JSON Schema格式
```
{
"protocol" : "Userservice", // 该Schema描述对象
"namespace" : "org.apache.avro.ipc.specific", // 包名
"version" : "1.0.5", // 版本
"types" : [ {
"name" : "Address", // 类
"type" : "record"
"fields" : [ { // 字段
"name" : "city",
"type" : "string"
}, {
"name" : "postcode",
"type" : "string"
}, {
"name" : "street",
"type" : "string"
} ]
}, {
"name" : "UserInfo",
"type" : "record"
"fields" : [ {
"name" : "name",
"type" : "string"
}, {
"name" : "userid",
"type" : "int"
}, {
"name" : "address",
"type" : {
"type" : "array",
"items" : "Address"
},
"default" : [ ]
} ]
} ],
"messages" : { }
}
```
**Thrift**
典型特征
- Thrift是Facebook开源提供的一个高性能,轻量级RPC服务框架,其产生正是为了满足当前大数据量、分布式、跨语言、跨平台数据通讯的需求。
- Thrift并不仅仅是序列化协议,而是一个RPC框架。
- 相对于JSON和XML而言,Thrift在空间开销和解析性能上有了比较大的提升,对于对性能要求比较高的分布式系统,它是一个优秀的RPC解决方案
- 但是由于Thrift的序列化被嵌入到Thrift框架里面,Thrift框架本身并没有透出序列化和反序列化接口,这导致其很难和其他传输层协议共同使用(例如HTTP)。
应用场景
- 对于需求为高性能,分布式的RPC服务,Thrift是一个优秀的解决方案。
- 它支持众多语言和丰富的数据类型,并对于数据字段的增删具有较强的兼容性。所以非常适用于作为公司内部的面向服务构建(SOA)的标准RPC框架。
劣势
- Thrift的文档相对比较缺乏,目前使用的群众基础相对较少。
- 另外由于其Server是基于自身的Socket服务,所以在跨防火墙访问时,安全是一个顾虑,所以在公司间进行通讯时需要谨慎。
- 另外Thrift序列化之后的数据是Binary数组,不具有可读性,调试代码时相对困难。
- 最后,由于Thrift的序列化和框架紧耦合,无法支持向持久层直接读写数据,所以不适合做数据持久化序列化协议。
-
IDL文件举例
```
struct Address
{
1: required string city;
2: optional string postcode;
3: optional string street;
}
struct UserInfo
{
1: required string userid;
2: required i32 name;
3: optional list<Address> address;
}
```
**性能与空间对比**
- XML序列化(Xstream)无论在性能和简洁性上比较差。
- Thrift与Protobuf相比在时空开销方面都有一定的劣势。
- Protobuf和Avro在两方面表现都非常优越。
**选型建议**
以上描述的五种序列化和反序列化协议都各自具有相应的特点,适用于不同的场景:
1、对于公司间的系统调用,如果性能可以在100ms以上的服务,基于XML的SOAP协议是一个值得考虑的方案。
2、基于Web browser的Ajax,以及Mobile app与服务端之间的通讯,JSON协议是首选。对于性能要求不太高,或者以动态类型语言为主,或者传输数据载荷很小的的运用场景,JSON也是非常不错的选择。
3、对于调试环境比较恶劣的场景,采用JSON或XML能够极大的提高调试效率,降低系统开发成本。
4、当对性能和简洁性有极高要求的场景,Protobuf,Thrift,Avro之间具有一定的竞争关系。
5、对于T级别的数据的持久化应用场景,Protobuf和Avro是首要选择。如果持久化后的数据存储在Hadoop子项目里,Avro会是更好的选择。
6、由于Avro的设计理念偏向于动态类型语言,对于动态语言为主的应用场景,Avro是更好的选择。
7、对于持久层非Hadoop项目,以静态类型语言为主的应用场景,Protobuf会更符合静态类型语言工程师的开发习惯。
8、如果需要提供一个完整的RPC解决方案,Thrift是一个好的选择。
9、如果序列化之后需要支持不同的传输层协议,或者需要跨防火墙访问的高性能场景,Protobuf可以优先考虑。