关于java的对象序列化
最近做东西的时候碰到不少客户端与服务端交互,我们没有用消息中间件,所以需要传序列化的对象,在编码过程中碰到不少java对象序列化的问题,有不少心得,写下来给大家分享一下
java的对象序列化说白了就是把对象转换成字节序列流,通过socket,http或者其他媒介传播到对端,对端接收到后通过反序列化将其还原成发送端的对象实例。java的对象序列化使用相当广泛,WebSession ,EJB,RMI都是基于此机制来做的。使用序列化对象的好处不言而喻:如两个模块间需要传递数据,我们常见的有ftp,http,rcp,但这些机制大部分是传文件,如果能在这之上直接传递java对象,将对两者之间的交互带来许多便利,例如我们可以将客户端提交的表单数据封装在java类中直接传递给服务器(jsp中request的getAttribute(),setAttribute()就是采用的这种机制)
java的对象序列化很简单,如果你想要你自己的一个对象能够序列化,只需实现Serializable接口即可,如下所示:
public class YourClass implements Serializable{
}
然后就可以通过ObjectInputStream流和ObjectOutPutStream流来读写你的序列化对象了,使用也很简单,跟普通的IO流读写没什么区别,如下所示:
将序列化对象写入ObjectOutPutStream流:
OutputStream ous;
ObjectOutputStream oos;
try
{
ous = response.getOutputStream(); //outPutStream可以是任何输出流,即你可以把序列化对象写入文件,字节数组
oos = new ObjectOutputStream(ous); //socket输出流.....都可以,这里是写入servlet的输出流中
YourClass yourClassObject = new YourClass();
oos.writeObject(yourClassObject);
oos.flush();
oos.close();
ous.close();
}
catch (IOException e)
{
System.out.println("servlet输出流异常");
e.printStackTrace();
}
从ObjectInputStream流中读取java序列化对象(反序列化)
}
然后就可以通过ObjectInputStream流和ObjectOutPutStream流来读写你的序列化对象了,使用也很简单,跟普通的IO流读写没什么区别,如下所示:
将序列化对象写入ObjectOutPutStream流:
OutputStream ous;
ObjectOutputStream oos;
try
{
ous = response.getOutputStream(); //outPutStream可以是任何输出流,即你可以把序列化对象写入文件,字节数组
oos = new ObjectOutputStream(ous); //socket输出流.....都可以,这里是写入servlet的输出流中
YourClass yourClassObject = new YourClass();
oos.writeObject(yourClassObject);
oos.flush();
oos.close();
ous.close();
}
catch (IOException e)
{
System.out.println("servlet输出流异常");
e.printStackTrace();
}
从ObjectInputStream流中读取java序列化对象(反序列化)
InputStream in = con.getInputStream();
ois = new ObjectInputStream(in);
Object resultObject = ois.readObject(); //每次读一个对象,如写入多个,调用多次readObject()
catch (IOException e)
{
//"获取服务端输出流错误!
}
catch (ClassNotFoundException e)
{
//客户端找不到对应的对象
}
finally
{
if(ois != null)
{
try
{
ois.close();
}
catch (IOException e)
{
JOptionPane.showMessageDialog(this,"资源回收错误!");
System.out.println(e.getMessage());
}
}
}
ois = new ObjectInputStream(in);
Object resultObject = ois.readObject(); //每次读一个对象,如写入多个,调用多次readObject()
catch (IOException e)
{
//"获取服务端输出流错误!
}
catch (ClassNotFoundException e)
{
//客户端找不到对应的对象
}
finally
{
if(ois != null)
{
try
{
ois.close();
}
catch (IOException e)
{
JOptionPane.showMessageDialog(this,"资源回收错误!");
System.out.println(e.getMessage());
}
}
}
OK,使用起来很方便吧?但是你在序列化过程需要注意一些地方,这也是我在编码中发现的一些问题
1:发端和收端的序列化java类的包路径要一致
因为java是以包路径作为java类的命名空间的,如果发端的类是com.XXXX.ObjectA,而接收端的类是com.YYYY.ObjectA,即使ObjectA的定义完全一样,反序列化时也会认为是不同的对象而报找不到 com.XXXX.ObjectA类定义的错误(其实这点有点像废话,不是一个包类的java对象当然是不同的对象,但在实际中很容易烦此类错误,比如客户端和服务端的模块结构差别很大,很可能服务端的某个类的目录结构和客户端是不一致的,这样的话是不能成功序列化的)
2:版本问题
这个问题很经典了,如果一个类实现了序列化,那它必须承担起升版后如何保证接收端用老版本的类能成功反序列化的问题。jdk的解决方案是加入serialVersionUID字段,只要该字段值相同就认为是同一个对象。serialVersionUID可自己手动填 ,也可让IDE给你计算出一个。当该字段存在时,如果新版本的类传递到收端,收端根据该字段判断与老版本是同一对象时,它将”尽最大可能转换“,这意味着新版本的类里的新添加的方法,变量将被舍去。
当然你也可以自己定义版本问题的策略,比如你想要保证服务端和客户端部署的类版本始终保持一致,那你可以在新版本的类发布的时候,更改serialVersionUID字段值,这样在收端肯定会抛出serialVersionUID不一致异常,你可以捕获这个异常,然后提示客户端需要更新该类版本。
3:安全问题
java的反序列化过程实际上就是从字节流重新构造一个类实例的过程,因此它实际上是一种不通过构造函数的对象初始化,这就意味着如果窃取了传输中的该字节流,可以从中分析出改类的一切内部细节,包括私有变量、方法。应此如果不想在序列化时将所有类方法,成员变量都暴露的话,可以为这些方法,成员变量加上transient,该字段的意思是:”不用序列化我了,谢谢!"
或者还有一种办法,就是覆盖默认的Serializable接口的readObject(ObjectOutPutStream ous)方法和writeObject(ObjectInputStream ins)方法,在该方法中自定义序列化哪些字段,反序列化哪些字段。
如果你还觉得不太安全的话,jdk1。4以上版本加入了一个readResovle方法,在反序列化将执行该方法,你可以override该方法,而在其中加入自己的校验代码,以判断反序列化后的对象实例是否正确。
4:资源回收
有一点需要注意,ObjectOutPutStream和ObjectInputStream流在每write或者read一个对象后都会保持该对象的强引用,这些对象将一直不能被垃圾收集器回收,因此一定要记住在序列化操作结束后重置或者close ObjectOutPutStream、ObjectInputStream以使其应用的序列化对象得以被回收。在上面的反序列化中有代码实例
总之,java的对象序列化虽然很方便,但要用好不容易,特别像版本这类问题,需要仔细处理。我的观点是只要版本不一致,立即报错,不作任何”尽力转换“的努力:)
1:发端和收端的序列化java类的包路径要一致
因为java是以包路径作为java类的命名空间的,如果发端的类是com.XXXX.ObjectA,而接收端的类是com.YYYY.ObjectA,即使ObjectA的定义完全一样,反序列化时也会认为是不同的对象而报找不到 com.XXXX.ObjectA类定义的错误(其实这点有点像废话,不是一个包类的java对象当然是不同的对象,但在实际中很容易烦此类错误,比如客户端和服务端的模块结构差别很大,很可能服务端的某个类的目录结构和客户端是不一致的,这样的话是不能成功序列化的)
2:版本问题
这个问题很经典了,如果一个类实现了序列化,那它必须承担起升版后如何保证接收端用老版本的类能成功反序列化的问题。jdk的解决方案是加入serialVersionUID字段,只要该字段值相同就认为是同一个对象。serialVersionUID可自己手动填 ,也可让IDE给你计算出一个。当该字段存在时,如果新版本的类传递到收端,收端根据该字段判断与老版本是同一对象时,它将”尽最大可能转换“,这意味着新版本的类里的新添加的方法,变量将被舍去。
当然你也可以自己定义版本问题的策略,比如你想要保证服务端和客户端部署的类版本始终保持一致,那你可以在新版本的类发布的时候,更改serialVersionUID字段值,这样在收端肯定会抛出serialVersionUID不一致异常,你可以捕获这个异常,然后提示客户端需要更新该类版本。
3:安全问题
java的反序列化过程实际上就是从字节流重新构造一个类实例的过程,因此它实际上是一种不通过构造函数的对象初始化,这就意味着如果窃取了传输中的该字节流,可以从中分析出改类的一切内部细节,包括私有变量、方法。应此如果不想在序列化时将所有类方法,成员变量都暴露的话,可以为这些方法,成员变量加上transient,该字段的意思是:”不用序列化我了,谢谢!"
或者还有一种办法,就是覆盖默认的Serializable接口的readObject(ObjectOutPutStream ous)方法和writeObject(ObjectInputStream ins)方法,在该方法中自定义序列化哪些字段,反序列化哪些字段。
如果你还觉得不太安全的话,jdk1。4以上版本加入了一个readResovle方法,在反序列化将执行该方法,你可以override该方法,而在其中加入自己的校验代码,以判断反序列化后的对象实例是否正确。
4:资源回收
有一点需要注意,ObjectOutPutStream和ObjectInputStream流在每write或者read一个对象后都会保持该对象的强引用,这些对象将一直不能被垃圾收集器回收,因此一定要记住在序列化操作结束后重置或者close ObjectOutPutStream、ObjectInputStream以使其应用的序列化对象得以被回收。在上面的反序列化中有代码实例
总之,java的对象序列化虽然很方便,但要用好不容易,特别像版本这类问题,需要仔细处理。我的观点是只要版本不一致,立即报错,不作任何”尽力转换“的努力:)