Java 序列化的实现原理

(Disclaimer:未经许可请勿转载。如需转载请先与我联系。)


[align=center][b]Java序列化的原理[/b][/align]
前沿
欢迎进入JDK源码阅读之序列化专题!java序列化从JDK1.1版本就开始,是一项比较成熟的技术。对于初学者可能很容易就能学会如何编写序列化类,但是对其详细的原理以及一些细节上的技术了解还是比较少的。本专题将分三个部分从JDK源代码的角度向大家介绍java序列化相关的知识:
Part1:对序列化协议做出比较深入的探讨,最后总结出序列化的基本算法;
Part2:了解自定义序列化的方法及其原理,理解其在安全方面的重要作用;
Part3:了解java序列化的可重构性,有利于软件的可扩宽性。
基本概念
对象的序列化(Object serialization)机制,就是将对象编码成一个字节流(序列化serialization),以及从字节流编码中重新构建对象(反序列化deserialization)的过程。一旦将对象序列化后,一方面可以将其持久化到磁盘上,供以后反序列化使用;另一方面在分布式环境中经常将对象从这一端网络传递到另一端,需要一种在两端传输数据的协议,而java序列化机制就提供了这种协议的实现。
如何序列化一个对象
一个对象如果想实现序列化只需实现java.io.Serializable接口,该接口没有方法,只是一种标记。JDK中源代码如下:
java.io.ObjectOutputStream.writeObject()
└java.io.ObjectOutputStream.writeObject0();
---------------------------------------------------------------------
1085 private void writeObject0(Object obj, boolean unshared)
1086 throws IOException
-中略-
//判断该类是否实现了java.io.Serializable接口
1157 } else if (obj instanceof Serializable) {
1158 writeOrdinaryObject(obj, desc, unshared);
1159 } else {
1160 if (extendedDebugInfo) {
1161 throw new NotSerializableException(
1162 cl.getName() + "\n" + debugInfoStack.toString());
1163 } else {
1164 throw new NotSerializableException(cl.getName());
1165 }
1166 }
-中略-
---------------------------------------------------------------------
由程序的1157行可知如果序列化了未实现Serializable接口的对象,将会抛出NotSerializableException(1164行)异常,说明该类不具有序列化。现在我们根据具体的实例来说明序列化的基本原理。

假设我们创建一个实现Serializable接口的Fruit水果类,其中包含了一个int类型weight重量属性;同时创建了继承自该类的Apple苹果子类,其中包含有一个String类型的name名称属性。
Fruit.java 如下:
---------------------------------------------------------------------
package com.fnst.infoQ;

public class Fruit implements Serializable{
private int weight;
public Fruit() {this.weight = 10;}
}
---------------------------------------------------------------------
Apple.java 如下:
---------------------------------------------------------------------
package com.fnst.infoQ;

public class Apple extends Fruit{
private String name;
public Apple(String _name){this.name = _name;}
public Apple() {this.name = "Default Apple";}

public static void main(String args[]) throws IOException {
FileOutputStream fos = new FileOutputStream("a.txt");
ObjectOutputStream oos = new ObjectOutputStream(fos);
Apple apple = new Apple();
oos.writeObject(apple);
oos.flush();
oos.close();
}
}
---------------------------------------------------------------------
执行程序后,Apple对象序列化到a.txt文件中,其中内容如下:
---------------------------------------------------------------------
[color=red]AC ED 00 05[/color] [color=cyan]73 72 00 14 63 6F 6D 2E 66 6E 73 74
2E 69 6E 66 6F 51 2E 41 70 70 6C 65 55 D6 C7 F2
12 79 30 B5 02 00 01 4C 00 04 6E 61 6D 65 74 00
12 4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69
6E 67 3B 78 72 00 14 63 6F 6D 2E 66 6E 73 74 2E
69 6E 66 6F 51 2E 46 72 75 69 74 64 79 B9 B9 16
D6 47 FB 02 00 01 49 00 06 77 65 69 67 68 74 78
70[/color] [color=violet]00 00 00 0A 74 00 0D 44 65 66 61 75 6C 74 20
41 70 70 6C 65[/color]
---------------------------------------------------------------------
Java序列化序列化对象的信息包括:类元数据描述、类的属性、父类信息以及属性域的值。Java将这些信息分成3部分:序列化头信息、类的描述部分以及属性域的值部分。现在对a.txt文件加以分析,其中包含一些序列化机制中提供的特殊字段,这些字段被定义在java.io.ObjectStreamConstants接口中。

第一部分 序列化头信息(见颜色)
[1]. 0xAC 0xED :STREAM_MAGIC 流的幻数,用于标记序列化协议
[2]. 0x00 0x05 :STREAM_VERSION 标记序列化协议的版本
以上两行信息在ObjectOutputStream类对象构造时就被写入到了序列化缓冲区中,代码如下;
java.io.ObjectOutputStream() 【构造函数】
└java.io.ObjectOutputStream.writeStreamHeader
---------------------------------------------------------------------
615 protected void writeStreamHeader() throws IOException {
616 bout.writeShort(STREAM_MAGIC); //将流的幻数写入流缓冲区,步骤1
617 bout.writeShort(STREAM_VERSION);//将序列化协议写入流缓冲区,步骤2
618 }
---------------------------------------------------------------------
ObjectOutputStream的构造函数中会调用writeStreamHeader方法,将序列化的头信息写入到流缓冲区中。
第二部分 类的描述部分(见颜色)
//首先是父类描述部分
[3]. 0x73 : TC_OBJECT. 声明这是一个新的对象.
[4]. 0x72 : TC_CLASSDESC 声明这是一个新的类描述
[5]. 0x00 0x14 :类名字的长度,换算成十进制就是20
[6]. 0x63 0x6F 0x6D 0x2E 0x66 0x6E 0x73 0x74 0x2E 0x69 0x6E 0x66 0x6F 0x51
0x2E 0x41 0x70 0x70 0x6C 0x65 :代表类的名称com.fnst.InfoQ.Apple
[7]. 0x55 0xD6 0xC7 0xF2 0x12 0x79 0x30 0xB5 : 序列化ID的类型为long型因此占用8个字节
[8]. 0x02 :标记号,该字节的8位分表代表不同的含义,
SC_EXTERNALIZABLE 0x04 : 该类实现了java.io.Externalizable接口
SC_BLOCK_DATA 0x08 : Externalizable接口的writeExternal方法写入的数据SC_SERIALIZABLE 0x02 : 该类实现了java.io.Serializable接口SC_WRITE_METHOD 0x01 : 该序列化类实现了writeObject方法
SC_ENUM 0x10 : 该类是枚举(enum)类型
该标记号通过上述信息进行或运算(|)而获得。
[9]. 0x00 0x01 : 代表类属性域的个数
[10].0x4C : 域类型,0x4C代表L即该域类型为java对象类型
[11].0x00 0x04 : 域名称长度
[12].0x6E 0x61 0x6D 0x65 :域名称name
//如果域不是基本类型,则用一个字符串表示其域的类型
[13].0x74 : TC_STRING 一个新字符串
[14].0x00 0x12 :域类型的长度
[15].0x4C 0x6A 0x61 0x76 0x61 0x2F 0x6C 0x61 0x6E 0x67 0x2F 0x53 0x74 0x72 0x69 0x6E 0x67 0x3B :对象类型签名Ljava.lang.String;
[16].0x78 : TC_ENDBLOCKDATA 对象数据块结束标志
//其次是父类描述部分
[17].0x72 : TC_CLASSDESC 声明这是一个新的类描述
[18].0x00 0x14 : 类名长度,换算成十进制就是20
[19].0x63 0x6F 0x6D 0x2E 0x66 0x6E 0x73 0x74 0x2E 0x69 0x6E 0x66 0x6F 0x51 0x2E 0x46 0x72 0x75 0x69 0x74 :类权限定名com.fnst.InfoQ.Fruit
[20].0x64 0x79 0xB9 0xB9 0x16 0xD6 0x47 0xFB : 序列化ID
[21].0x02 : 标记号
[22].0x00 0x01 : 域个数
[23].0x49 : 域类型,0x49代表I即int类型
[24].0x00 0x06 : 域名称长度
[25].0x77 0x65 0x69 0x67 0x68 0x74 : 域名称weight
[26].0x78 : TC_ENDBLOCKDATA 对象数据块结束标志
[27].0x70 : TC_NULL 再没有父类的标志
以上信息的写入由java.io.FileOutputStream的writeObject方法完成,代码的调用关系如下:
java.io.FileOutputStream.writeObject ()
└java.io.FileOutputStream.writeObject0()
└java.io.FileOutputStream.writeOrdinaryObject()
└java.io.FileOutputStream.writeClassDesc()
└java.io.FileOutputStream.writeNonProxyDesc()
└java.io.writeClassDescriptor()
└java.io.ObjectStreamClass. writeNonProxy()
java.io.FileOutputStream.writeOrdinaryObject()方法
---------------------------------------------------------------------
1381 private void writeOrdinaryObject(Object obj,
1382 ObjectStreamClass desc,
1383 boolean unshared)
-中略-
1394 bout.writeByte(TC_OBJECT);//写入流缓冲区中一个新对象的开始标志,步骤[3]
1395 writeClassDesc(desc, false);//写入流缓冲区中该对象的类描述信息
-中略-
1340 writeSerialData(obj, desc);//写入类对象各个域(包括父类的域)的值
-中略-
---------------------------------------------------------------------

java.io.FileOutputStream.writeNonProxyDesc()方法
---------------------------------------------------------------------
1243 private void writeNonProxyDesc(ObjectStreamClass desc, boolean unshared)
1244 throws IOException
1245 {
1246 bout.writeByte(TC_CLASSDESC);//写入流缓冲区一个新类描述,步骤[4]
1247 handles.assign(unshared ? null : desc);
-中略-
//判断使用的序列化版本协议。
1249 if (protocol == PROTOCOL_VERSION_1) {
1250 // do not invoke class descriptor write hook with old protocol
1251 desc.writeNonProxy(this);
1252 } else {
1253 writeClassDescriptor(desc);//写入该类的描述信息
1254 }
-中略-
//写入流缓冲区对象数据块结束标志,步骤[16]
1260 bout.writeByte(TC_ENDBLOCKDATA);
1261
//递归写入该类父类的类描述信息
1262 writeClassDesc(desc.getSuperDesc(), false);
---------------------------------------------------------------------

java.io.ObjectStreamClass. writeNonProxy()方法
---------------------------------------------------------------------
665 void writeNonProxy(ObjectOutputStream out) throws IOException {
666 out.writeUTF(name); //写入类名称的长度域内容,步骤[5],[6]
667 out.writeLong(getSerialVersionUID());//写入类的序列化ID,步骤[7]
668
669 byte flags = 0;//flags 即为要写入的标记位
//该类是否实现了java.io.Externalizable接口
670 if (externalizable) {
671 flags |= ObjectStreamConstants.SC_EXTERNALIZABLE;
672 int protocol = out.getProtocolVersion();
673 if (protocol != ObjectStreamConstants.PROTOCOL_VERSION_1) {
674 flags |= ObjectStreamConstants.SC_BLOCK_DATA;
675 }
//该类是否实现了java.io.Serializable
676 } else if (serializable) {
677 flags |= ObjectStreamConstants.SC_SERIALIZABLE;
678 }
//该类实现了writeObject方法
679 if (hasWriteObjectData) {
680 flags |= ObjectStreamConstants.SC_WRITE_METHOD;
681 }
//该类为枚举类型
682 if (isEnum) {
683 flags |= ObjectStreamConstants.SC_ENUM;
684 }
685 out.writeByte(flags);//写入流缓冲区标记位,步骤[8]
686
687 out.writeShort(fields.length);//写入域个数,步骤[9]
688 for (int i = 0; i < fields.length; i++) {
689 ObjectStreamField f = fields[i];
690 out.writeByte(f.getTypeCode());//写入该域类型编码,步骤[10]
691 out.writeUTF(f.getName());//写入该域的名称长度域内容,步骤[11],[12]
//判断该域类型是否是基本类型,如果该类型前面是以” L”开头的则代表是java类对象,、//如果以”[”开头则代表是数组,否则该域类型为java基本类型
692 if (!f.isPrimitive()) {
//将java对象类型签名或者数组类型签名写入流中
693 out.writeTypeString(f.getTypeString());
694 }
695 }
696}
---------------------------------------------------------------------
第三部分 属性域的值部分(见颜色)
//按照从父类到子类的顺序写入域的值
[28].0x00 0x00 0x00 0x0A : 父类域weight的值为10
[29].0x74 :TC_STRING 一个新字符串
[30].0x00 0x0D :域值的长度,十进制为13
[31].0x44 0x65 0x66 0x61 0x75 0x6C 0x74 0x20 0x41 0x70 0x70 0x6C 0x65 : 域值DefaultApple。域值的写入由上述代码1340行的 writeSerialData函数写入,而该函数的核心在java.io.ObjcetOutputStream.defaultWriteFields中定义,代码如下:
---------------------------------------------------------------------
1493 private void defaultWriteFields(Object obj, ObjectStreamClass desc)
1494 throws IOException
1495 {
1496 // REMIND: perform conservative isInstance check here?
1497 desc.checkDefaultSerialize();
1498
1499 int primDataSize = desc.getPrimDataSize();//获取基本类型的域的个数
//创建存储基本类型域值的字节数组
1500 if (primVals == null || primVals.length < primDataSize) {
1501 primVals = new byte[primDataSize];
1502 }
1503 desc.getPrimFieldValues(obj, primVals);//获取基本类型域的值
1504 bout.write(primVals, 0, primDataSize, false);//将值写入流缓冲区中
1505
//获取对象的所有域
1506 ObjectStreamField[] fields = desc.getFields(false);
//创建存储Java对象类型的域数组
1507 Object[] objVals = new Object[desc.getNumObjFields()];
//序列化对象的基本类型域的个数
1508 int numPrimFields = fields.length - objVals.length;
//获取序列化对象Java对象类型的域的值
1509 desc.getObjFieldValues(obj, objVals);
//将Java对象类型的域写入到流缓冲区中
1510 for (int i = 0; i < objVals.length; i++) {
-中略-
1517 try {
//将Java对象类型域递归写入序列化流缓冲区中,知道最后的域类型为java基本类型
//java.lang.String,枚举类型以及Class类型等
1518 writeObject0(objVals[i],
fields[numPrimFields + i].isUnshared());
} finally {
-下略-
---------------------------------------------------------------------
到此为止,我们对java序列化的原理有了一些基本的认识。通过源代码的阅读,我们总结java序列化算法的基本步骤:
a) 输出序列化的头部信息,包括标识序列化协议的幻数以及协议的版本;
b) 按照由子类到父类的顺序,递归的输出类的描述信息,直到不再有父类为止;类描述信息按照类元数据,类属性信息的顺序写入序列化流中;
c) 按照由父类到子类的顺序,递归的输出对象域的实际数据值;而对象的属性信息是按照基本数据类型到java对象类型的顺序写入序列化流中;其中java对象类型的属性会从步骤a)重新开始递归的输出,直到不再存在java对象类型的属性。
小结
本文从JDK源代码的角度分析了java默认的序列化机制的基本原理,最后总结出了序列化的算法。但是这种序列化是不安全,因为序列化二进制格式完全编写在文档中或者在网络中传播,并且完全可逆。如果采用java默认序列化机制在网络上传输苹果的价格信息(商业机密),我们完全可以截获这些二进制数据按照上述规则获取苹果的价格。为了解决这一问题java提供了自定义的序列化的方法。下一篇中我们将介绍在序列化类中添加readObject和writeObject方法以及实现java.io.Externalizable接口实现自定义序列化的原理。


-以上-

2013年09月21日于南京 。

你可能感兴趣的:(JDK,component)