Java 提供的对象输入流
ObjectInputStream
和输出流ObjectOutputStream
,可以直接把 Java 对象作为可存储的字节数据写入文件,也可以传输到网络上。对于程序员来说,基于 JDK 默认的序列化机制可以避免操作底层的字节数组,从而提高开发效率。Java 序列化的主要目的是网络传输和对象持久化。
# 一、无法跨语言
无法跨语言,是 Java 序列化最致命的问题。对于跨进程的服务调用,服务提供者可能是 Java 意外的其他语言,当我们需要和异构语言交互时,Java 序列化就难以胜任
由于 Java 序列化技术是 Java 语言内部的私有协议,其他语言并不支持,对于用户来说它完全是个黑盒子。对于 Java 序列化后的字节数组,别的语言无法进行反序列化,这就严重阻碍了它的应用。
事实上,目前几乎所有流行的 Java RCP 通信框架,都没有使用 Java 序列化作为编解码框架,原因就在于它无法跨语言,而这些 RPC 框架往往需要支持跨语言调用。
# 二、序列化后的码流太大
【1】通过一个实例看下 Java 序列化后的字节数组大小。如下 UserInfo 对象是实现了序列化接口的对象,并生成了默认的序列号:serialVersionUID = 1L。说明 UserInfo 对象可以通过 JDK 默认的序列化机制进行序列化和反序列化。并创建了一个与之比较码流大小的方法 codeC ,此方法基于 ByteBuffer 的通用二进制编解码技术对 UserInfo 对象进行编码,编码结果也是 byte 数组。
【序列化 ID 问题】:两个客户端 A 和 B 试图通过网络传递对象数据,A 端将对象 C 序列化为二进制数据再传给 B,B 反序列化得到 C。 问题:C 对象的全类路径假设为 com.yintong.UserInfo,在 A 和 B 端都有这么一个类文件,功能代码完全一致。也都实现了 Serializable 接口,但是反序列化时总是提示不成功。 解决:虚拟机是否允许反序列化,不仅取决于类路径和功能代码是否一致,一个非常重要的一点是两个类的序列化 ID 是否一致(就是 private static final long serialVersionUID = 1L)。清单 1 中,虽然两个类的功能代码完全一致,但是序列化 ID 不同,他们无法相互序列化和反序列化。
//实现了序列化接口的实例
public class UserInfo implements Serializable{
private static final long serialVersionUID = 1L;
//用户ID
private int ID;
//用户名
private String name;
//有参构造器
public UserInfo(int iD, String name) {
super();
ID = iD;
this.name = name;
}
//根据 buffer缓冲区 获取字节数组
public byte[] codeC() {
//定义字节缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
//获取name属性的二进制字节流
byte[] value = this.name.getBytes();
//用于写入 int 值的相对 put 方法(可选操作)。
//将 n 个包含给定 int 值的字节按照当前的字节顺序写入到此缓冲区的当前位置,然后将该位置增加 n。
buffer.putInt(value.length);
//存入 二进制值
buffer.put(value);
//写入一个int值=ID到ByteBuffer中。
buffer.putInt(this.ID);
//切换为读
buffer.flip();
value = null;
//remaining 返回剩余的可用长度,次长度为实际读取的数据长度
byte[] result = new byte[buffer.remaining()];
//获取buffer中的值到 result
buffer.get(result);
return result;
}
public int getID() {
return ID;
}
public void setID(int iD) {
ID = iD;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
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
【2】下面是测试程序,先调用两种编码接口对 UserInfo 进行编码,然后分别打印两者编码后的流大小进行对比。
public class TestUserInfo {
public static void main(String[] args) throws Exception {
UserInfo info = new UserInfo(101, "zheng zhao xiang");
//JDK 序列化流程
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(info);
oos.flush();
oos.close();
byte[] b = bos.toByteArray();
System.out.println("JDB 序列化后流的长度: "+b.length);
bos.close();
System.out.println("通过缓冲区 buffer 处理后的流长度: "+info.codeC().length);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
【3】测试结果如下:采用 JDK 序列化机制编码后的二进制数组大小是通过缓冲区处理后的 4 倍。
JDB 序列化后流的长度: 97
通过缓冲区buffer 处理后的流长度:24
2
# 三、序列化性能太低
【1】下面从序列化的性能角度看下 JDK 的表现如何。将之前的例子进行修改。对 UserInfo 中的 codeC 方法改造如下:
public byte[] codeC(ByteBuffer buffer) {
//清空缓冲区
buffer.clear();
//获取name属性的二进制字节流
byte[] value = this.name.getBytes();
//用于写入 int 值的相对 put 方法(可选操作)。
//将 n 个包含给定 int 值的字节按照当前的字节顺序写入到此缓冲区的当前位置,然后将该位置增加 n。
buffer.putInt(value.length);
//存入 二进制值
buffer.put(value);
//写入一个int值=ID到ByteBuffer中。
buffer.putInt(this.ID);
//切换为读
buffer.flip();
value = null;
//remaining 返回剩余的可用长度,次长度为实际读取的数据长度
byte[] result = new byte[buffer.remaining()];
//获取buffer中的值到 result
buffer.get(result);
return result;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
【2】对 Java 序列化和二进制编码分别进行性能测试,编码 100万次,然后统计消耗的总时间:
public class PerformTestUserInfo {
public static void main(String[] args) throws IOException {
UserInfo info = new UserInfo(101, "zheng zhao xiang");
//循环次数
//JDK 序列化流程
ByteArrayOutputStream bos = null;
int loop = 1000000;
ObjectOutputStream oos = null;
//写之前获取系统时间
long startTimeMillis = System.currentTimeMillis();
for(int i=0; i<loop; i++) {
bos = new ByteArrayOutputStream();
oos = new ObjectOutputStream(bos);
oos.writeObject(info);
oos.flush();
oos.close();
byte[] b = bos.toByteArray();
bos.close();
}
long endTimeMillis = System.currentTimeMillis();
System.out.println("JDK 序列化花费的时间: "+(endTimeMillis - startTimeMillis)+" ms");
ByteBuffer buffer = ByteBuffer.allocate(1024);
startTimeMillis = System.currentTimeMillis();
for(int i=0; i<loop; i++) {
info.codeC(buffer);
}
endTimeMillis = System.currentTimeMillis();
System.out.println("通过缓冲区 buffer花费的时间: "+(endTimeMillis - startTimeMillis)+" ms");
}
}
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
【3】结果展示:结果非常令人惊讶,Java 序列化的性能只有二进制编码的 11% 左右,可见原生序列化的性能很差。
JDB 序列化花费的时间: 1861ms
通过缓冲区buffer花费的时间:210ms
2
# 四、结论
无论是序列化后的码流大小,还是序列化的性能,JDK 默认的序列化机制表现都很差。因此,我们通常不会选择 Java 序列化作为远程跨节点调用的编解码框架。而是使用业界提供的很多优秀的编解码框架,它们在克服了 JDK 默认的序列化框架缺点的基础上,还增加了很多亮点。例如:Google 的 Protobuf、Facebook 的 Thrift 和 JBoss 的 Marshalling 等等,我后期都会学习和整理相关的博文。