一、串行化的概念和目的
特别地,串行化主要有三种用途:
ByteArrayOutputStream
流的方式,数据将写入内存中的字节数组中。该字节数组可以用来创建初始对象的副本,一个类,如果要使其对象可以被串行化,必须实现Serializable接口。我们定义一个类Student如下:
- import java.io.Serializable;
- public class Student implements Serializable {
- int id;// 学号
- String name;// 姓名
- int age;// 年龄
- String department; // 系别
- public Student(int id, String name, int age, String department) {
- this.id = id;
- this.name = name;
- this.age = age;
- this.department = department;
- }
- }
- import java.io.FileInputStream;
- import java.io.FileOutputStream;
- import java.io.IOException;
- import java.io.ObjectInputStream;
- import java.io.ObjectOutputStream;
- public class ObjectSer {
- public static void main(String args[]) throws IOException,
- ClassNotFoundException {
- Student stu = new Student(981036, "LiuMing", 18, "CSD");
- FileOutputStream fo = new FileOutputStream("data.ser");
- ObjectOutputStream so = new ObjectOutputStream(fo);
- try {
- so.writeObject(stu);
- so.close();
- } catch (IOException e) {
- System.out.println(e);
- }
- stu = null;
- FileInputStream fi = new FileInputStream("data.ser");
- ObjectInputStream si = new ObjectInputStream(fi);
- try {
- stu = (Student) si.readObject();
- si.close();
- } catch (IOException e)
- {
- System.out.println(e);
- }
- System.out.println("Student Info:");
- System.out.println("ID:" + stu.id);
- System.out.println("Name:" + stu.name);
- System.out.println("Age:" + stu.age);
- System.out.println("Dep:" + stu.department);
- }
- }
- 运行结果如下:
- Student Info:
- ID:981036
- Name:LiuMing
- Age:18
- Dep:CSD
在这个例子中,我们首先定义了一个类Student,实现了Serializable接口 ,然后通过对象输出流的writeObject()方法将Student对象保存到文件 data.ser中 。之后,通过对家输入流的readObjcet()方法从文件data.ser中读出保存下来的Student对象 。从运行结果可以看到,通过串行化机制,可以正确地保存和恢复对象的状态。
- //LoggingInfo.java
- import java.io.FileInputStream;
- import java.io.FileOutputStream;
- import java.io.ObjectInputStream;
- import java.io.ObjectOutputStream;
- import java.util.Date;
- public class LoggingInfo implements java.io.Serializable {
- private static final long serialVersionUID = 1L;
- private Date loggingDate = new Date();
- private String uid;
- private transient String pwd;
- LoggingInfo(String user, String password) {
- uid = user;
- pwd = password;
- }
- public String toString() {
- String password = null;
- if (pwd == null) {
- password = "NOT SET";
- } else {
- password = pwd;
- }
- return "logon info: \n " + "user: " + uid + "\n logging date : "
- + loggingDate.toString() + "\n password: " + password;
- }
- public static void main(String[] args) {
- LoggingInfo logInfo = new LoggingInfo("MIKE", "MECHANICS");
- System.out.println(logInfo.toString());
- try {
- ObjectOutputStream o = new ObjectOutputStream(new FileOutputStream(
- "logInfo.out"));
- o.writeObject(logInfo);
- o.close();
- } catch (Exception e) {// deal with exception
- }
- // To read the object back, we can write
- try {
- ObjectInputStream in = new ObjectInputStream(new FileInputStream(
- "logInfo.out"));
- LoggingInfo logInfo1 = (LoggingInfo) in.readObject();
- System.out.println(logInfo1.toString());
- } catch (Exception e) {// deal with exception
- }
- }
- }
总结:
序列化分为两大部分:序列化和反序列化。序列化是这个过程的第一部分,将数据分解成字节流,以便存储在文件中或在网络上传输。反序列化就是打开字节流并重构对象。对象序列化不仅要将基本数据类型转换成字节表示,有时还要恢复数据。恢复数据要求有恢复数据的对象实例。ObjectOutputStream中的序列化过程与字节流连接,包括对象类型和版本信息。反序列化时,JVM用头信息生成对象实例,然后将对象字节流中的数据复制到对象数据成员中
序列化的过程就是对象写入字节流和从字节流中读取对象。将对象状态转换成字节流之后,可以用java.io包中的各种字节流类将其保存到文件中,管道到另一线程中或通过网络连接将对象数据发送到另一主机。对象序列化功能非常简单、强大,在RMI、Socket、JMS、EJB都有应用。对象序列化问题在网络编程中并不是最激动人心的课题,但却相当重要,具有许多实用意义。
对象序列化可以实现分布式对象。主要应用例如:RMI要利用对象序列化运行远程主机上的服务,就像在本地机上运行对象时一样。
java对象序列化不仅保留一个对象的数据,而且递归保存对象引用的每个对象的数据。可以将整个对象层次写入字节流中,可以保存在文件中或在网络连接上传递。利用对象序列化可以进行对象的“深复制”,即复制对象本身及引用的对象本身。序列化一个对象可能得到整个对象序列。
最后列举一些经常遇到的一些真实情境,它们与 Java 序列化相关,通过分析情境出现的原因,使读者轻松牢记 Java 序列化中的一些高级认识。
序列化 ID 问题
情境:两个客户端 A 和 B 试图通过网络传递对象数据,A 端将对象 C 序列化为二进制数据再传给 B,B 反序列化得到 C。
问题:C 对象的全类路径假设为 com.inout.Test,在 A 和 B 端都有这么一个类文件,功能代码完全一致。也都实现了 Serializable 接口,但是反序列化时总是提示不成功。
解决:虚拟机是否允许反序列化,不仅取决于类路径和功能代码是否一致,一个非常重要的一点是两个类的序列化 ID 是否一致(就是 private static final long serialVersionUID = 1L)。两段相同代码中,虽然两个类的功能代码完全一致,但是序列化 ID 不同,他们无法相互序列化和反序列化。
序列化 ID 在 Eclipse 下提供了两种生成策略,一个是固定的 1L,一个是随机生成一个不重复的 long 类型数据(实际上是使用 JDK 工具生成),在这里有一个建议,如果没有特殊需求,就是用默认的 1L 就可以,这样可以确保代码一致时反序列化成功。那么随机生成的序列化 ID 有什么作用呢,有些时候,通过改变序列化 ID 可以用来限制某些用户的使用。
对敏感字段加密
情境:服务器端给客户端发送序列化对象数据,对象中有一些数据是敏感的,比如密码字符串等,希望对该密码字段在序列化时,进行加密,而客户端如果拥有解密的密钥,只有在客户端进行反序列化时,才可以对密码进行读取,这样可以一定程度保证序列化对象的数据安全。
解决:在序列化过程中,虚拟机会试图调用对象类里的 writeObject 和 readObject 方法,进行用户自定义的序列化和反序列化,如果没有这样的方法,则默认调用是 ObjectOutputStream 的 defaultWriteObject 方法以及 ObjectInputStream 的 defaultReadObject 方法。用户自定义的 writeObject 和 readObject 方法可以允许用户控制序列化的过程,比如可以在序列化的过程中动态改变序列化的数值。基于这个原理,可以在实际应用中得到使用,用于敏感字段的加密工作,清单 3 展示了这个过程。
- 清单 3. 静态变量序列化问题代码
- private static final long serialVersionUID = 1L;
- private String password = "pass";
- public String getPassword() {
- return password;
- }
- public void setPassword(String password) {
- this.password = password;
- }
- private void writeObject(ObjectOutputStream out) {
- try {
- PutField putFields = out.putFields();
- System.out.println("原密码:" + password);
- password = "encryption";//模拟加密
- putFields.put("password", password);
- System.out.println("加密后的密码" + password);
- out.writeFields();
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- private void readObject(ObjectInputStream in) {
- try {
- GetField readFields = in.readFields();
- Object object = readFields.get("password", "");
- System.out.println("要解密的字符串:" + object.toString());
- password = "pass";//模拟解密,需要获得本地的密钥
- } catch (IOException e) {
- e.printStackTrace();
- } catch (ClassNotFoundException e) {
- e.printStackTrace();
- }
- }
- public static void main(String[] args) {
- try {
- ObjectOutputStream out = new ObjectOutputStream(
- new FileOutputStream("result.obj"));
- out.writeObject(new Test());
- out.close();
- ObjectInputStream oin = new ObjectInputStream(new FileInputStream(
- "result.obj"));
- Test t = (Test) oin.readObject();
- System.out.println("解密后的字符串:" + t.getPassword());
- oin.close();
- } catch (FileNotFoundException e) {
- e.printStackTrace();
- } catch (IOException e) {
- e.printStackTrace();
- } catch (ClassNotFoundException e) {
- e.printStackTrace();
- }
- }
- 在清单 3 的 writeObject 方法中,对密码进行了加密,在 readObject 中则对 password 进行解密,只有拥有密钥的客户端,才可以正确的解析出密码,确保了数据的安全。