如何正确的使用Java序列化技术(转)
??摘要:本文比较全面的介绍了Java 序列化技术方方面面的知识,从序列化技术的基础谈起,
介绍了Java 序列化技术的机制和序列化技术的原理。并在随后的部分详细探讨了序列化的
高级主题-如何精确的控制序列化机制。通过阅读该文章,你可以了解如何使用Java 序列
化机制的方式和正确使用的方法,避免实际编程中对该技术的误用。并能掌握如何高效使用
该技术来完成特殊的功能。
关键字:序列化(Serialize)、反序列化—eSerialize)、类加载(ClassLoad)、指纹技术
(fingerprint)
1 Java 序列化技术概述
Java 序列化技术可以使你将一个对象的状态写入一个Byte 流里,并且可以从其它地方
把该Byte 流里的数据读出来。重新构造一个相同的对象。这种机制允许你将对象通过网络
进行传播,并可以随时把对象持久化到数据库、文件等系统里。Java的序列化机制是RMI、
EJB、JNNI等技术的技术基础。
1.1 序列化技术基础
并非所有的Java 类都可以序列化,为了使你指定的类可以实现序列化,你必须使该类
实现如下接口:
java.io.Serializable
需要注意的是,该接口什么方法也没有。实现该类只是简单的标记你的类准备支持序列
化功能。我们来看如下的代码:
?1.2 对象的序列化及反序列化
上面的类Person 类实现了Serializable 接口,因此是可以序列化的。我们如果要把一个
可以序列化的对象序列化到文件里或者数据库里,需要下面的类的支持:
java.io.ObjectOutputStream
如何正确的使用Java序列化技术 技术研究系列
下面的代码负责完成Person类的序列化操作:?1.3 序列化对类的处理原则
并不是一个实现了序列化接口的类的所有字段及属性都是可以序列化的。我们分为以下
几个部分来说明:
u 如果该类有父类,则分两种情况来考虑,如果该父类已经实现了可序列化接口。则
其父类的相应字段及属性的处理和该类相同;如果该类的父类没有实现可序列化接
口,则该类的父类所有的字段属性将不会序列化。
u 如果该类的某个属性标识为static类型的,则该属性不能序列化;
u 如果该类的某个属性采用transient关键字标识,则该属性不能序列化;
需要注意的是,在我们标注一个类可以序列化的时候,其以下属性应该设置为transient
来避免序列化:
u 线程相关的属性;
u 需要访问IO、本地资源、网络资源等的属性;
u 没有实现可序列化接口的属性;(注:如果一个属性没有实现可序列化,而我们又
没有将其用transient 标识, 则在对象序列化的时候, 会抛出
java.io.NotSerializableException异常)。
1.4 构造函数和序列化
对于父类的处理,如果父类没有实现序列化接口,则其必须有默认的构造函数(即没有
参数的构造函数)。为什么要这样规定呢?我们来看实际的例子。仍然采用上面的Humanoid
和Person 类。我们在其构造函数里分别加上输出语句:?在命令行运行其序列化程序和反序列化程序的结果为:
如何正确的使用Java序列化技术 技术研究系列
可以看到,在从流中读出数据构造Person对象的时候,Person 的父类Humanoid的默认
构造函数被调用了。当然,这点完全不用担心,如果你没有给父类一个默认构造函数,则编
译的时候就会报错。
这里,我们把父类Humanoid做如下的修改:?我们主要通过背景为黄色的两行代码查看其类加载器,运行结果如下:
由此可以看出,序列化类的类加载器正式其反序列化实现类的类加载器。这样的话我们
就可以通过使最新的Person 类的版本发布为只有该反序列化器的ClassLoader可见。而较旧
的版本则不为该ClassLoader 可见的方法来避免在反序列化过程中类的多重版本的问题。当
然,下面就类的版本问题我们还要做专门的探讨。
如何正确的使用Java序列化技术 技术研究系列
2.2.2 序列化类多重版本的控制
如果在反序列化的JVM 里出现了该类的不同时期的版本,那么反序列化机制是如何处
理的呢?
为了避免这种问题,Java的序列化机制提供了一种指纹技术,不同的类带有不同版本的
指纹信息,通过其指纹就可以辨别出当前JVM 里的类是不是和将要反序列化后的对象对应
的类是相同的版本。该指纹实现为一个64bit的long 类型。通过安全的Hash算法(SHA-1)
来将序列化的类的基本信息(包括类名称、类的编辑者、类的父接口及各种属性等信息)处
理为该64bit的指纹。我们可以通过JDK自带的命令serialver来打印某个可序列化类的指纹
信息。如下:
当我们的两边的类版本不一致的时候,反序列化就会报错:
如何正确的使用Java序列化技术 技术研究系列
解决之道:从上面的输出可以看出,该指纹是通过如下的内部变量来提供的:
private static final long serialVersionUID;
如果我们在类里提供对该属性的控制,就可以实现对类的序列化指纹的自定义控制。为
此,我们在Person 类里定义该变量:
private static final long serialVersionUID= 6921661392987334380L;
则当我们修改了Person 类,发布不同的版本到反序列化端的JVM,也不会有版本冲突
的问题了。需要注意的是,serialVersionUID 的值是需要通过serialver 命令来取得。而不能
自己随便设置,否则可能有重合的。
需要注意的是,手动设置serialVersionUID 有时候会带来一些问题,比如我们可能对类
做了关键性的更改。引起两边类的版本产生实质性的不兼容。为了避免这种失败,我们需要
知道什么样的更改会引起实质性的不兼容,下面的表格列出了会引起实质性不兼容和可以忽
略(兼容)的更改:
更改类型 例子
兼容的更改
u 添加属性(Adding fields)
u 添加/删除类(adding/removing classes)
u 添加/删除writeObject/readObject方法(adding/removing
writeObject/readObject)
u 添加序列化标志(adding Serializable)
u 改变访问修改者(changing access modifier)
u 删除静态/不可序列化属性(removing static/transient from
a field)
不兼容的更改
u 删除属性—eleting fields)
u 在一个继承或者实现层次里删除类(removing classes in a
hierarchy)
u 添加静态/不可序列化字段(adding static/transient to a
field)
u 修改简单变量类型(changing type of a primitive)
u switching between Serializable or Externalizable
u 删除序列化标志(removing Serializable/Externalizable)
u 改变readObject/writeObject对默认属性值的控制(changing
whether readObject/writeObject handles default field
data)
u adding writeReplace or readResolve that produces
objects incompatible with older versions
另外,从Java 的序列化规范里并没有指出当我们对类做了实质性的不兼容修改后反序
列化会有什么后果。并不是所有的不兼容修改都会引起反序列化的失败。比如,如果我们删
除了一个属性,则在反序列化的时候,反序列化机制只是简单的将该属性的数据丢弃。从
JDK 的参考里,我们可以得到一些不兼容的修改引起的后果如下表:
如何正确的使用Java序列化技术 技术研究系列
不兼容的修改 引起的反序列化结果
删除属性
—eleting a field) Silently ignored
在一个继承或者实现层次里删除类
(Moving classes in inheritance
hierarchy)
Exception
添加静态/不可序列化属性
(Adding static/transient)
Silently ignored
修改基本属性类型
(Changing primitive type)
Exception
改变对默认属性值的使用
(Changing use of default field data)
Exception
在序列化和非序列化及内外部类之间切换
(Switching Serializable and
Externalizable)
Exception
删除Serializable或者Externalizable标志
(Removing Serializable or
Externalizable)
Exception
返回不兼容的类
(Returning incompatible class)
Depends on incompatibility
2.3 显示的控制对属性的序列化过程
在默认的Java 序列化机制里,有关对象属性到byte 流里的属性的对应映射关系都是自
动而透明的完成的。在序列化的时候,对象的属性的名称默认作为byte 流里的名称。当该
对象反序列化的时候,就是根据byte 流里的名称来对应映射到新生成的对象的属性里去的。
举个例子来说。在我们的一个Person对象序列化的时候,Person的一个属性firstName就作
为byte 流里该属性默认的名称。当该Person 对象反序列化的时候,序列化机制就把从byte
流里得到的firstName 的值赋值给新的Person 实例里的名叫firstName的属性。
Java的序列化机制提供了相关的钩子函数给我们使用,通过这些钩子函数我们可以精确
的控制上述的序列化及反序列化过程。ObjectInputStream的内部类GetField提供了对把属性
数据从流中取出来的控制,而ObjectOutputStream的内部类PutField则提供了把属性数据放
入流中的控制机制。就ObjectInputStream来讲,我们需要在readObject方法里来完成从流中
读取相应的属性数据。比如我们现在把Person 类的版本从下面的表一更新到表二:?
为此,我们需要编写Person类的readObject方法如下:?
??我们的执行顺序是:
1) 编译老的Person及所有类;
2) 将老的Person序列化到文件里;
3) 修改为新版本的Person类;
4) 编译新的Person类;
5) 反序列化Person;
执行结果非常顺利,修改后的反序列化机制仍然正确的从流中获取了旧版本Person 的
属性信息并完成对新版本的Person的属性赋值。
使用ObjectInputStream的readObject 来处理反序列化的属性时,有两点需要注意:
u 一旦采用自己控制属性的反序列化,则必须完成所有属性的反序列化(即要给所有
属性赋值);
u 在使用内部类GetField 的get 方法的时候需要注意,如果get 的是一个既不在老版
本出现的属性,有没有在新版本出现的属性,则该方法会抛出异常:
IllegalArgumentException: no such field,所以我们应该在一个try块里
来使用该方法。
同理,我们可以通过writeObject 方法来控制对象属性的序列化过程。这里就不再一一
举例了,如果你有兴趣的话,可以自己实现Person 类的writeObject 方法,并且使用
ObjectOutputStream的内部类PutField来完成属性的手动序列化操作。
3 总结
Java 序列化机制提供了强大的处理能力。一般来讲,为了尽量利用Java 提供的自动化
机制,我们不需要对序列化的过程做任何的干扰。但是在某些时候我们需要实现一些特殊的
功能,比如类的多版本的控制,特殊字段的序列化控制等。我们可以通过多种方式来实现这
些功能:
u 利用序列化机制提供的钩子函数readObject和writeObject;
u 覆盖序列化类的metaData 信息;
如何正确的使用Java序列化技术 技术研究系列
u 使类实现Externalizable 接口而不是实现Serializable接口。
关于Externalizable 接口更多的介绍,可以参考JDK 的帮助提供的详细文档,同时也可
以快速参考《Thinking in Java》这本书第十章-Java IO系统的介绍。
参考资料:
1、 SUN关于Java 的虚拟机规范《The Java Virtual Machine Specification》;
2、 《Thinking in Java》;
3、 《基于Java平台的组件化开发技术》。
关于作者:
高雁冰(网名Haiger 或者tuskrabbit)是深圳华为技术有限公司的商业网络业务部总体设计
部架构设计师,拥有多个大型企业应用系统(百万、千万用户级)的架构设计经验。个人研
究方向主要集中在J2EE平台的全程建模技术、WEB工程技术及企业应用集成(EAI)的咨
询和实施上。可以通过gaoyb@huawei.com与他取得联系。本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/qingtanlang/archive/2008/03/30/2231377.aspx