`
bingooh
  • 浏览: 52836 次
  • 性别: Icon_minigender_1
  • 来自: 佛山
社区版块
存档分类
最新评论

11.74 谨慎的使用序列化

阅读更多

Java的序列机制:

序列化 把JVM里的对象转换为字节数据,此数据可传输给其它应用程序或保存到固定存储(以IO流的方式)
反序列化 把对象序列化的字节数据转换为JVM里的对象

 

类或其继承的父类只要实现java.io.Serializable接口,其对象即可被序列化。此接口为标记接口,不包含任何方法。如果没有实现此接口,在序列化时会触发NotSerializableException

 

使用ObjectInput、ObjectOutput可以序列化对象,典型代码如下,需要注意的是输入/输出方法要互相对应:

		//序列化对象
		private void writeObject(String filePath,Object o){
			try{
				ObjectOutput out=new ObjectOutputStream(new FileOutputStream(new File(filePath)));
				out.writeObject(o);//需要与输入流的输入方法对应
				out.flush();
				out.close();
			}catch(Exception e){
				e.printStackTrace();
			}
		}

		//反序列化对象
		private Object readObject(String filePath){
			try{
				ObjectInput input=new ObjectInputStream(new FileInputStream(new File(filePath)));
				Object object=input.readObject(); //需要与输出流的输出方法对应
				input.close();
				return object;
			}catch(Exception e){
				e.printStackTrace();
				return null;
			}
		}

 

与序列化相关的方法和属性包括:

private static final long serialVersionUID = 1L; 序列号,用于验证序列化类的版本号
private void writeObject(ObjectOutputStream out) throws IOException 如果需要自定义序列化数据(即写入序列化输出流的数据),应覆盖此方法
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException 如果需要自定义反序列化数据(即从序列化输入流读取的数据)或者需要在反序列化时自行设置对象属性值,应覆盖此方法
private void readObjectNoData() throws ObjectStreamException 如果覆盖了此方法,对象的属性和输入流所包含的属性不匹配,会回调此方法,详见后面解释
private Object writeReplace() throws ObjectStreamException  如果需要使用另一个对象代替当前被序列化的对象,应该覆盖此方法
private Object readResolve() throws ObjectStreamException  如果需要从另一个对象反序列化为当前对象,应该覆盖此方法

 

默认情况下,只有非static和非transient的属性可以被序列化(因为是对象序列化,所以默认static属性不会被序列化)。在writeObject()、readObject()可以调用ObjectOutputStream.defaultWriteObject()、ObjectInputStream.defaultReadObject()实现默认的序列化。可以在writeObject()/readObject()对序列化属性进行加密/解密。

 

如果同一个对象被多次写入序列化输出流里,那么仅有第一次对象序列化的字节会被写入,后续写入的是对第一次写入字节的引用数据。

 

为了方便解释(代码可能后期补上),我们假设客户端和服务端都具有com.bingo.Hello类,两个类的数据结构完全相同。为了区分用c-Hello表示客户端的Hello类,s-Hello表示服务端的Hello类。假设服务端把s-Hello对象序列化后的字节数据发送给客户端进行反序列化。

 

如果s-Hello的serialVersionUID=1L, c-Hello的serialVersionUID=2L,那么客户端在反序列化时会抛出InvalidClassException,因此两者的序列号不匹配。如果没有显式的声明serialVersionUID,那么JVM会自动生成一个不重复的序列号。如果客户端和服务端处于不同的JVM实例,那么对同一个com.bingo.Hello类,生成的序列号是不同的,这可能会导致反序列化时失败。因此建议总是显式的声明serialVersionUID。

 

如果需要客户端和服务端使用同一个Hello类,那么服务端的s-Hello应该显式声明使用一个随机的serialVersionUID,这样客户端只能从服务端加载s-Hello.class,然后再进行反序列化。如果服务端修改了s-Hello,那么应同时修改serialVersionUID,以便客户端加载最新的s-Hello.class(即实现版本管理功能)。如果不需要使用同一个Hello类,那么建议显式的声明serialVersionUID=1L

 

假设c-Hello实现了ParentHello类(仅存在于客户端),而s-Hello没有实现此类。那么在反序列化时会调用

readObjectNoData()(个人认为是调用s-Hello的readObjectNoData(),待测试),如果不希望反序列化时改变对象的继承结构,那么应在readObjectNoData()里直接抛出InvalidObjectException

 

 writeReplace()/readResolve()可以使用另一个对象代替当前对象进行序列化/反序列化,一般如果单例类可以序列化,那么需要覆盖单例类的readResolve()返回当前单例对象,避免使用反序列化的创建的另一个单例对象。此方法的另一种用法是实现flyweight设计模式,考虑以下类:

public class State implements Serializable {
	public static final State ON=new State(0);
	public static final State OFF=new State(1);
	
	private int value;
	
	private State(int value){
		this.value=value;
	}
}

 

如果在代码里大量使用了State.ON、State.OFF,那么在反序列化时会创建大量的State对象(默认情况下反序列化将创建一个新的对象),可以使用如下代码避免此问题:

public class State implements Serializable {
	public static final State ON=new State(0);
	public static final State OFF=new State(1);
	
	private int value;
	
	private State(int value){
		this.value=value;
	}
	
	 private Object writeReplace() throws ObjectStreamException{
		 return value==ON.value?ON:OFF;//序列化同一个对象
	 }
	 
	 private Object readResolve() throws ObjectStreamException{
		 return value==ON.value?ON:OFF;//反序列化同一个对象
	 }
}

 

另一种方法是使用序列化代理类(类似flyweight设计模式),如下:

public class State implements Serializable {
	public static final State ON=new State(0);
	public static final State OFF=new State(1);
	
	private int value;
	
	private State(int value){
		this.value=value;
	}
	 
         //使用StateProxy作为序列化对象。State不需要覆盖readResolve()
	 private Object writeReplace() throws ObjectStreamException{
		 return value==ON.value?StateProxy.ON:StateProxy.OFF;
	 }
	 

	 
	 //序列化代理类
	 private static final class StateProxy implements Serializable{
		 final static StateProxy ON=new StateProxy(State.ON.value);
		 final static StateProxy OFF=new StateProxy(State.OFF.value);
		 
		 private int value;
		 
		 private StateProxy(int value){
			 this.value=value;
		 }
                 
                 //返回反序列的被代理的State对象
		 private Object readResolve() throws ObjectStreamException{
			 return value==ON.value?State.ON:State.OFF;
		 }	 
	 }
	 
}

 

以下为测试代码:

		@Test
		public void testStateSerial(){
			writeObject("./state.ser", State.OFF);
			State state=(State)readObject("./state.ser");
			
			Assert.assertSame(state, State.OFF);
		}

注:如果序列化类是类似State的简单的类,最好的方式是使用枚举类。同一个枚举量反序列化后返回同一个对象。所以也可以使用枚举类实现单例模式

 

如果子类实现了Serializable接口,但是父类没有实现。那么JVM不会序列化父类对象,但是序列化子类对象时必须调用父类的构造函数,所以此情况父类必须提供无参的构造函数。

 

Apache Commons提供了序列化工具类 ,具体请参考官方帮助文档

 

其它需要注意的地方:

1.修改可序列化类的结构,可能导致已经序列化的对象在反序列化时失败(可重新加载最新的class文件后再进行反序列化 )

 

2.反序列化可能引入安全问题

 

3.序列化增加测试工作(原因见第1条)

 

4.建议仅有值对象的类(如Date,BigInteger)提供序列化功能

 

5.接口和用于继承的父类,内部类应尽量不要实现Serializable接口

 

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics