Java序列化的原理

转载 2016年06月01日 09:36:31

转自:http://jianleixing.iteye.com/blog/2015495

Java序列化的原理

前沿 
欢迎进入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文件中,其中内容如下: 
--------------------------------------------------------------------- 
AC ED 00 05 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
 00 00 00 0A 74 00 0D 44 65 66 61 75 6C 74 20 
41 70 70 6C 65
 
--------------------------------------------------------------------- 
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日于南京 。 

举报

相关文章推荐

Protocol Buffer 序列化原理大揭秘 - 为什么Protocol Buffer性能这么好?

前言 习惯用 Json、XML 数据存储格式的你们,相信大多都没听过Protocol Buffer Protocol Buffer 其实 是 Google出品的一种轻量 & 高效的结构化数据存储格式,...

序列化原理机制浅谈

序列化原理机制浅谈

我是如何成为一名python大咖的?

人生苦短,都说必须python,那么我分享下我是如何从小白成为Python资深开发者的吧。2014年我大学刚毕业..

对Java Serializable(序列化)的理解和总结

我对Java Serializable(序列化)的理解和总结 博客分类:  Java技术 JavaOSSocketCC++  1、序列化是干什么的?        简单说就是...

让rm–rf 不那么危险

原文链接:http://www.prudentwoo.com/archives/559 “命令敲得多了,常在河边走,难免会湿鞋” 前几天,一手误,敲错了命令...

Dubbo学习总结(1)——Dubbo入门基础与实例讲解

Dubbo是阿里巴巴SOA服务化治理方案的核心框架,每天为2,000+个服务提供3,000,000,000+次访问量支持,并被广泛应用于阿里巴巴集团的各成员站点。Dubbo是一个分布式服务框架,致力于...

svn的merge使用例子

svn的merge使用例子

Dubbo-Admin管理平台和Zookeeper注册中心的搭建

ZooKeeper是一个分布式的,开放源码的分布式应用程序协调服务,是Google的Chubby一个开源的实现,是Hadoop和Hbase的重要组件。它是一个为分布式应用提供一致性服务的软件,提供的功...

Caffe学习笔记(五)—— 相关cpp编译及数据转成lmdb格式

本文主要介绍:Windows下,如何caffe-windows-master\tools 文件夹中的cpp进行编译和调用,以及如何把图像转化为lmdb格式。 1. 相关cpp编译 1.1 创建控制...

linux rm -rf * 文件恢复记

周海汉/文 2013.9.12 手太快,肠子都毁清了。本来是删除一个文件 rm path/myfile.txt 结果不知为何加了个*,变成了 rm path/myfile.txt * 赶紧ls,发现...

"rm -rf /"删服务器直播

有朋友说”rm -rf /”可以直接删除服务器所有资料,我好奇,然后尝试了下,下面开始直播. 当然不能直接去删公司服务器,我还想混口饭吃, 我们用个虚拟机吧,Ubuntu 16.04 的Virtua...
返回顶部
收藏助手
不良信息举报
您举报文章:深度学习:神经网络中的前向传播和反向传播算法推导
举报原因:
原因补充:

(最多只允许输入30个字)