一.概念:
Java序列化是把对象状态保存到一个字节流中的过程,反序列化则是把由序列化生成的这一个字节流重新转换成对象的过程。
二.作用:
目前,一个典型的企业化应用程序一般会由多个组件构成,并且被分布在网络各端的不同系统之上。在java中,一切都是对象,多个组件之间要进行交互,就需要一种数据交换的机制。一个可行的办法是你自己定义一套协议然后进行对象数据传输,这意味着接受端要想重新创建这个对象,必须知道发送端的协议,使用这样的办法就使得这个系统对第三方组件的交互变得艰难。因此,一个有效可行的对象协议用来在组件之间传输数据就变得必要了。序列化就是为了解决这个问题而产生的,java组件之间采用这个协议来进行数据传输。
图1展示了一个C/S模式下的客户端和服务器端通过java序列化来进行对象传输的过程:
---------------------------------------------------图1--------------------------------------------------------
三.使用默认的序列化形式:
要使用java默认的序列化形式序列化一个对象,你必须保证你要序列化的对象实现了java.io.Serializable接口,如:
import java.io.Serializable;
class TestSerial implements Serializable {
public byte version = 100;
public byte count = 0;
}
在以上类中,和创建一个普通类唯一的不同之处就是你必须得实现Serializable 接口,Serializable 接口是个标记接口,它当中没有方法,只是用来在序列化时告诉序列化机制,这个类可以被序列化。
现在你已经把这个类变得可序列化,下一步就是真正得把这个对象序列化,这是由j ava.io.ObjectOutputStream 类的writeObject()方法完成的。如:
public static void main(String args[]) throws IOException {
FileOutputStream fos = new FileOutputStream("temp.out");
ObjectOutputStream oos = new ObjectOutputStream(fos);
TestSerial ts = new TestSerial();
oos.writeObject(ts);
oos.flush();
oos.close();
}
上述代码把TestSerial对象的状态存储到temp.out文件中,oos.writeObject(ts);实际上调用了序列化的算法,最终把文件写入到temp.out中。
要想从持久化文件中重新创建TestSerial对象,只需要调用下面的代码:
public static void main(String args[]) throws IOException {
FileInputStream fis = new FileInputStream("temp.out");
ObjectInputStream oin = new ObjectInputStream(fis);
TestSerial ts = (TestSerial) oin.readObject();
System.out.println("version="+ts.version);
}
对象的读取创建发生在oin.readObject();方法调用时,它读取先前持久化在文件中的二进制字节流并创建一个和持久化之前状态完全相同的复刻版本对象。因为readObject可以读取任何可序列化的对象,要转成指定的TestSerial类,需要经过类型转换。
字节流细节:
那么序列化之后的对象文件里是什么样的内容呢?前面例子中把TestSerial对象序列化到temp.out文件当中,以下,就是16进制文件的具体内容:
回头再看TestSerial类,其中只有两个byte类型的成员变量,大小分别为1个字节,所以,TestSerial对象的实际内容应该是2个字节,但是temp.out文件中却足足有74个字节。这就奇怪了,其它的字节从何而来?这些字节具体又代表着什么意思?原来,是它们是由序列化算法引入,用以来重新创建类对象的。内容的细节将会在接下来做具体的分析。
大体来讲,java序列化机制做了以下这几件事情:
1.将对象实例相关的类元数据输出
2.递归地输出类的超类描述
3.类描述完了以后,开始从最顶层的超类开始 输出对象实例的实际数据值
4.从上自下递归输出实例数据
以下依次来解释temp.out文件中的内容:
AC ED:STREAM_MAGIC,声明使用了序列化协议。
00 05 :使用两个字节标识了序列化版本。
0x73:TC_OBJECT。标识一个对象的开始。
0x72:TC_CLASSDESC。标识一个类描述的开始。
00 22:使用两个字节标识类名的长度,这里长度为34。
63 6E 2E 63 6F 6D 2E 69 63 65 67 61 72 64 65 6E 2E 73 65 72 69 61 6C 2E 54 65 73 74 53 65 72 69 61 6C:类名长度之后的34个字节表示类名本身。cn.com.icegarden.serial.TestSerial
B5 58 B9 39 E6 7D 59 3A:该八位为序列化id,如果没有指定,则会由算法随机生成一个 8byte 的 ID。
0x02:标识该类支持序列化。
00 02:表示该类的属性字段数量。
0x 42:字段类型标识,B,表示byte。
00 05:字段名长度,即count字段名长度,为5.
63 6F 75 6E 74:即count字段名。
0x 42:字段类型标识,B,表示byte。
00 07:字段名长度,即version字段名长度,为7
76 65 72 73 69 6F 6E:即version字段名。
0x78: TC_ENDBLOCKDATA, 类描述结束的标志 。
0x70: TC_NULL, 说明没有其他超类的标志。
0x00:即count的值。
0x64:100,即version的值。
以上看了一个简单类结构的序列化字节码,接下来看一个稍微复杂的类结构序列化后的字节码。
class parent implements Serializable {
int parentVersion = 10;
}
class contain implements Serializable{
int containVersion = 11;
}
public class SerialTest extends parent implements Serializable {
int version = 66;
contain con = new contain();
public int getVersion() {
return version;
}
public static void main(String args[]) throws IOException {
FileOutputStream fos = new FileOutputStream("temp.out");
ObjectOutputStream oos = new ObjectOutputStream(fos);
SerialTest st = new SerialTest();
oos.writeObject(st);
oos.flush();
oos.close();
}
}
这个例子中,SerialTest 类继承了 Parent 超类,内部还持有一个 Container 对象,序列化后的字节码文件内容如下:
00000000h: AC ED 00 05 73 72 00 22 63 6E 2E 63 6F 6D 2E 69 ;
00000010h: 63 65 67 61 72 64 65 6E 2E 73 65 72 69 61 6C 2E ;
00000020h: 53 65 72 69 61 6C 54 65 73 74 48 21 86 FD 33 1C ;
00000030h: 46 5E 02 00 02 49 00 07 76 65 72 73 69 6F 6E 4C ;
00000040h: 00 03 63 6F 6E 74 00 21 4C 63 6E 2F 63 6F 6D 2F ;
00000050h: 69 63 65 67 61 72 64 65 6E 2F 73 65 72 69 61 6C ;
00000060h: 2F 63 6F 6E 74 61 69 6E 3B 78 72 00 1E 63 6E 2E ;
00000070h: 63 6F 6D 2E 69 63 65 67 61 72 64 65 6E 2E 73 65 ;
00000080h: 72 69 61 6C 2E 70 61 72 65 6E 74 C7 B3 8A 1F C8 ;
00000090h: 86 9E C1 02 00 01 49 00 0D 70 61 72 65 6E 74 56 ;
000000a0h: 65 72 73 69 6F 6E 78 70 00 00 00 0A 00 00 00 42 ;
000000b0h: 73 72 00 1F 63 6E 2E 63 6F 6D 2E 69 63 65 67 61 ;
000000c0h: 72 64 65 6E 2E 73 65 72 69 61 6C 2E 63 6F 6E 74 ;
000000d0h: 61 69 6E F6 F8 03 4B A4 A4 25 ED 02 00 01 49 00 ;
000000e0h: 0E 63 6F 6E 74 61 69 6E 56 65 72 73 69 6F 6E 78 ;
000000f0h: 70 00 00 00 0B
依次再来看字节码文件内容的含义:
AC ED:STREAM_MAGIC,声明使用了序列化协议。
00 05 :使用两个字节标识了序列化版本。
0x73:TC_OBJECT。表示一个对象的开始。
0x72:TC_CLASSDESC。标识一个类描述的开始。
0022:用两个byte表示类名的长度,这里是34位。
63 6E 2E 63 6F 6D 2E 69 63 65 67 61 72 64 65 6E 2E 73 65 72 69 61 6C 2E 53 65 72 69 61 6C 54 65 73 74:cn.com.icegarden.serial.SerialTest
48 21 86 FD 33 1C 46 5E:八个byte表示的序列化versionId,如果没有指定,则会由算法随机生成一个 8byte 的 ID。
0x02:表示该类支持序列化。
00 02:用两个字节表示域的个数,这里是2个字段。
0x 49:即I,用来表示int。
00 07: 域名字长度 .
63 6F 6E: 域名字描述, con
描述对象类型引用时需要使用 JVM 的标准对象签名表示法
0x4C: 即L,域的类型
00 03: 域名字长度 .
76 65 72 73 69 6F 6E: 域名字描述, version
0x74: TC_STRING. 代表一个 new String. 用 String 来引用对象。
00 21:该String的长度,33位。
4C 63 6E 2F 63 6F 6D 2F 69 63 65 67 61 72 64 65 6E 2F 73 65 72 69 61 6C 2F 63 6F 6E 74 61 69 6E 3B这里33位即为Lcn/com/icegarden/serial/contain;(JVM 的标准对象签名表示法 )。
0x78: TC_ENDBLOCKDATA, 对象数据块结束的标志 (一般和0x72遥相呼应)。
至此,SerialTest类的描述已经完成,接下来要对付类parent进行描述:
0x72: TC_CLASSDESC. 声明这个是个新类 .
00 1E: 类名长度 .
63 6E 2E ;
63 6F 6D 2E 69 63 65 67 61 72 64 65 6E 2E 73 65 72 69 61 6C 2E 70 61 72 65 6E 74: cn.com.icegarden.serial.parent, 类名描述。
C7 B3 8A 1F C8 86 9E C1: SerialVersionUID , 序列化 ID. 如果没有指定,则会由算法随机生成一个 8byte 的 ID。
0x02: 标记号 . 该值声明该对象支持序列化 .
00 01: 类中域的个数.
0x49:即I,表示int
00 0D:字段名长度,13位。
70 61 72 65 6E 74 56 65 72 73 69 6F 6E:parentVersion ,域名字描述。
0x78:TC_ENDBLOCKDATA, 对象数据块结束的标志 (一般和0x72遥相呼应)。
0x70:TC_NULL, 说明没有其他超类的标志。
到此为止,算法已经对所有的类的描述都做了输出。接下来会从父类开始,依次输出字段的长度。
00 00 00 0A: 10, parentVersion 域的值 .
00 00 00 42 66,version 域的值。
接下来是con域,因为它是一个对象引用,所以比较特殊,包括要对它对应的类进行声明:
0x73:TC_OBJECT。表示一个对象的开始。
0x72:TC_CLASSDESC。标识一个类描述的开始。
001F:表示类名长度31。
63 6E 2E 63 6F 6D 2E 69 63 65 67 61 72 64 65 6E 2E 73 65 72 69 61 6C 2E 63 6F 6E 74 61 69 6E:cn.com.icegarden.serial.contain。
F6 F8 03 4B A4 A4 25 ED:SerialVersionUID , 序列化 ID. 如果没有指定,则会由算法随机生成一个 8byte 的 ID。
0x02:表示该类支持序列化。
00 01:表示类中域的个数。
0x49:即I,表示int。
00 0E:表示字段的长度,14。
63 6F 6E 74 61 69 6E 56 65 72 73 69 6F 6E:containVersion,字段名。
0x78: TC_ENDBLOCKDATA, 对象数据块结束的标志 (一般和0x72遥相呼应)。
0x70:TC_NULL, 说明没有其他超类的标志。
至此,contain类的描述结束。
00 00 00 0B:这里四位输出containVersion字段的值,11。
看过以上复杂对象的序列化文件内容后,总结下该文件内容结构如下:
1.开头是固定的STREAM_MAGIC和版本号,我们看到的版本号都是00 05。
2.接下来进行对象描述,都是以0x73开头,其后跟着类似个类描述(从当前类开始,循环描述上层类),类描述都包含在0x72和0x78中,类描述到Object类以下之后,用0x70表示没有超类,即类描述结束。
3.接下来是从顶层类开始输出类中字段的值,如果字段是对象引用,递归描述对象,即重新以0x73开头,做对象的描述,其中同样包含类描述和字段值,直到字段值都为基本类型。
4.结束没有标识(使用默认的序列化形式)。
以下是java.io.ObjectStreamConstants 定义的一些在序列化字节流中可能用到的常量:
final static short STREAM_MAGIC = (short)0xaced;
final static short STREAM_VERSION = 5;
final static byte TC_NULL = (byte)0x70;
final static byte TC_REFERENCE = (byte)0x71;
final static byte TC_CLASSDESC = (byte)0x72;
final static byte TC_OBJECT = (byte)0x73;
final static byte TC_STRING = (byte)0x74;
final static byte TC_ARRAY = (byte)0x75;
final static byte TC_CLASS = (byte)0x76;
final static byte TC_BLOCKDATA = (byte)0x77;
final static byte TC_ENDBLOCKDATA = (byte)0x78;
final static byte TC_RESET = (byte)0x79;
final static byte TC_BLOCKDATALONG = (byte)0x7A;
final static byte TC_EXCEPTION = (byte)0x7B;
final static byte TC_LONGSTRING = (byte) 0x7C;
final static byte TC_PROXYCLASSDESC = (byte) 0x7D;
final static int baseWireHandle = 0x7E0000;
四.使用默认序列化形式的代价
首先是两个tip
1. The object to be persisted must mark all nonserializable fields transient。在可序列化的对象中,对于不想要序列化的字段,要用transient标识标注。此外,静态数据也不会被序列化。
2. 关于序列版本UID,如果没有指定,序列化算法会将一个复杂的过程作用在这个类上,从而在运行时产生这个标识,所以,如果不指定该UID的情况,对类中元素任何一个小的改动都会造成标识的不一致而使得反序列化失败。
3. 使用java默认序列化形式而付出的一个代价是,如果你接受了默认的序列化协议,这个类中私有的和包级私有的实例域将都变成导出的API的一部分。
如果没有给序列化的类提供final long型名为serialVersionUID的序列版本UID,一个对象在序列化之后,如果对该类进行了重构,比如说增加一个字段,反序列化时就会出现错误,产生InvalidClassException。解决这个问题的办法是给这个类指定一个serialVersionUID字段值,只要重构之后类的这个字段值没有改变,那么在反序列化时就依然可以用之前生成的字节流生成现在的类对象,新增加的字段将被设置为默认值。
4因为反序列化机制中没有显式的构造器,所以你很容易忘记要确保:反序列化过程必须也要保证所有"由构造器建立起来的约束关系"。比如Thread,散列表这些跟运行时环境相关的元素。
5随着类发行新的版本,相关的测试负担也增加了。当一个可序列化的类被修订的时候,很重要的一点是,要检查是否可以"在新版本中序列化一个实例,然后在旧版本中反序列化",反之亦然。
6.一个结论:决定实现Serializable,会增加出错和出现安全问题的可能性,因为它导致实例要利用语言之外的机制来创建(通过序列化机制来还原实例,而不是通过new关键字),而不是用普通的构造器。
五.考虑使用自定义的序列化协议
定制自定义的序列化形式(协议)的方法是在要序列化的类中实现writeObject和readObject。
1.出于安全性的考虑。序列化的二进制格式完全编写在文档中,并且完全可逆,这对于安全性有着不良的影响。我们可以为序列化和反序列化时分别实现writeObject和readObject方法来定制序列化协议。例如一个可序列化类中有一个不希望直接体现在二进制文件中的字段值,如age,可以在序列化前后对这个字段做一些操作。
private void writeObject(java.io.ObjectOutputStream stream)
throws java.io.IOException
{
age = age << 2;
stream.defaultWriteObject();
}
在反序列化时,读取了对象的相关信息之后,再把age字段移位回来:
private void readObject(java.io.ObjectInputStream stream)
throws java.io.IOException, ClassNotFoundException
{
stream.defaultReadObject();
// "Decrypt"/de-obscure the sensitive data
age = age << 2;
}
当然,可以设计更复杂的算法。
2. 假如发现比默认的序列化形式能够更合理地描述对象的逻辑状态的序列化形式,出于性能,对象约束关系等的考虑,可以考虑使用自定义的序列化形式。看一个例子(来自effective java):
// Awful candidate for default serialized form
public final class StringList implements Serializable {
private int size = 0;
private Entry head = null;
private static class Entry implements Serializable {
String data;
Entry next;
Entry previous;
}
... // Remainder omitted
}
从逻辑意义上讲,StringList类表示了一个String的列表。但是从物理意义上讲,它把该序列表示成一个双向链表。如果你接受了默认的序列化形式,该序列化形式将不遗余力地镜像出链表中的所有项,以及这些项之间的所有双向链接。根据本文第三条中分析过的默认序列化形式的字节流细节,可以想象,对这个类使用默认的序列化形式将会在时间空间上占据多大的消耗,栈溢出也是一个潜在的威胁,因为默认的序列化过程要对对象图执行一次递归遍历,即使对于中等规模的对象图,这样的操作也可能会引起栈溢出。
显而易见得,如果仅从逻辑意义出发,对比默认序列化形式,类似数学题中解题时使用简便方法,可以考虑从逻辑意
义上出发定制一个合适的序列化形式。
对于StringList类,合理的序列化只需要把字符串的数量字段,然后紧跟着序列化出那些字段串字段,这就在逻辑上完整表示了StringList类的逻辑数据。通过自定义writeObject和readObject来完成了这个自定义序列化形式的实现:
// StringList with a reasonable custom serialized form
public final class StringList implements Serializable {
private transient int size = 0;
private transient Entry head = null;
// No longer Serializable!
private static class Entry {
String data;
Entry next;
Entry previous;
}
// Appends the specified string to the list
public final void add(String s) { ... }
/**
* Serialize this {@code StringList} instance.
*
* @serialData The size of the list (the number of strings
* it contains) is emitted ({@code int}), followed by all of
* its elements (each a {@code String}), in the proper
* sequence.
*/
private void writeObject(ObjectOutputStream s)
throws IOException {
s.defaultWriteObject();
s.writeInt(size);
// Write out all elements in the proper order.
for (Entry e = head; e != null; e = e.next)
s.writeObject(e.data);
}
private void readObject(ObjectInputStream s)
throws IOException, ClassNotFoundException {
s.defaultReadObject();
int numElements = s.readInt();
// Read in all elements and insert them in list
for (int i = 0; i < numElements; i++)
add((String) s.readObject());
}
... // Remainder omitted
}
尽管StringList的所有域都是transient的,但writeObject方法仍然要首先调用defaultWriteObject,readObject要首先调用defaultReadObject。如果所有的实例域都是瞬时的,从技术角度而言,不调用defaultWriteObject和defaultReadObject也是允许的,但是不推荐这样做。这样得到的序列化形式允许在以后的发行版本中增加非瞬时的实例域,并且还能保持向前或者向后兼容性。如果某一个实例将在未来的版本中被序列化,然后在前一个版本中被反序列化,那么,后增加的域将被忽略掉。如果旧版本的readObject方法没有调用defaultReadObject,反序列化过程将失败,引发StreamCorrupted Exception异常。
即使在writeObject方法中不做任何操作,执行该类序列化之后,也会把完整的类描述体现在字节流中。只是如果不调用defaultReadObject方法,则不会输出字段值。所以可以简单成在writeObject方法中调用defaultReadObject的作用是将当前类的非静态和非transient字段写入此流。
六.使用序列化代理
使用序列化代理。只要为原始类提供一个writeReplace方法,可以序列化不同类型的对象来代替它。类似地,如果反序列化期间发现一个readResolve方法,那么将调用该方法,将替代对象提供给调用者。
准备一个测试类:
package cn.com.icegarden.serial;
import java.io.Serializable;
public class People implements Serializable{
public byte age = 22;
public byte sex = 1;
public int height = 173;
public int weight = 120;
}
序列化后的总字节数为103byte,具体字节:
如果使用代理来进行序列化,需要在People类中实现writeReplace方法
private Object writeReplace()
throws java.io.ObjectStreamException
{
return new Proxy(this);
}
总字节数为95
Proxy类的内容为:
package cn.com.icegarden.serial;
import java.io.Serializable;
public class Proxy implements Serializable{
String data;
public Proxy(People people){
data = people.age+","+people.height+","+people.sex+","+people.weight+","+people.party;
}
private Object readResolve()
throws java.io.ObjectStreamException
{
String[] pieces = data.split(",");
People people = new People(Byte.valueOf(pieces[0]),Integer.valueOf(pieces[1]),Byte.valueOf(pieces[2]),Integer.valueOf(pieces[3]), Integer.valueOf(pieces[4]));
return people;
}
}
这样,就完成了把People类通过序列化代理模式来序列化和反序列化了。
1. 这个模式的好处是它极大地消除了序列化机制中语言本身之外的特征,因为反序列化实例与任何其他实例的创建方式一样,是通过构造器或静态工厂方法而创建的。这样你就不必单独确保被反序列化的实例一定要遵守类的约束条件。
2. 这么做的另一个好处是可以阻止伪字节流的攻击。