JDK序列化
在分布式架构中,序列化是分布式的基础构成之一,我们需要把单台设备上的数据通过序列化(编码、压缩)后通过网络传输给网络中的其它设备,从而实现信息交换。 JDK对Java中的对象序列化提供了支持,原生的Java序列化要求序列化的类必须实现java.io.Serializable
接口,该接口是一个标记接口(不包含任何方法)。 下面定义一个POJO类(仅用于演示,没有任何实际意义),它将被序列化和反序列化
public class Data implements Serializable {
private Integer a;
private Long b;
private Float c;
private Double d;
private Boolean e;
private Character f;
private Byte g;
private Short h;
private int a0;
private long b0;
private float c0;
private double d0;
private boolean e0;
private char f0;
private byte g0;
private short h0;
private String i;
private Date j;
// getter / setter ...
}
使用Java序列化代码非常简单,我们需要构造一个ObjectOutputStream
,该类接收一个输出流(用于输出序列化后的对象信息),这里为了方便演示,我用了ByteArrayOutputStream
,将对象序列为一个字节数组
// 执行序列化
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream output = new ObjectOutputStream(baos);
output.writeObject(data);
baos.close();
output.close();
byte[] buf = baos.toByteArray();
assertEquals(947, buf.length);
代码里省略了构造测试对象的代码(属性有点多),演示了序列化的过程,除了构造输出流和关闭注流代码,实际序列化代码只有一句:output.writeObject(data);
,所以Java的序列化代码实现还是比较简单的。 测试代码中包含一个关于序列化后数据大小的测试,有947个字节,后面其它的序列化会与之形成对比。 当网络一端接收到这个字节数组(数据流)后,会执行反序列化,得到序列化前的数据,下面实现反序列化
// 执行反序列化
ByteArrayInputStream bais = new ByteArrayInputStream(buf);
ObjectInputStream input = new ObjectInputStream(bais);
Data data2 = (Data) input.readObject();
bais.close();
input.close();
assertFalse(data == data2);
assertEquals(data.getA(), data2.getA());
assertEquals(data.getI(), data2.getI());
assertEquals(data.getJ(), data2.getJ());
代码里实现了将字节数组反序列化为一个Data对象,测试语句证明了反序列化对象与原对象不是一个对象(之前讲对象克隆时提到过可以使用序列化、反序列化来实现,这里证明了这一点),但其属性都是一致的,也就是说我们正确得到了序列化前的数据。
使用Serializable
实现序列化时,如果某一个或某几个字段不需要序列化,可以使用transient
关键字修改字段即可
private transient String password;
JDK还提供另一种序列化方式,通过Externalizable
接口来实现
public class Data3 implements Externalizable {
private Integer id;
private String name;
private Date birthday;
@Override
public void writeExternal(ObjectOutput output) throws IOException {
output.writeInt(this.id);
output.writeUTF(this.name);
output.writeObject(this.birthday);
}
@Override
public void readExternal(ObjectInput input) throws IOException, ClassNotFoundException {
this.id = input.readInt();
this.name = input.readUTF();
this.birthday = (Date) input.readObject();
}
// getter / setter ...
}
这里不解释,其与Hadoop提供的序列化机制几乎相同,所以请参考Hadoop的序列化。
Hadoop序列化
在Hadoop中由于经常需要向DataNode复制数据,Hadoop设计了一套特殊的序列化代码(实际仍是完全由JDK实现,其实现方式与Externalizable机制基本类似)。
public class Data2 {
private Integer a;
private Long b;
private Float c;
private Double d;
private Boolean e;
private Character f;
private Byte g;
private Short h;
private int a0;
private long b0;
private float c0;
private double d0;
private boolean e0;
private char f0;
private byte g0;
private short h0;
private String i;
private Date j;
public byte[] serialize() throws IOException {
return Data2.serialize(this);
}
/**
* 序列化当前对象
*
* @return
*/
public static final byte[] serialize(Data2 data) throws IOException {
assert data != null;
ByteArrayOutputStream baos = new ByteArrayOutputStream();
DataOutput output = new DataOutputStream(baos);
// 序列化的数据参考 JdkSerializeTest 中的Data对象
// 序列化、反序列化的过程都是一个字段一个字段的实现,虽然繁琐,但序列化后的大小和性能都比JDK原生序列化API强很多
output.writeInt(data.getA());
output.writeInt(data.getA0());
output.writeLong(data.getB());
output.writeLong(data.getB0());
output.writeFloat(data.getC());
output.writeFloat(data.getC0());
output.writeDouble(data.getD());
output.writeDouble(data.getD0());
output.writeBoolean(data.getE());
output.writeBoolean(data.isE0());
output.writeChar(data.getF());
output.writeChar(data.getF0());
output.writeByte(data.getG());
output.writeByte(data.getG0());
output.writeShort(data.getH());
output.writeShort(data.getH0());
writeString(output, data.getI());
// 序列化日期时使用时间戳表示
output.writeLong(data.getJ().getTime());
return baos.toByteArray();
}
/**
* 反序列化 Data2 对象
*
* @param buf
* @return
*/
public static final Data2 deserialize(byte[] buf) throws IOException {
// 执行反序列化,注意读取的顺序与写入的顺序要一致
ByteArrayInputStream bais = new ByteArrayInputStream(buf);
DataInput input = new DataInputStream(bais);
Data2 data = new Data2();
data.setA(input.readInt());
data.setA0(input.readInt());
data.setB(input.readLong());
data.setB0(input.readLong());
data.setC(input.readFloat());
data.setC0(input.readFloat());
data.setD(input.readDouble());
data.setD0(input.readDouble());
data.setE(input.readBoolean());
data.setE0(input.readBoolean());
data.setF(input.readChar());
data.setF0(input.readChar());
data.setG(input.readByte());
data.setG0(input.readByte());
data.setH(input.readShort());
data.setH0(input.readShort());
data.setI(readString(input));
data.setJ(new Date(input.readLong()));
return data;
}
/**
* 向 DataOutput 写入字符类型稍微复杂一些
*
* @param out
* @param s
* @throws IOException
* @see org.apache.hadoop.io.WritableUtils#writeString(DataOutput, String)
*/
private static final void writeString(DataOutput out, String s) throws IOException {
if (s != null) {
byte[] buffer = s.getBytes("UTF-8");
int len = buffer.length;
// 先写入字符串长度
out.writeInt(len);
// 再写入字符串内容(字节数组)
out.write(buffer, 0, len);
} else {
out.writeInt(-1);
}
}
/**
* 与 writeString(DataOutput, String) 方法相反,用于读取字符串类型数据
*
* @param in
* @return
* @throws IOException
* @see #writeString(DataOutput, String)
*/
private static final String readString(DataInput in) throws IOException {
int length = in.readInt();
if (length == -1) return null;
byte[] buffer = new byte[length];
in.readFully(buffer); // could/should use readFully(buffer,0,length)?
return new String(buffer, "UTF-8");
}
// getter / setter ...
}
代码里实现了序列化和反序列化逻辑,Data2是一个POJO类,与上例中的Data类属性完全一样,只是多了序列化和反序列化方法(这两个方法写在POJO类中的原因是其序列化、反序列化有顺序要求,放在外面会难以控制)。 从实现代码中发现实际序列化、反序列化是由DataOutput
、DataInput
两个接口及其实现类来实现的,这些类完全由JDK提供,并不依赖任何第三方的库,由于手动控制了序列化、反序列化,所以其性能和序列化后的大小控制都非常好
// 序列化的数据参考 JdkSerializeTest 中的Data对象
// 序列化、反序列化的过程都是一个字段一个字段的实现,虽然繁琐,但序列化后的大小和性能都比JDK原生序列化API强很多
byte[] buf = data.serialize();
// 测试序列化大小:JDK序列化后是947,这里只有204
assertEquals(204, buf.length);
// 执行反序列化,注意读取的顺序与写入的顺序要一致
Data2 data2 = Data2.deserialize(buf);
assertFalse(data == data2);
assertEquals(data.getA(), data2.getA());
assertEquals(data.getA0(), data2.getA0());
// 由于浮点数在计算时会有误差,这里第三个参数用于控制误差
assertEquals(data.getC(), data2.getC(), 0.0);
assertEquals(data.getC0(), data2.getC0(), 0.0);
assertEquals(data.getE(), data2.getE());
assertEquals(data.isE0(), data2.isE0());
assertEquals(data.getF(), data2.getF());
assertEquals(data.getF0(), data2.getF0());
assertEquals(data.getG(), data2.getG());
assertEquals(data.getG0(), data2.getG0());
assertEquals(data.getH(), data2.getH());
assertEquals(data.getH0(), data2.getH0());
assertEquals(data.getI(), data2.getI());
assertEquals(data.getJ(), data2.getJ());
可以看出同样对象序列化后只有204个字节,约为之前的1/4,而且序列化的性能也调出很多,后面会给出简单对比。
Hessian序列化
在一些开源框架中(如:Dubbo),也使用Hessian库(这里指的是Hessian2)来实现序列化。
// 执行序列化
ByteArrayOutputStream baos = new ByteArrayOutputStream();
Hessian2Output hessian2Output = new Hessian2Output(baos);
hessian2Output.writeObject(data);
hessian2Output.close();
// 获取字节数组前,必须先关闭Hessian2Output,否则取得字节数组长度为0(原因暂不清楚)
byte[] buf = baos.toByteArray();
baos.close();
// 测试断言
Assert.assertNotNull(buf);
Assert.assertEquals(373, buf.length);
System.out.println(new String(buf));
// 执行反序列化
Hessian2Input hessian2Input = new Hessian2Input(new ByteArrayInputStream(buf));
Data data2 = (Data) hessian2Input.readObject();
hessian2Input.close();
// 测试断言
assertFalse(data == data2);
assertEquals(data.getA(), data2.getA());
assertEquals(data.getI(), data2.getI());
assertEquals(data.getJ(), data2.getJ());
相对JDK序列化和Hadoop序列化,其序列化后的数据大小居中,实际上性能也是居中的。但该库的优势在于,其跨语言的特性,也就是说可以向非Java语言的程序发送序列化数据,并能由对应语言的Hessian库实现反序列化。
性能比较
下面使用10,000次循环序列化、反序列化(单线程)来测试三种序列化方式的耗时(该测试仅供参考,场景有限,并不能真的说明三种方式优劣程度)。
- jdk
@Test
public void performance() throws IOException, ClassNotFoundException {
final int loop = 10_000;
long time = System.currentTimeMillis();
for (int i = 0; i < loop; i++) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream output = new ObjectOutputStream(baos);
output.writeObject(data);
baos.close();
output.close();
byte[] buf = baos.toByteArray();
// 执行反序列化
ByteArrayInputStream bais = new ByteArrayInputStream(buf);
ObjectInputStream input = new ObjectInputStream(bais);
input.readObject();
bais.close();
input.close();
}
// loop = 10,000 -> 程序执行耗时:1037 毫秒!
System.out.println(String.format("程序执行耗时:%d 毫秒!", System.currentTimeMillis() - time));
}
- hadoop
@Test
public void performance() throws IOException {
final int loop = 10_000;
long time = System.currentTimeMillis();
for (int i = 0; i < loop; i++) {
// 执行序列化
byte[] buf = data.serialize();
// 执行反序列化
Data2.deserialize(buf);
}
// loop = 10,000 -> 程序执行耗时:75 毫秒!
System.out.println(String.format("程序执行耗时:%d 毫秒!", System.currentTimeMillis() - time));
}
- hessian
public void performance() throws IOException {
final int loop = 10_000;
long time = System.currentTimeMillis();
for (int i = 0; i < loop; i++) {
// 执行序列化
ByteArrayOutputStream baos = new ByteArrayOutputStream();
Hessian2Output hessian2Output = new Hessian2Output(baos);
hessian2Output.writeObject(data);
hessian2Output.close();
byte[] buf = baos.toByteArray();
// 执行反序列化
Hessian2Input hessian2Input = new Hessian2Input(new ByteArrayInputStream(buf));
hessian2Input.readObject();
hessian2Input.close();
}
// loop = 10,000 -> 程序执行耗时:300 毫秒!
System.out.println(String.format("程序执行耗时:%d 毫秒!", System.currentTimeMillis() - time));
}
结论(非权威,有兴趣的自行研究吧) | 循环次数 | jdk (947bytes) | hadoop (204bytes) | hessian (373bytes) | | - | - | - | - | | 10,000 | 1,037ms | 75ms | 300ms |
其它序列化
实际应用中,序列化可选方案很多,像Hadoop还可以用Avro、Protobuf来进行序列化,下面列出一些常用的序列化库:
- JSON,是一种规范,对应的库非常多,比如:Jackson、Fastjson等
- Avro,Hadoop提供的一套跨平台序列化方案
- Protobuf,Google提供的一套跨平台序列化方案
- Thrift,Apache提供的一套跨平台序列化方案
- Kryo
- FST
- Dubbo 后面三个都只能用于Java,其中Dubbo是Dubbo框架提供的序列化方案(经查阅源码,2.6.x及以后的版本中不再提供)
结语
序列化在分布式架构中(比较偏底层)是很重要的一环,好的序列化方案可以节省大量的带宽,并且提升程序处理速度。
后面列出的一些序列化方案本文未详细解释,这里先留个坑,后面将专门撰文来讲解。
源码仓库: