Java 序列化的高级认识
2011-09-25 20:00
260 查看
Java序列化的高级认识
引言将Java对象序列化为二进制文件的Java序列化技术是Java系列技术中一个较为重要的技术点,在大部分情况下,开发人员只需要了解被序列化的类需要实现Serializable接口,使用ObjectInputStream和ObjectOutputStream进行对象的读写。然而在有些情况下,光知道这些还远远不够,文章列举了笔者遇到的一些真实情境,它们与Java序列化相关,通过分析情境出现的原因,使读者轻松牢记
Java序列化中的一些高级认识。
本文将逐一的介绍几个情境,顺序如下面的列表。
序列化ID的问题
静态变量序列化
父类的序列化与Transient关键字
对敏感字段加密
序列化存储规则
列表的每一部分讲述了一个单独的情境,读者可以分别查看。
序列化ID问题
情境:两个客户端A和B试图通过网络传递对象数据,A端将对象C序列化为二进制数据再传给B,B反序列化得到C。
问题:C对象的全类路径假设为com.inout.Test,在A和B端都有这么一个类文件,功能代码完全一致。也都实现了Serializable接口,但是反序列化时总是提示不成功。
解决:虚拟机是否允许反序列化,不仅取决于类路径和功能代码是否一致,一个非常重要的一点是两个类的序列化ID是否一致(就是privatestaticfinallongserialVersionUID=1L)。清单1中,虽然两个类的功能代码完全一致,但是序列化ID不同,他们无法相互序列化和反序列化。
清单1.相同功能代码不同序列化ID的类对比
packagecom.inout; importjava.io.Serializable; publicclassAimplementsSerializable{ privatestaticfinallongserialVersionUID=1L; privateStringname; publicStringgetName() { returnname; } publicvoidsetName(Stringname) { this.name=name; } } packagecom.inout; importjava.io.Serializable; publicclassAimplementsSerializable{ privatestaticfinallongserialVersionUID=2L; privateStringname; publicStringgetName() { returnname; } publicvoidsetName(Stringname) { this.name=name; } }
序列化ID在Eclipse下提供了两种生成策略,一个是固定的1L,一个是随机生成一个不重复的long类型数据(实际上是使用JDK工具生成),在这里有一个建议,如果没有特殊需求,就是用默认的1L就可以,这样可以确保代码一致时反序列化成功。那么随机生成的序列化ID有什么作用呢,有些时候,通过改变序列化ID可以用来限制某些用户的使用。
特性使用案例
读者应该听过Façade模式,它是为应用程序提供统一的访问接口,案例程序中的Client客户端使用了该模式,案例程序结构图如图1所示。
图1.案例程序结构
Client端通过FaçadeObject才可以与业务逻辑对象进行交互。而客户端的FaçadeObject不能直接由Client生成,而是需要Server端生成,然后序列化后通过网络将二进制对象数据传给Client,Client负责反序列化得到Façade对象。该模式可以使得Client端程序的使用需要服务器端的许可,同时Client端和服务器端的FaçadeObject类需要保持一致。当服务器端想要进行版本更新时,只要将服务器端的
FaçadeObject类的序列化ID再次生成,当Client端反序列化FaçadeObject就会失败,也就是强制Client端从服务器端获取最新程序。
静态变量序列化
情境:查看清单2的代码。
清单2.静态变量序列化问题代码
publicclassTestimplementsSerializable{ privatestaticfinallongserialVersionUID=1L; publicstaticintstaticVar=5; publicstaticvoidmain(String[]args){ try{ //初始时staticVar为5 ObjectOutputStreamout=newObjectOutputStream( newFileOutputStream("result.obj")); out.writeObject(newTest()); out.close(); //序列化后修改为10 Test.staticVar=10; ObjectInputStreamoin=newObjectInputStream(newFileInputStream( "result.obj")); Testt=(Test)oin.readObject(); oin.close(); //再读取,通过t.staticVar打印新的值 System.out.println(t.staticVar); }catch(FileNotFoundExceptione){ e.printStackTrace(); }catch(IOExceptione){ e.printStackTrace(); }catch(ClassNotFoundExceptione){ e.printStackTrace(); } } }
最后的输出是10,对于无法理解的读者认为,打印的staticVar是从读取的对象里获得的,应该是保存时的状态才对。之所以打印10的原因在于序列化时,并不保存静态变量,这其实比较容易理解,序列化保存的是对象的状态,静态变量属于类的状态,因此序列化并不保存静态变量。
父类的序列化与Transient关键字
情境:一个子类实现了Serializable接口,它的父类都没有实现Serializable接口,序列化该子类对象,然后反序列化后输出父类定义的某变量的数值,该变量数值与序列化时的数值不同。
解决:要想将父类对象也序列化,就需要让父类也实现Serializable接口。如果父类不实现的话的,就需要有默认的无参的构造函数。在父类没有实现Serializable接口时,虚拟机是不会序列化父对象的,而一个Java对象的构造必须先有父对象,才有子对象,反序列化也不例外。所以反序列化时,为了构造父对象,只能调用父类的无参构造函数作为默认的父对象。因此当我们取父对象的变量值时,它的值是调用父类无参构造函数后的值。如果你考虑到这种序列化的情况,在父类无参构造函数中对变量进行初始化,否则的话,父类变量值都是默认声明的值,如
int型的默认是0,string型的默认是null。
Transient关键字的作用是控制变量的序列化,在变量声明前加上该关键字,可以阻止该变量被序列化到文件中,在被反序列化后,transient变量的值被设为初始值,如int型的是0,对象型的是null。
特性使用案例
我们熟悉使用Transient关键字可以使得字段不被序列化,那么还有别的方法吗?根据父类对象序列化的规则,我们可以将不需要被序列化的字段抽取出来放到父类中,子类实现Serializable接口,父类不实现,根据父类序列化规则,父类的字段数据将不被序列化,形成类图如图2所示。
图2.案例程序类图
上图中可以看出,attr1、attr2、attr3、attr5都不会被序列化,放在父类中的好处在于当有另外一个Child类时,attr1、attr2、attr3依然不会被序列化,不用重复抒写transient,代码简洁。
对敏感字段加密
情境:服务器端给客户端发送序列化对象数据,对象中有一些数据是敏感的,比如密码字符串等,希望对该密码字段在序列化时,进行加密,而客户端如果拥有解密的密钥,只有在客户端进行反序列化时,才可以对密码进行读取,这样可以一定程度保证序列化对象的数据安全。
解决:在序列化过程中,虚拟机会试图调用对象类里的writeObject和readObject方法,进行用户自定义的序列化和反序列化,如果没有这样的方法,则默认调用是ObjectOutputStream的defaultWriteObject方法以及ObjectInputStream的defaultReadObject方法。用户自定义的writeObject和readObject方法可以允许用户控制序列化的过程,比如可以在序列化的过程中动态改变序列化的数值。基于这个原理,可以在实际应用中得到使用,用于敏感字段的加密工作,清单
3展示了这个过程。
清单3.静态变量序列化问题代码
[align=left][/align]
[align=left][/align]
privatestaticfinallongserialVersionUID=1L;
privateStringpassword="pass";
publicStringgetPassword(){
returnpassword;
}
publicvoidsetPassword(Stringpassword){
this.password=password;
}
privatevoidwriteObject(ObjectOutputStreamout){
try{
PutFieldputFields=out.putFields();
System.out.println("原密码:"+password);
password="encryption";//模拟加密
putFields.put("password",password);
System.out.println("加密后的密码"+password);
out.writeFields();
}catch(IOExceptione){
e.printStackTrace();
}
}
privatevoidreadObject(ObjectInputStreamin){
try{
GetFieldreadFields=in.readFields();
Objectobject=readFields.get("password","");
System.out.println("要解密的字符串:"+object.toString());
password="pass";//模拟解密,需要获得本地的密钥
}catch(IOExceptione){
e.printStackTrace();
}catch(ClassNotFoundExceptione){
e.printStackTrace();
}
}
publicstaticvoidmain(String[]args){
try{
ObjectOutputStreamout=newObjectOutputStream(
newFileOutputStream("result.obj"));
out.writeObject(newTest());
out.close();
ObjectInputStreamoin=newObjectInputStream(newFileInputStream(
"result.obj"));
Testt=(Test)oin.readObject();
System.out.println("解密后的字符串:"+t.getPassword());
oin.close();
}catch(FileNotFoundExceptione){
e.printStackTrace();
}catch(IOExceptione){
e.printStackTrace();
}catch(ClassNotFoundExceptione){
e.printStackTrace();
}
}
在清单3的writeObject方法中,对密码进行了加密,在readObject中则对password进行解密,只有拥有密钥的客户端,才可以正确的解析出密码,确保了数据的安全。执行清单3后控制台输出如图3所示。
图3.数据加密演示
特性使用案例
RMI技术是完全基于Java序列化技术的,服务器端接口调用所需要的参数对象来至于客户端,它们通过网络相互传输。这就涉及RMI的安全传输的问题。一些敏感的字段,如用户名密码(用户登录时需要对密码进行传输),我们希望对其进行加密,这时,就可以采用本节介绍的方法在客户端对密码进行加密,服务器端进行解密,确保数据传输的安全性。
序列化存储规则
情境:问题代码如清单4所示。
清单4.存储规则问题代码
ObjectOutputStreamout=newObjectOutputStream(
newFileOutputStream("result.obj"));
Testtest=newTest();
//试图将对象两次写入文件
out.writeObject(test);
out.flush();
System.out.println(newFile("result.obj").length());
out.writeObject(test);
out.close();
System.out.println(newFile("result.obj").length());
ObjectInputStreamoin=newObjectInputStream(newFileInputStream(
"result.obj"));
//从文件依次读出两个文件
Testt1=(Test)oin.readObject();
Testt2=(Test)oin.readObject();
oin.close();
//判断两个引用是否指向同一个对象
System.out.println(t1==t2);
清单3中对同一对象两次写入文件,打印出写入一次对象后的存储大小和写入两次后的存储大小,然后从文件中反序列化出两个对象,比较这两个对象是否为同一对象。一般的思维是,两次写入对象,文件大小会变为两倍的大小,反序列化时,由于从文件读取,生成了两个对象,判断相等时应该是输入false才对,但是最后结果输出如图4所示。
图4.示例程序输出
我们看到,第二次写入对象时文件只增加了5字节,并且两个对象是相等的,这是为什么呢?
解答:Java序列化机制为了节省磁盘空间,具有特定的存储规则,当写入文件的为同一对象时,并不会再将对象的内容进行存储,而只是再次存储一份引用,上面增加的5字节的存储空间就是新增引用和一些控制信息的空间。反序列化时,恢复引用关系,使得清单3中的t1和t2指向唯一的对象,二者相等,输出true。该存储规则极大的节省了存储空间。
特性案例分析
查看清单5的代码。
清单5.案例代码
ObjectOutputStreamout=newObjectOutputStream(newFileOutputStream("result.obj"));
Testtest=newTest();
test.i=1;
out.writeObject(test);
out.flush();
test.i=2;
out.writeObject(test);
out.close();
ObjectInputStreamoin=newObjectInputStream(newFileInputStream(
"result.obj"));
Testt1=(Test)oin.readObject();
Testt2=(Test)oin.readObject();
System.out.println(t1.i);
System.out.println(t2.i);
清单4的目的是希望将test对象两次保存到result.obj文件中,写入一次以后修改对象属性值再次保存第二次,然后从result.obj中再依次读出两个对象,输出这两个对象的i属性值。案例代码的目的原本是希望一次性传输对象修改前后的状态。
结果两个输出的都是1,原因就是第一次写入对象以后,第二次再试图写的时候,虚拟机根据引用关系知道已经有一个相同对象已经写入文件,因此只保存第二次写的引用,所以读取时,都是第一次保存的对象。读者在使用一个文件多次writeObject需要特别注意这个问题。
相关文章推荐
- Java 序列化的高级认识
- Java 序列化的高级认识
- Java 序列化的高级认识
- Java 序列化的一些高级认识
- Java序列化的高级认识
- Java 序列化的高级认识
- java序列化高级认识
- Java序列化高级认识
- Java 序列化的高级认识
- Java 序列化的高级认识
- Java 序列化的高级认识
- JAVA语言序列化和反序列化(3)-序列化高级认识
- Java 序列化的高级认识
- Java 序列化的高级认识 hadoop序列化 avro
- Java 序列化的高级认识
- Java 序列化的高级认识
- Java 序列化的高级认识
- Java 序列化的高级认识
- JAVA序列化的高级认识
- Java 序列化的高级认识