您的位置 首页 java

浅析 Java 序列化

序列化 机制

序列化 (Serialization) 是指将数据结构或对象状态转换成字节流 (例如存储成文件、内存缓冲,或经由网络传输) ,以留待后续在相同或另一台计算机环境中,能够恢复对象原来状态的过程。序列化机制在 Java 中有着广泛的应用,EJB、RMI、Hessian等技术都以此为基础。

序列化

我们先用一个简单的序列化示例来看看Java究竟是如何对一个对象进行序列化的:

public class SerializationDemo implements Serializable {
 private String stringField;
 private int intField;
 public SerializationDemo(String s, int i) {
 this.stringField = s;
 this.intField = i;
 }
 public static void main(String[] args) throws IOException {
 ByteArrayOutputStream bout = new ByteArrayOutputStream();
 ObjectOutputStream out = new ObjectOutputStream(bout);
 out.writeObject(new SerializationDemo("gyyyy", 97777));
 }
}

 

如果熟悉PHP的同学应该知道,这个对象在经过PHP序列化后得到的 字符串 如下 (因为PHP与Java的编程习惯有所区别,这里字段访问权限全改为了public,private和protected从表现形式上来说差不多,只是多了些特殊的标识而已,为了减少一些零基础的同学不必要的疑惑,这里暂不讨论)

O:17:"SerializationDemo":2:{s:11:"stringField";s:5:"gyyyy";s:8:"intField";i:97777;}
 

其中,O:17:”…”表示当前是一个对象,以及该对象类名的字符串长度和值,2:{…}表示该类有2个字段 (元素间用;分隔,键值对也分为前后两个元素表示,也就是说,如果是2个字段,则总共会包含4个元素) ,s:11:”…”表示当前是一个长度为11的字符串,i:…表示当前是一个整数。

由此可知,PHP序列化字符串基本上是可人读的,而且对于类对象来说,字段等成员属性的序列化顺序与定义顺序一致。我们完全可以通过手工的方式来构造任意一个PHP对象的序列化字符串。

而该对象经过Java序列化后得到的则是一个二进制串:

ac ed 00 05 73 72 00 11 53 65 72 69 61 6c 69 7a ....sr.. Serializ
61 74 69 6f 6e 44 65 6d 6f d9 35 3c f7 d6 0a c6 ationDem o.5<....
d5 02 00 02 49 00 08 69 6e 74 46 69 65 6c 64 4c ....I..i ntFieldL
00 0b 73 74 72 69 6e 67 46 69 65 6c 64 74 00 12 ..string Fieldt..
4c 6a 61 76 61 2f 6c 61 6e 67 2f 53 74 72 69 6e Ljava/la ng/Strin
67 3b 78 70 00 01 7d f1 74 00 05 67 79 79 79 79 g;xp..}. t..gyyyy

 

仔细观察二进制串中的部分可读数据,我们也可以差不多分辨出该对象的一些基本内容。但同样为了手写的目的 (为什么有这个目的?原因很简单,为了不被语言环境束缚) ,以及为接下来的序列化执行流程分析做准备,我们先依次来解读一下这个二进制串中的各个元素。

  • 0xaced,魔术头
  • 0x0005,版本号 (JDK主流版本一致,下文如无特殊标注,都以JDK8u为例)
  • 0x73,对象类型标识 (0x7n基本上都定义了类型标识符常量,但也要看出现的位置,毕竟它们都在可见字符的范围,详见java.io.ObjectStreamConstants)
  • 0x72,类描述符标识
  • 0x0011…,类名字符串长度和值 (Java序列化中的UTF8格式标准)
  • 0xd9353cf7d60ac6d5,序列版本唯一标识 (serialVersionUID,简称SUID)
  • 0x02,对象的序列化属性标志位,如是否是Block Data模式、自定义writeObject(),Serializable、Externalizable或Enum类型等
  • 0x0002,类的字段个数
  • 0x49,整数类型签名的第一个字节,同理,之后的0x4c为字符串类型签名的第一个字节 (类型签名表示与 JVM 规范中的定义相同)
  • 0x0008…,字段名字符串长度和值,非原始数据类型的字段还会在后面加上数据类型标识、完整类型签名长度和值,如之后的0x740012…
  • 0x78 Block Data结束标识
  • 0x70 父类描述符标识,此处为null
  • 0x00017df1 整数字段intField的值 (Java序列化中的整数格式标准) ,非原始数据类型的字段则会按对象的方式处理,如之后的字符串字段stringField被识别为字符串类型,输出字符串类型标识、字符串长度和值

由此可以看出,除了基本格式和一些整数表现形式上的不同之外,Java和PHP的序列化结果还是存在很多相似的地方,比如除了具体值外都会对类型进行描述。

需要注意的是,Java序列化中对字段进行封装时,会按原始和非原始数据类型排序 (有同学可能想问为什么要这么做,这里我只能简单解释原因有两个,一是因为它们两个的表现形式不同,原始数据类型字段可以直接通过偏移量读取固定个数的字节来赋值;二是在封装时会计算原始类型字段的偏移量和总偏移量,以及非原始类型字段的个数,这使得反序列化阶段可以很方便的把原始和非原始数据类型分成两部分来处理) ,且其中又会按字段名排序。

而开头固定的0xaced0005也可以作为Java序列化二进制串 (Base64编码为rO0AB…) 的识别标识。

让我们把这个对象再改复杂些:

class SerializationSuperClass implements Serializable {
 private String superField;
}
class SerializationComponentClass implements Serializable {
 private String componentField;
}
public class SerializationDemo extends SerializationSuperClass implements Serializable {
 private SerializationComponentClass component;
 // omit
}

 

它序列化后的二进制串大家可以自行消化理解一下,注意其中的嵌套对象,以及0x71表示的Reference类型标识 (形式上与JVM的常量池类似,用于非原始数据类型的引用对象池索引,这个引用对象池在序列化和反序列化创建时的元素填充顺序会保持一致)

ac ed 00 05 73 72 00 11 53 65 72 69 61 6c 69 7a ....sr.. Serializ
61 74 69 6f 6e 44 65 6d 6f 1a 7f cd d3 53 6f 6b ationDem o....Sok
15 02 00 03 49 00 08 69 6e 74 46 69 65 6c 64 4c ....I..i ntFieldL
00 09 63 6f 6d 70 6f 6e 65 6e 74 74 00 1d 4c 53 ..compon entt..LS
65 72 69 61 6c 69 7a 61 74 69 6f 6e 43 6f 6d 70 erializa tionComp
6f 6e 65 6e 74 43 6c 61 73 73 3b 4c 00 0b 73 74 onentCla ss;L..st
72 69 6e 67 46 69 65 6c 64 74 00 12 4c 6a 61 76 ringFiel dt..Ljav
61 2f 6c 61 6e 67 2f 53 74 72 69 6e 67 3b 78 72 a/ lang /S tring;xr
00 17 53 65 72 69 61 6c 69 7a 61 74 69 6f 6e 53 ..Serial izationS
75 70 65 72 43 6c 61 73 73 de c6 50 b7 d1 2f a3 uperClas s..P../.
27 02 00 01 4c 00 0a 73 75 70 65 72 46 69 65 6c '...L..s uperFiel
64 71 00 7e 00 02 78 70 70 00 01 7d f1 73 72 00 dq.~..xp p..}.sr.
1b 53 65 72 69 61 6c 69 7a 61 74 69 6f 6e 43 6f .Seriali zationCo
6d 70 6f 6e 65 6e 74 43 6c 61 73 73 3c 76 ba b7 mponentC lass<v..
dd 9e 76 c4 02 00 01 4c 00 0e 63 6f 6d 70 6f 6e ..v....L ..compon
65 6e 74 46 69 65 6c 64 71 00 7e 00 02 78 70 70 entField q.~..xpp
74 00 05 67 79 79 79 79 t..gyyyy
 
浅析 Java 序列化

简单的分析一下序列化的执行流程:

  1. ObjectOutputStream实例初始化时,将魔术头和版本号写入bout (BlockDataOutputStream类型)
  2. 调用ObjectOutputStream.writeObject()开始写对象数据
  • 写入对象类型标识
  • writeClassDesc()进入分支writeNonProxyDesc()写入类描述数据
  • writeSerialData()写入对象的序列化数据
  • 写入类描述符标识
  • 写入类名
  • 写入SUID (当SUID为空时,会进行计算并赋值,细节见下面关于SerialVersionUID章节)
  • 计算并写入序列化属性标志位
  • 写入字段信息数据
  • 写入Block Data结束标识
  • 写入父类描述数据
  • 若类自定义了writeObject(),则调用该方法写对象,否则调用defaultWriteFields()写入对象的字段数据 (若是非原始类型,则递归处理子对象)
  • ObjectStreamClass.lookup()封装待序列化的类描述 (返回ObjectStreamClass类型) ,获取包括类名、自定义serialVersionUID、可序列化字段 (返回ObjectStreamField类型) 和构造方法,以及writeObject、readObject方法等
  • writeOrdinaryObject()写入对象数据

关注“IT魔幻屋”,获取更多java编程经验分享,通往技术资深的道路不止一条,我们在路上,那么,你呢?

文章来源:智云一二三科技

文章标题:浅析 Java 序列化

文章地址:https://www.zhihuclub.com/181276.shtml

关于作者: 智云科技

热门文章

网站地图