[javase]java序列化学习笔记
2013-04-23 01:55
429 查看
参考文章:
http://www.infoq.com/cn/articles/cf-java-object-serialization-rmi http://www.ibm.com/developerworks/cn/java/j-5things1/ http://www.ibm.com/developerworks/cn/java/j-lo-serial/ http://blog.csdn.net/zhaozheng7758/article/details/7820018
对于一个存在于Java虚拟机中的对象来说,其内部的状态只保持在内存中。JVM停止之后,这些状态就丢失了。在很多情况下,对象的内部状态是需要被持久化下来的。提到持久化,最直接的做法是保存到文件系统或是数据库之中。这种做法一般涉及到自定义存储格式以及繁琐的数据转换。对象关系映射(Object-relational
mapping)是一种典型的用关系数据库来持久化对象的方式,也存在很多直接存储对象的对象数据库。对象序列化机制(object serialization)是Java语言内建的一种对象持久化方式,可以很容易的在JVM中的活动对象和字节数组(流)之间进行转换。除了可以很简单的实现持久化之外,序列化机制的另外一个重要用途是在远程方法调用中,用来对开发人员屏蔽底层实现细节。
Java序列化的目的:
支持运行在不同虚拟机上不同版本类之间的双向通讯;
定义允许JAVA类读取用相同类较老版本写入的数据流的机制;
定义允许JAVA类写用相同类较老版本读取的数据流的机制;
提供对持久性和RMI的序列化;
产生压缩流且运行良好以使RMI能序列化;
辨别写入的是否是本地流;
保持非版本化类的低负载;
由于Java提供了良好的默认支持,实现基本的对象序列化是件比较简单的事。待序列化的Java类只需要实现Serializable接口即可。Serializable仅是一个标记接口,并不包含任何需要实现的具体方法。实现该接口只是为了声明该Java类的对象是可以被序列化的。实际的序列化和反序列化工作是通过ObjectOuputStream和ObjectInputStream来完成的。ObjectOutputStream的writeObject方法可以把一个Java对象写入到流中,ObjectInputStream的readObject方法可以从流中读取一个Java对象。在写入和读取的时候,虽然用的参数或返回值是单个对象,但实际上操纵的是一个对象图,包括该对象所引用的其它对象,以及这些对象所引用的另外的对象。Java会自动帮你遍历对象图并逐个序列化。除了对象之外,Java中的基本类型和数组也是可以通过
ObjectOutputStream和ObjectInputStream来序列化的。
上面的代码给出了典型的把Java对象序列化之后保存到磁盘上,以及从磁盘上读取的基本方式。 User类只是声明了实现Serializable接口。
在默认的序列化实现中,Java对象中的非静态和非瞬时域都会被包括进来,而与域的可见性声明没有关系。这可能会导致某些不应该出现的域被包含在序列化之后的字节数组中,比如密码等隐私信息。由于Java对象序列化之后的格式是固定的,其它人可以很容易的从中分析出其中的各种信息。对于这种情况,一种解决办法是把域声明为瞬时的,即使用transient关键词。另外一种做法是添加一个serialPersistentFields?
域来声明序列化时要包含的域。从这里可以看到在Java序列化机制中的这种仅在书面层次上定义的契约。声明序列化的域必须使用固定的名称和类型。在后面还可以看到其它类似这样的契约。虽然Serializable只是一个标记接口,但它其实是包含有不少隐含的要求。下面的代码给出了 serialPersistentFields的声明示例,即只有firstName这个域是要被序列化的。
基本的对象序列化机制让开发人员可以在包含哪些域上进行定制。如果想对序列化的过程进行更加细粒度的控制,就需要在类中添加writeObject和对应的 readObject方法。这两个方法属于前面提到的序列化机制的隐含契约的一部分。在通过ObjectOutputStream的 writeObject方法写入对象的时候,如果这个对象的类中定义了writeObject方法,就会调用该方法,并把当前 ObjectOutputStream对象作为参数传递进去。writeObject方法中一般会包含自定义的序列化逻辑,比如在写入之前修改域的值,或是写入额外的数据等。对于writeObject中添加的逻辑,在对应的readObject中都需要反转过来,与之对应。
在添加自己的逻辑之前,推荐的做法是先调用Java的默认实现。在writeObject方法中通过ObjectOutputStream的defaultWriteObject来完成,在readObject方法则通过ObjectInputStream的defaultReadObject来实现。
假设 Person 类中的敏感数据是 age 字段。毕竟,女士忌谈年龄。 我们可以在序列化之前模糊化该数据,将数位循环左移一位,然后在反序列化之后复位。(您可以开发更安全的算法,当前这个算法只是作为一个例子。)
为了 “hook” 序列化过程,我们将在 Person 上实现一个 writeObject 方法;为了
“hook” 反序列化过程,我们将在同一个类上实现一个 readObject 方法。重要的是这两个方法的细节要正确 — 如果访问修改方法、参数或名称不同,那么代码将不被察觉地失败,Person 的
age 将暴露。
在有些情况下,可能会希望在序列化的时候使用另外一个对象来代替当前对象。其中的动机可能是当前对象中包含了一些不希望被序列化的域,比如这些域都是从另外一个域派生而来的;也可能是希望隐藏实际的类层次结构;还有可能是添加自定义的对象管理逻辑,如保证某个类在JVM中只有一个实例。相对于把无关的域都设成transient来说,使用对象替换是一个更好的选择,提供了更多的灵活性。替换对象的作用类似于Java EE中会使用到的传输对象(Transfer
Object)。
demo1:
一个订单系统中需要把订单的相关信息序列化之后,通过网络来传输。订单类Order引用了客户类Customer。在默认序列化的情况下,Order类对象被序列化的时候,其引用的Customer类对象也会被序列化,这可能会造成用户信息的泄露。对于这种情况,可以创建一个另外的对象来在序列化的时候替换当前的Order类的对象,并把用户信息隐藏起来。
demo2:打包与解包代理
在通过ObjectInputStream的readObject方法读取到一个对象之后,这个对象是一个新的实例,但是其构造方法是没有被调用的,其中的域的初始化代码也没有被执行。对于那些没有被序列化的域,在新创建出来的对象中的值都是默认的。也就是说,这个对象从某种角度上来说是不完备的。这有可能会造成一些隐含的错误。调用者并不知道对象是通过一般的new操作符来创建的,还是通过反序列化所得到的。解决的办法就是在类的readObject方法里面,再执行所需的对象初始化逻辑。对于一般的Java类来说,构造方法中包含了初始化的逻辑。可以把这些逻辑提取到一个方法中,在readObject方法中调用此方法。
把一个Java对象序列化之后,所得到的字节数组一般会保存在磁盘或数据库之中。在保存完成之后,有可能原来的Java类有了更新,比如添加了额外的域。这个时候从兼容性的角度出发,要求仍然能够读取旧版本的序列化数据。在读取的过程中,当ObjectInputStream发现一个对象的定义的时候,会尝试在当前JVM中查找其Java类定义。这个查找过程不能仅根据Java类的全名来判断,因为当前JVM中可能存在名称相同,但是含义完全不同的Java 类。这个对应关系是通过一个全局惟一标识符serialVersionUID来实现的。通过在实现了Serializable接口的类中定义该域,就声明了该Java类的一个惟一的序列化版本号。JVM会比对从字节数组中得出的类的版本号,与JVM中查找到的类的版本号是否一致,来决定两个类是否是兼容的。对于开发人员来说,需要记得的就是在实现了Serializable接口的类中定义这样的一个域,并在版本更新过程中保持该值不变。当然,如果不希望维持这种向后兼容性,换一个版本号即可。该域的值一般是综合Java类的各个特性而计算出来的一个哈希值,可以通过Java提供的serialver命令来生成。在Eclipse中,如果Java类实现了Serializable接口,Eclipse会提示并帮你生成这个serialVersionUID。
在类版本更新的过程中,某些操作会破坏向后兼容性。如果希望维持这种向后兼容性,就需要格外的注意。一般来说,在新的版本中添加东西不会产生什么问题,而去掉一些域则是不行的。(经过测试,增加和去掉一些域不报错)
demo1:接受的对象比写入文件的对象字段多
demo2:接受的对象比写入文件的对象字段少
1.将对象写入文件和从文件读取对象与对象的版本无关,只需要是同一个class,且serialVersion相同即可
2.新增的字段按照默认方式进行赋值,基本类型(int->0, long->0L, double->0.0, boolean->false ...),对象类型赋值为null.
前面提到,Java对象序列化之后的内容格式是公开的。所以可以很容易的从中提取出各种信息。从实现的角度来说,可以从不同的层次来加强序列化的安全性。
对序列化之后的流进行加密。这可以通过CipherOutputStream来实现。
实现自己的writeObject和readObject方法,在调用defaultWriteObject之前,先对要序列化的域的值进行加密处理。
使用一个SignedObject或SealedObject来封装当前对象,用SignedObject或SealedObject进行序列化。
在从流中进行反序列化的时候,可以通过ObjectInputStream的registerValidation方法添加ObjectInputValidation接口的实现,用来验证反序列化之后得到的对象是否合法。
使用SealedObject的demo:
RMI(Remote Method Invocation)是Java中的远程过程调用(Remote Procedure Call,RPC)实现,是一种分布式Java应用的实现方式。它的目的在于对开发人员屏蔽横跨不同JVM和网络连接等细节,使得分布在不同JVM上的对象像是存在于一个统一的JVM中一样,可以很方便的互相通讯。之所以在介绍对象序列化之后来介绍RMI,主要是因为对象序列化机制使得RMI非常简单。调用一个远程服务器上的方法并不是一件困难的事情。开发人员可以基于Apache
MINA或是Netty这样的框架来写自己的网络服务器,亦或是可以采用REST架构风格来编写HTTP服务。但这些解决方案中,不可回避的一个部分就是数据的编排和解排(marshal/unmarshal)。需要在Java对象和传输格式之间进行互相转换,而且这一部分逻辑是开发人员无法回避的。RMI的优势在于依靠Java序列化机制,对开发人员屏蔽了数据编排和解排的细节,要做的事情非常少。JDK
5之后,RMI通过动态代理机制去掉了早期版本中需要通过工具进行代码生成的繁琐方式,使用起来更加简单。
RMI采用的是典型的客户端-服务器端架构。首先需要定义的是服务器端的远程接口,这一步是设计好服务器端需要提供什么样的服务。对远程接口的要求很简单,只需要继承自RMI中的Remote接口即可。Remote和Serializable一样,也是标记接口。远程接口中的方法需要抛出RemoteException。定义好远程接口之后,实现该接口即可。如下面的Calculator是一个简单的远程接口。
实现了远程接口的类的实例称为远程对象。创建出远程对象之后,需要把它注册到一个注册表之中。这是为了客户端能够找到该远程对象并调用。
CalculatorServer是远程对象的Java类。在它的start方法中通过UnicastRemoteObject的exportObject把当前对象暴露出来,使得它可以接收来自客户端的调用请求。再通过Registry的rebind方法进行注册,使得客户端可以查找到。
客户端的实现就是首先从注册表中查找到远程接口的实现对象,再调用相应的方法即可。实际的调用虽然是在服务器端完成的,但是在客户端看来,这个接口中的方法就好像是在当前JVM中一样。这就是RMI的强大之处。
在运行的时候,需要首先通过rmiregistry命令来启动RMI中用到的注册表服务器。
为了通过Java的序列化机制来进行传输,远程接口中的方法的参数和返回值,要么是Java的基本类型,要么是远程对象,要么是实现了 Serializable接口的Java类。当客户端通过RMI注册表找到一个远程接口的时候,所得到的其实是远程接口的一个动态代理对象。当客户端调用其中的方法的时候,方法的参数对象会在序列化之后,传输到服务器端。服务器端接收到之后,进行反序列化得到参数对象。并使用这些参数对象,在服务器端调用实际的方法。调用的返回值Java对象经过序列化之后,再发送回客户端。客户端再经过反序列化之后得到Java对象,返回给调用者。这中间的序列化过程对于使用者来说是透明的,由动态代理对象自动完成。除了序列化之外,RMI还使用了动态类加载技术。当需要进行反序列化的时候,如果该对象的类定义在当前JVM中没有找到,RMI会尝试从远端下载所需的类文件定义。可以在RMI程序启动的时候,通过JVM参数java.rmi.server.codebase来指定动态下载Java类文件的URL。
demo1:
false 才对,但是最后结果输出如下图所示
原因是:Java 序列化机制为了节省磁盘空间,具有特定的存储规则,当写入文件的为同一对象时,并不会再将对象的内容进行存储,而只是再次存储一份引用,上面增加的 5 字节的存储空间就是新增引用和一些控制信息的空间。反序列化时,恢复引用关系,使得清单 3 中的 t1 和 t2 指向唯一的对象,二者相等,输出 true。该存储规则极大的节省了存储空间。
demo2:
上面一段代码的目的是希望将 test 对象两次保存到 result.obj 文件中,写入一次以后修改对象属性值再
次保存第二次,然后从 result.obj 中再依次读出两个对象,输出这两个对象的 i 属性值。案例代码的目的原本是希望一次性传输对象修改前后的状态。
结果两个输出的都是 1, 原因就是第一次写入对象以后,第二次再试图写的时候,虚拟机根据引用关系知道已经有一个相同对象已经写入文件,因此只保存第二次写的引用,所以读取时,都是第一次保存的对象。读者在使用一个文件多次 writeObject 需要特别注意这个问题。
外部化和序列化是实现同一目标的两种不同方法。下面让我们分析一下序列化和外部化之间的主要区别。
通过Serializable接口对对象序列化的支持是内建于核心 API 的,但是java.io.Externalizable的所有实现者必须提供读取和写出的实现。Java 已经具有了对序列化的内建支持,也就是说只要制作自己的类java.io.Serializable,Java 就会试图存储和重组你的对象。如果使用外部化,你就可以选择完全由自己完成读取和写出的工作,Java 对外部化所提供的唯一支持是接口:
voidreadExternal(ObjectInput in)
void writeExternal(ObjectOutput out)
现在如何实现readExternal() 和writeExternal() 就完全看你自己了。
序列化会自动存储必要的信息,用以反序列化被存储的实例,而外部化则只保存被存储的类的标识。当你通过java.io.Serializable接口序列化一个对象时,有关类的信息,比如它的属性和这些属性的类型,都与实例数据一起被存储起来。在选择走Externalizable这条路时,Java 只存储有关每个被存储类型的非常少的信息。
demo:
重要!!!
1.将对象写入文件和从文件读取对象与对象的版本无关,只需要是同一个class,且serialVersion相同即可
2.新增的字段按照默认方式进行赋值,基本类型(int->0, long->0L, double->0.0, boolean->false ...),对象类型赋值为null.
3.测试RPC调用同样如此,远程端interface增加和减少字段,本地调用后不报错,只需要序列号相同即可
http://www.infoq.com/cn/articles/cf-java-object-serialization-rmi http://www.ibm.com/developerworks/cn/java/j-5things1/ http://www.ibm.com/developerworks/cn/java/j-lo-serial/ http://blog.csdn.net/zhaozheng7758/article/details/7820018
对象序列化的必要性及目的
对于一个存在于Java虚拟机中的对象来说,其内部的状态只保持在内存中。JVM停止之后,这些状态就丢失了。在很多情况下,对象的内部状态是需要被持久化下来的。提到持久化,最直接的做法是保存到文件系统或是数据库之中。这种做法一般涉及到自定义存储格式以及繁琐的数据转换。对象关系映射(Object-relationalmapping)是一种典型的用关系数据库来持久化对象的方式,也存在很多直接存储对象的对象数据库。对象序列化机制(object serialization)是Java语言内建的一种对象持久化方式,可以很容易的在JVM中的活动对象和字节数组(流)之间进行转换。除了可以很简单的实现持久化之外,序列化机制的另外一个重要用途是在远程方法调用中,用来对开发人员屏蔽底层实现细节。
Java序列化的目的:
支持运行在不同虚拟机上不同版本类之间的双向通讯;
定义允许JAVA类读取用相同类较老版本写入的数据流的机制;
定义允许JAVA类写用相同类较老版本读取的数据流的机制;
提供对持久性和RMI的序列化;
产生压缩流且运行良好以使RMI能序列化;
辨别写入的是否是本地流;
保持非版本化类的低负载;
基本的对象序列化
由于Java提供了良好的默认支持,实现基本的对象序列化是件比较简单的事。待序列化的Java类只需要实现Serializable接口即可。Serializable仅是一个标记接口,并不包含任何需要实现的具体方法。实现该接口只是为了声明该Java类的对象是可以被序列化的。实际的序列化和反序列化工作是通过ObjectOuputStream和ObjectInputStream来完成的。ObjectOutputStream的writeObject方法可以把一个Java对象写入到流中,ObjectInputStream的readObject方法可以从流中读取一个Java对象。在写入和读取的时候,虽然用的参数或返回值是单个对象,但实际上操纵的是一个对象图,包括该对象所引用的其它对象,以及这些对象所引用的另外的对象。Java会自动帮你遍历对象图并逐个序列化。除了对象之外,Java中的基本类型和数组也是可以通过ObjectOutputStream和ObjectInputStream来序列化的。
try { User user = new User("Alex", "Cheng"); ObjectOutputStream output = new ObjectOutputStream(new FileOutputStream("user.bin")); output.writeObject(user); output.close(); } catch (IOException e) { e.printStackTrace(); } try { ObjectInputStream input = new ObjectInputStream(new FileInputStream("user.bin")); User user = (User) input.readObject(); System.out.println(user); } catch (Exception e) { e.printStackTrace(); }
上面的代码给出了典型的把Java对象序列化之后保存到磁盘上,以及从磁盘上读取的基本方式。 User类只是声明了实现Serializable接口。
在默认的序列化实现中,Java对象中的非静态和非瞬时域都会被包括进来,而与域的可见性声明没有关系。这可能会导致某些不应该出现的域被包含在序列化之后的字节数组中,比如密码等隐私信息。由于Java对象序列化之后的格式是固定的,其它人可以很容易的从中分析出其中的各种信息。对于这种情况,一种解决办法是把域声明为瞬时的,即使用transient关键词。另外一种做法是添加一个serialPersistentFields?
域来声明序列化时要包含的域。从这里可以看到在Java序列化机制中的这种仅在书面层次上定义的契约。声明序列化的域必须使用固定的名称和类型。在后面还可以看到其它类似这样的契约。虽然Serializable只是一个标记接口,但它其实是包含有不少隐含的要求。下面的代码给出了 serialPersistentFields的声明示例,即只有firstName这个域是要被序列化的。
private static final ObjectStreamField[] serialPersistentFields = { new ObjectStreamField("firstName", String.class) };
自定义对象序列化
基本的对象序列化机制让开发人员可以在包含哪些域上进行定制。如果想对序列化的过程进行更加细粒度的控制,就需要在类中添加writeObject和对应的 readObject方法。这两个方法属于前面提到的序列化机制的隐含契约的一部分。在通过ObjectOutputStream的 writeObject方法写入对象的时候,如果这个对象的类中定义了writeObject方法,就会调用该方法,并把当前 ObjectOutputStream对象作为参数传递进去。writeObject方法中一般会包含自定义的序列化逻辑,比如在写入之前修改域的值,或是写入额外的数据等。对于writeObject中添加的逻辑,在对应的readObject中都需要反转过来,与之对应。在添加自己的逻辑之前,推荐的做法是先调用Java的默认实现。在writeObject方法中通过ObjectOutputStream的defaultWriteObject来完成,在readObject方法则通过ObjectInputStream的defaultReadObject来实现。
假设 Person 类中的敏感数据是 age 字段。毕竟,女士忌谈年龄。 我们可以在序列化之前模糊化该数据,将数位循环左移一位,然后在反序列化之后复位。(您可以开发更安全的算法,当前这个算法只是作为一个例子。)
为了 “hook” 序列化过程,我们将在 Person 上实现一个 writeObject 方法;为了
“hook” 反序列化过程,我们将在同一个类上实现一个 readObject 方法。重要的是这两个方法的细节要正确 — 如果访问修改方法、参数或名称不同,那么代码将不被察觉地失败,Person 的
age 将暴露。
public class Person implements java.io.Serializable { public Person(String fn, String ln, int a) { this.firstName = fn; this.lastName = ln; this.age = a; } private void writeObject(java.io.ObjectOutputStream stream) throws java.io.IOException { // "Encrypt"/obscure the sensitive data age = age << 2; stream.defaultWriteObject(); } private void readObject(java.io.ObjectInputStream stream) throws java.io.IOException, ClassNotFoundException { stream.defaultReadObject(); // "Decrypt"/de-obscure the sensitive data age = age << 2; } private String firstName; private String lastName; private int age; }
序列化时的对象替换
在有些情况下,可能会希望在序列化的时候使用另外一个对象来代替当前对象。其中的动机可能是当前对象中包含了一些不希望被序列化的域,比如这些域都是从另外一个域派生而来的;也可能是希望隐藏实际的类层次结构;还有可能是添加自定义的对象管理逻辑,如保证某个类在JVM中只有一个实例。相对于把无关的域都设成transient来说,使用对象替换是一个更好的选择,提供了更多的灵活性。替换对象的作用类似于Java EE中会使用到的传输对象(TransferObject)。
demo1:
一个订单系统中需要把订单的相关信息序列化之后,通过网络来传输。订单类Order引用了客户类Customer。在默认序列化的情况下,Order类对象被序列化的时候,其引用的Customer类对象也会被序列化,这可能会造成用户信息的泄露。对于这种情况,可以创建一个另外的对象来在序列化的时候替换当前的Order类的对象,并把用户信息隐藏起来。
private static class OrderReplace implements Serializable { private static final long serialVersionUID = 4654546423735192613L; private String orderId; public OrderReplace(Order order) { this.orderId = order.getId(); } private Object readResolve() throws ObjectStreamException { //根据orderId查找Order对象并返回 } }这个替换对象类OrderReplace只保存了Order的ID。在Order类的writeReplace方法中返回了一个OrderReplace对象。这个对象会被作为替代写入到流中。同样的,需要在OrderReplace类中定义一个readResolve方法,用来在读取的时候再转换回 Order类对象。这样对调用者来说,替换对象的存在就是透明的。
private Object writeReplace() throws ObjectStreamException { return new OrderReplace(this); }
demo2:打包与解包代理
writeReplace和
readResolve方法使
Person类可以将它的所有数据(或其中的核心数据)打包到一个
PersonProxy中,将它放入到一个流中,然后在反序列化时再进行解包。
public class Person implements java.io.Serializable { public Person(String fn, String ln, int a) { this.firstName = fn; this.lastName = ln; this.age = a; } private Object writeReplace() throws java.io.ObjectStreamException { return new PersonProxy(this); } public String firstName; public String lastName; public int age; public Person spouse; public static void main(String[] args) throws Exception{ ObjectOutputStream oos = new ObjectOutputStream( new FileOutputStream("person.txt")); Person p = new Person("fn", "ln", 20); p.spouse = p; oos.writeObject(p); ObjectInputStream ois = new ObjectInputStream( new FileInputStream("person.txt")); Person p2 = (Person) ois.readObject(); System.out.println(p2.firstName); } } class PersonProxy implements java.io.Serializable { public String data; public PersonProxy(Person orig) { data = orig.firstName + "," + orig.lastName + "," + orig.age; if (orig.spouse != null) { data = data + "," + orig.spouse.firstName + "," + orig.spouse.lastName + "," + orig.spouse.age; } } private Object readResolve() throws java.io.ObjectStreamException { String[] pieces = data.split(","); Person result = new Person(pieces[0], pieces[1], Integer.parseInt(pieces[2])); if (pieces.length > 3) { result = new Person(pieces[3], pieces[4], Integer .parseInt(pieces[5])); result.spouse = result; } return result; } }
序列化与对象创建
在通过ObjectInputStream的readObject方法读取到一个对象之后,这个对象是一个新的实例,但是其构造方法是没有被调用的,其中的域的初始化代码也没有被执行。对于那些没有被序列化的域,在新创建出来的对象中的值都是默认的。也就是说,这个对象从某种角度上来说是不完备的。这有可能会造成一些隐含的错误。调用者并不知道对象是通过一般的new操作符来创建的,还是通过反序列化所得到的。解决的办法就是在类的readObject方法里面,再执行所需的对象初始化逻辑。对于一般的Java类来说,构造方法中包含了初始化的逻辑。可以把这些逻辑提取到一个方法中,在readObject方法中调用此方法。
版本更新
把一个Java对象序列化之后,所得到的字节数组一般会保存在磁盘或数据库之中。在保存完成之后,有可能原来的Java类有了更新,比如添加了额外的域。这个时候从兼容性的角度出发,要求仍然能够读取旧版本的序列化数据。在读取的过程中,当ObjectInputStream发现一个对象的定义的时候,会尝试在当前JVM中查找其Java类定义。这个查找过程不能仅根据Java类的全名来判断,因为当前JVM中可能存在名称相同,但是含义完全不同的Java 类。这个对应关系是通过一个全局惟一标识符serialVersionUID来实现的。通过在实现了Serializable接口的类中定义该域,就声明了该Java类的一个惟一的序列化版本号。JVM会比对从字节数组中得出的类的版本号,与JVM中查找到的类的版本号是否一致,来决定两个类是否是兼容的。对于开发人员来说,需要记得的就是在实现了Serializable接口的类中定义这样的一个域,并在版本更新过程中保持该值不变。当然,如果不希望维持这种向后兼容性,换一个版本号即可。该域的值一般是综合Java类的各个特性而计算出来的一个哈希值,可以通过Java提供的serialver命令来生成。在Eclipse中,如果Java类实现了Serializable接口,Eclipse会提示并帮你生成这个serialVersionUID。在类版本更新的过程中,某些操作会破坏向后兼容性。如果希望维持这种向后兼容性,就需要格外的注意。一般来说,在新的版本中添加东西不会产生什么问题,而去掉一些域则是不行的。(经过测试,增加和去掉一些域不报错)
demo1:接受的对象比写入文件的对象字段多
public static void main(String[] args) throws Exception { //写入文件,对应的BaseClass2为 /*class BaseClass2 implements Serializable{ private static final long serialVersionUID = 1L; public int i; public BaseClass2(int x){ this.i = x; } }*/ // BaseClass2 baseClass2 = new BaseClass2(2); // ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("demo1.txt")); // oos.writeObject(baseClass2); // System.out.println("写入文件成功..."); // oos.close(); //读取文件,对应的BaseClass2和Aoo为 /*class BaseClass2 implements Serializable{ private static final long serialVersionUID = 1L; public int i; public Aoo2 aoo2; public BaseClass2(int x){ this.i = x; } } class Aoo2 implements Serializable{ private static final long serialVersionUID = 1L; public int a; public Aoo2(int a) { this.a = a; } }*/ ObjectInputStream ois = new ObjectInputStream(new FileInputStream("demo1.txt")); BaseClass2 baseClass2 = (BaseClass2) ois.readObject(); System.out.println(baseClass2.i); System.out.println(baseClass2.aoo2); System.out.println(baseClass2.d); ois.close(); //测试结果: // 2 // null // 0.0 } }
demo2:接受的对象比写入文件的对象字段少
public static void main(String[] args) throws Exception { //写入文件,对应的BaseClass2和Aoo为 /*class BaseClass2 implements Serializable{ private static final long serialVersionUID = 1L; public int i; public Aoo2 aoo2; public double d; public BaseClass2(int x,double d, Aoo2 aoo2){ this.i = x; this.d = d; this.aoo2 = aoo2; } }*/ // BaseClass2 baseClass2 = new BaseClass2(2,2.0,new Aoo2(22)); // ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("demo1.txt")); // oos.writeObject(baseClass2); // System.out.println("写入文件成功..."); // oos.close(); //读取文件,对应的BaseClass2 /*class BaseClass2 implements Serializable{ private static final long serialVersionUID = 1L; public int i; public BaseClass2(int x){ this.i = x; } }*/ ObjectInputStream ois = new ObjectInputStream(new FileInputStream("demo1.txt")); BaseClass2 baseClass2 = (BaseClass2) ois.readObject(); System.out.println(baseClass2.i); ois.close(); //测试结果: // 2 }小结:
1.将对象写入文件和从文件读取对象与对象的版本无关,只需要是同一个class,且serialVersion相同即可
2.新增的字段按照默认方式进行赋值,基本类型(int->0, long->0L, double->0.0, boolean->false ...),对象类型赋值为null.
序列化安全性
前面提到,Java对象序列化之后的内容格式是公开的。所以可以很容易的从中提取出各种信息。从实现的角度来说,可以从不同的层次来加强序列化的安全性。对序列化之后的流进行加密。这可以通过CipherOutputStream来实现。
实现自己的writeObject和readObject方法,在调用defaultWriteObject之前,先对要序列化的域的值进行加密处理。
使用一个SignedObject或SealedObject来封装当前对象,用SignedObject或SealedObject进行序列化。
在从流中进行反序列化的时候,可以通过ObjectInputStream的registerValidation方法添加ObjectInputValidation接口的实现,用来验证反序列化之后得到的对象是否合法。
使用SealedObject的demo:
public class MainClass { public static void main(String args[]) throws Exception { SecretKey secretKey; Cipher encrypter, decrypter; secretKey = KeyGenerator.getInstance("DES").generateKey();// des加密 encrypter = Cipher.getInstance("DES"); encrypter.init(Cipher.ENCRYPT_MODE, secretKey); decrypter = Cipher.getInstance("DES"); decrypter.init(Cipher.DECRYPT_MODE, secretKey); MyClass cust, unsealed; SealedObject sealed; cust = new MyClass(); cust.name = "Paul"; cust.password = "password"; // Seal it, storing it in a SealedObject sealed = (new SealedObject(cust, encrypter)); FileOutputStream fos = new FileOutputStream("MainClass.txt"); ObjectOutputStream oos = new ObjectOutputStream(fos); oos.writeObject(sealed); oos.close(); // Try unsealing it FileInputStream fis = new FileInputStream("MainClass.txt"); ObjectInputStream ois = new ObjectInputStream(fis); SealedObject sealed2 = (SealedObject) ois.readObject(); String algorithmName = sealed2.getAlgorithm(); System.out.println(algorithmName); unsealed = (MyClass) sealed2.getObject(decrypter); System.out.println("NAME: " + unsealed.name); System.out.println("PASSWORD: " + unsealed.password); } } class MyClass implements Serializable { public String name; public String password; }
RMI
RMI(Remote Method Invocation)是Java中的远程过程调用(Remote Procedure Call,RPC)实现,是一种分布式Java应用的实现方式。它的目的在于对开发人员屏蔽横跨不同JVM和网络连接等细节,使得分布在不同JVM上的对象像是存在于一个统一的JVM中一样,可以很方便的互相通讯。之所以在介绍对象序列化之后来介绍RMI,主要是因为对象序列化机制使得RMI非常简单。调用一个远程服务器上的方法并不是一件困难的事情。开发人员可以基于ApacheMINA或是Netty这样的框架来写自己的网络服务器,亦或是可以采用REST架构风格来编写HTTP服务。但这些解决方案中,不可回避的一个部分就是数据的编排和解排(marshal/unmarshal)。需要在Java对象和传输格式之间进行互相转换,而且这一部分逻辑是开发人员无法回避的。RMI的优势在于依靠Java序列化机制,对开发人员屏蔽了数据编排和解排的细节,要做的事情非常少。JDK
5之后,RMI通过动态代理机制去掉了早期版本中需要通过工具进行代码生成的繁琐方式,使用起来更加简单。
RMI采用的是典型的客户端-服务器端架构。首先需要定义的是服务器端的远程接口,这一步是设计好服务器端需要提供什么样的服务。对远程接口的要求很简单,只需要继承自RMI中的Remote接口即可。Remote和Serializable一样,也是标记接口。远程接口中的方法需要抛出RemoteException。定义好远程接口之后,实现该接口即可。如下面的Calculator是一个简单的远程接口。
public interface Calculator extends Remote { String calculate(String expr) throws RemoteException; }
实现了远程接口的类的实例称为远程对象。创建出远程对象之后,需要把它注册到一个注册表之中。这是为了客户端能够找到该远程对象并调用。
public class CalculatorServer implements Calculator { public String calculate(String expr) throws RemoteException { return expr; } public void start() throws RemoteException, AlreadyBoundException { Calculator stub = (Calculator) UnicastRemoteObject.exportObject(this, 0); Registry registry = LocateRegistry.getRegistry(); registry.rebind("Calculator", stub); } }
CalculatorServer是远程对象的Java类。在它的start方法中通过UnicastRemoteObject的exportObject把当前对象暴露出来,使得它可以接收来自客户端的调用请求。再通过Registry的rebind方法进行注册,使得客户端可以查找到。
客户端的实现就是首先从注册表中查找到远程接口的实现对象,再调用相应的方法即可。实际的调用虽然是在服务器端完成的,但是在客户端看来,这个接口中的方法就好像是在当前JVM中一样。这就是RMI的强大之处。
public class CalculatorClient { public void calculate(String expr) { try { Registry registry = LocateRegistry.getRegistry("localhost"); Calculator calculator = (Calculator) registry.lookup("Calculator"); String result = calculator.calculate(expr); System.out.println(result); } catch (Exception e) { e.printStackTrace(); } } }
在运行的时候,需要首先通过rmiregistry命令来启动RMI中用到的注册表服务器。
为了通过Java的序列化机制来进行传输,远程接口中的方法的参数和返回值,要么是Java的基本类型,要么是远程对象,要么是实现了 Serializable接口的Java类。当客户端通过RMI注册表找到一个远程接口的时候,所得到的其实是远程接口的一个动态代理对象。当客户端调用其中的方法的时候,方法的参数对象会在序列化之后,传输到服务器端。服务器端接收到之后,进行反序列化得到参数对象。并使用这些参数对象,在服务器端调用实际的方法。调用的返回值Java对象经过序列化之后,再发送回客户端。客户端再经过反序列化之后得到Java对象,返回给调用者。这中间的序列化过程对于使用者来说是透明的,由动态代理对象自动完成。除了序列化之外,RMI还使用了动态类加载技术。当需要进行反序列化的时候,如果该对象的类定义在当前JVM中没有找到,RMI会尝试从远端下载所需的类文件定义。可以在RMI程序启动的时候,通过JVM参数java.rmi.server.codebase来指定动态下载Java类文件的URL。
序列化存储规则
demo1:ObjectOutputStream out = new ObjectOutputStream( new FileOutputStream("result.obj")); Test test = new Test(); //试图将对象两次写入文件 out.writeObject(test); out.flush(); System.out.println(new File("result.obj").length()); out.writeObject(test); out.close(); System.out.println(new File("result.obj").length()); <span style="white-space:pre"> </span>ObjectInputStream oin = new ObjectInputStream(new FileInputStream("result.obj")); //从文件依次读出两个文件 Test t1 = (Test) oin.readObject(); Test t2 = (Test) oin.readObject(); oin.close(); //判断两个引用是否指向同一个对象 System.out.println(t1 == t2);上面代码对同一对象两次写入文件,打印出写入一次对象后的存储大小和写入两次后的存储大小,然后从文件中反序列化出两个对象,比较这两个对象是否为同一对象。一般的思维是,两次写入对象,文件大小会变为两倍的大小,反序列化时,由于从文件读取,生成了两个对象,判断相等时应该是输入
false 才对,但是最后结果输出如下图所示
原因是:Java 序列化机制为了节省磁盘空间,具有特定的存储规则,当写入文件的为同一对象时,并不会再将对象的内容进行存储,而只是再次存储一份引用,上面增加的 5 字节的存储空间就是新增引用和一些控制信息的空间。反序列化时,恢复引用关系,使得清单 3 中的 t1 和 t2 指向唯一的对象,二者相等,输出 true。该存储规则极大的节省了存储空间。
demo2:
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("result.obj")); Test test = new Test(); test.i = 1; out.writeObject(test); out.flush(); test.i = 2; out.writeObject(test); out.close(); ObjectInputStream oin = new ObjectInputStream(new FileInputStream("result.obj")); Test t1 = (Test) oin.readObject(); Test t2 = (Test) oin.readObject(); System.out.println(t1.i); System.out.println(t2.i);
上面一段代码的目的是希望将 test 对象两次保存到 result.obj 文件中,写入一次以后修改对象属性值再
次保存第二次,然后从 result.obj 中再依次读出两个对象,输出这两个对象的 i 属性值。案例代码的目的原本是希望一次性传输对象修改前后的状态。
结果两个输出的都是 1, 原因就是第一次写入对象以后,第二次再试图写的时候,虚拟机根据引用关系知道已经有一个相同对象已经写入文件,因此只保存第二次写的引用,所以读取时,都是第一次保存的对象。读者在使用一个文件多次 writeObject 需要特别注意这个问题。
java中另外一种持久化方法-外部化(externalization)
外部化和序列化是实现同一目标的两种不同方法。下面让我们分析一下序列化和外部化之间的主要区别。通过Serializable接口对对象序列化的支持是内建于核心 API 的,但是java.io.Externalizable的所有实现者必须提供读取和写出的实现。Java 已经具有了对序列化的内建支持,也就是说只要制作自己的类java.io.Serializable,Java 就会试图存储和重组你的对象。如果使用外部化,你就可以选择完全由自己完成读取和写出的工作,Java 对外部化所提供的唯一支持是接口:
voidreadExternal(ObjectInput in)
void writeExternal(ObjectOutput out)
现在如何实现readExternal() 和writeExternal() 就完全看你自己了。
序列化会自动存储必要的信息,用以反序列化被存储的实例,而外部化则只保存被存储的类的标识。当你通过java.io.Serializable接口序列化一个对象时,有关类的信息,比如它的属性和这些属性的类型,都与实例数据一起被存储起来。在选择走Externalizable这条路时,Java 只存储有关每个被存储类型的非常少的信息。
demo:
public class Test implements Externalizable{ private int i; private String name; private boolean ishardcover; public Test() { } public Test(int i, String name, boolean b) { this.i = i; this.name = name; this.ishardcover = b; } @Override public void writeExternal(ObjectOutput out) throws IOException { out.writeInt(i); out.writeObject(name); out.writeBoolean(ishardcover); } @Override public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { i = in.readInt(); name = (String) in.readObject(); ishardcover = in.readBoolean(); } public static void main(String[] args) throws FileNotFoundException, IOException, ClassNotFoundException { ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("Test.txt")); oos.writeObject(new Test(8,"name",true)); ObjectInputStream ois = new ObjectInputStream(new FileInputStream("Test.txt")); Test test = (Test) ois.readObject(); System.out.println(test.i+","+test.name+","+test.ishardcover); } }
重要!!!
1.将对象写入文件和从文件读取对象与对象的版本无关,只需要是同一个class,且serialVersion相同即可
2.新增的字段按照默认方式进行赋值,基本类型(int->0, long->0L, double->0.0, boolean->false ...),对象类型赋值为null.
3.测试RPC调用同样如此,远程端interface增加和减少字段,本地调用后不报错,只需要序列号相同即可
相关文章推荐
- [javase学习笔记]-1.4 Java程序开发之初体验--Hello World
- 学习笔记之JavaSE(1)--Java开发前奏
- java中的序列化和反序列化学习笔记
- java 学习笔记11之对象序列化
- <JavaSE学习笔记>面向对象(2):Java内存机制
- 【Java学习笔记】对象的序列化和反序列化
- Java学习笔记1. Win7下JavaSE的安装和配置系
- [零散篇]Java学习笔记---Java的对象序列化以及文件IO处理
- java之序列化学习笔记(高效java之序列化)
- 学习笔记之JavaSE(26)--JavaAPI详解1
- [Java]java对象序列化学习笔记
- Java学习笔记(84)----------关于 Java 对象序列化你不知道的 5 件事
- java对象序列化学习笔记
- java对象序列化学习笔记
- [置顶] JavaSE学习笔记_5:Java多态
- 学习笔记_JavaSE_02_Java基础语法01
- java对象序列化学习笔记[转]
- Java学习笔记——IO操作之对象序列化及反序列化
- JavaSE基础学习笔记-Java I/O系统2-File
- JAVASE学习笔记:第四章 JAVA数组