一、串行化的概念和目的 


1. 什么是Serialization?

串行化(Serialization)是计算机科学中的一个概念,它是指将对象存储到介质(如文件、内在缓冲区等)中或是以二进制方式通过网络传输。之后可以通过反串行化从这些连续的位数据重新构建一个与原始对象状态相同的对象,因此在特定情况下也可以说是得到一个副本,但并不是所有情况都这样。
 

2. 为什么要Serilzation?

特别地,串行化主要有三种用途:
1)作为一种持久化机制
    如果使用的是FileOutputStream流的方式,则数据将被自动地写入文件中,
2)作为一种复制机制
    如果使用的是ByteArrayOutputStream流的方式,数据将写入内存中的字节数组中。该字节数组可以用来创建初始对象的副本,
3)作为一种通信机制
    如果是使用套接字(Socket)流的方式,则数据自动地通过网络连接传输一另一个端点,并由这个端点上的程序来决定做什么。


二、串行化方法 
            从JDK1.1开始,Java语言提供了对象串行化机制 ,在java.io包中,接口Serialization用来作为实现对象串行化的工具 ,只有实现了Serialization的类的对象才可以被串行化。 


            Serializable接口中没有任何的方法。当一个类声明要实现Serializable接口时,只是表明该类参加串行化协议,而不需要实现任何特殊的方法。下面我们通过实例介绍如何对对象进行串行化。 


1.定义一个可串行化对象 



            一个类,如果要使其对象可以被串行化,必须实现Serializable接口。我们定义一个类Student如下:

 

[java] view plaincopy
 
  1. import java.io.Serializable;     
  2.     
  3. public class Student implements Serializable {     
  4.     
  5.     int id;// 学号     
  6.     
  7.     String name;// 姓名     
  8.     
  9.     int age;// 年龄     
  10.     
  11.     String department; // 系别     
  12.     
  13.     public Student(int id, String name, int age, String department) {     
  14.     
  15.         this.id = id;     
  16.     
  17.         this.name = name;     
  18.     
  19.         this.age = age;     
  20.     
  21.         this.department = department;     
  22.     
  23.     }     
  24.     
  25. }    
2.构造对象的输入/输出流 


            要串行化一个对象,必须与一定的对象输出/输入流联系起来,通过对象输出流将对象状态保存下来,再通过对象输入流将对象状态恢复。 


            java.io包中,提供了ObjectInputStream和ObjectOutputStream将数据流功能扩展至可读写对象 。在ObjectInputStream 中用readObject()方法可以直接读取一个对象,ObjectOutputStream中用writeObject()方法可以直接将对象保存到输出流中。

 

 

[java] view plaincopy
 
  1. import java.io.FileInputStream;     
  2. import java.io.FileOutputStream;     
  3. import java.io.IOException;     
  4. import java.io.ObjectInputStream;     
  5. import java.io.ObjectOutputStream;     
  6.     
  7. public class ObjectSer {     
  8.     
  9.     public static void main(String args[]) throws IOException,     
  10.             ClassNotFoundException {     
  11.     
  12.         Student stu = new Student(981036, "LiuMing", 18, "CSD");     
  13.     
  14.         FileOutputStream fo = new FileOutputStream("data.ser");     
  15.     
  16.         ObjectOutputStream so = new ObjectOutputStream(fo);     
  17.     
  18.         try {     
  19.     
  20.             so.writeObject(stu);     
  21.     
  22.             so.close();     
  23.     
  24.         } catch (IOException e) {     
  25.             System.out.println(e);     
  26.         }     
  27.     
  28.         stu = null;     
  29.     
  30.         FileInputStream fi = new FileInputStream("data.ser");     
  31.     
  32.         ObjectInputStream si = new ObjectInputStream(fi);     
  33.     
  34.         try {     
  35.     
  36.             stu = (Student) si.readObject();     
  37.     
  38.             si.close();     
  39.     
  40.         } catch (IOException e)     
  41.     
  42.         {     
  43.             System.out.println(e);     
  44.         }     
  45.     
  46.         System.out.println("Student Info:");     
  47.     
  48.         System.out.println("ID:" + stu.id);     
  49.     
  50.         System.out.println("Name:" + stu.name);     
  51.     
  52.         System.out.println("Age:" + stu.age);     
  53.     
  54.         System.out.println("Dep:" + stu.department);     
  55.     
  56.     }     
  57.     
  58. }    
  59. 运行结果如下:  
  60.   
  61.         Student Info:   
  62.   
  63.   ID:981036   
  64.   
  65.   Name:LiuMing   
  66.   
  67.   Age:18   
  68.   
  69.   Dep:CSD  

 在这个例子中,我们首先定义了一个类Student,实现了Serializable接口 ,然后通过对象输出流的writeObject()方法将Student对象保存到文件 data.ser中 。之后,通过对家输入流的readObjcet()方法从文件data.ser中读出保存下来的Student对象 。从运行结果可以看到,通过串行化机制,可以正确地保存和恢复对象的状态。 



三、串行化的注意事项 
1.串行化能保存的元素 

            串行化只能保存对象的非静态成员交量,不能保存任何的成员方法和静态的成员变量,而且串行化保存的只是变量的值,对于变量的任何修饰符都不能保存。 

2.transient关键字 


            对于某些类型的对象,其状态是瞬时的,这样的对象是无法保存其状态的。例如一个Thread对象或一个FileInputStream对象 ,对于这些字段,我们必须用transient关键字标明,否则编译器将报措。 


            另外 ,串行化可能涉及将对象存放到 磁盘上或在网络上发达数据,这时候就会产生安全问题。因为数据位于Java运行环境之外,不在Java安全机制的控制之中。对于这些需要保密的字段,不应保存在永久介质中 ,或者不应简单地不加处理地保存下来 ,为了保证安全性。应该在这些字段前加上transient关键字。


下面是java规范中对transient关键字的解释:   
      The   transient   marker   is   not   fully   specified   by   The   Java   Language     Specification   but   is   used   in   object   serialization   to   mark   member   variables   that   should   not   be   serialized.



 以下是transient的一个应用举例:

 

 

[java] view plaincopy
 
  1. //LoggingInfo.java  
  2. import java.io.FileInputStream;  
  3. import java.io.FileOutputStream;  
  4. import java.io.ObjectInputStream;  
  5. import java.io.ObjectOutputStream;  
  6. import java.util.Date;  
  7. public class LoggingInfo implements java.io.Serializable {  
  8. private static final long serialVersionUID = 1L;  
  9. private Date loggingDate = new Date();  
  10. private String uid;  
  11. private transient String pwd;  
  12. LoggingInfo(String user, String password) {  
  13. uid = user;  
  14. pwd = password;  
  15. }  
  16. public String toString() {  
  17. String password = null;  
  18. if (pwd == null) {  
  19. password = "NOT SET";  
  20. } else {  
  21. password = pwd;  
  22. }  
  23. return "logon info: \n   " + "user: " + uid + "\n   logging date : "  
  24. + loggingDate.toString() + "\n   password: " + password;  
  25. }  
  26. public static void main(String[] args) {  
  27. LoggingInfo logInfo = new LoggingInfo("MIKE", "MECHANICS");  
  28. System.out.println(logInfo.toString());  
  29. try {  
  30. ObjectOutputStream o = new ObjectOutputStream(new FileOutputStream(  
  31. "logInfo.out"));  
  32. o.writeObject(logInfo);  
  33. o.close();  
  34. } catch (Exception e) {// deal with exception  
  35. }  
  36. // To read the object back, we can write  
  37. try {  
  38. ObjectInputStream in = new ObjectInputStream(new FileInputStream(  
  39. "logInfo.out"));  
  40. LoggingInfo logInfo1 = (LoggingInfo) in.readObject();  
  41. System.out.println(logInfo1.toString());  
  42. } catch (Exception e) {// deal with exception  
  43. }  
  44. }  
  45. }  

总结:

 

序列化分为两大部分:序列化和反序列化。序列化是这个过程的第一部分,将数据分解成字节流,以便存储在文件中或在网络上传输。反序列化就是打开字节流并重构对象。对象序列化不仅要将基本数据类型转换成字节表示,有时还要恢复数据。恢复数据要求有恢复数据的对象实例。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 展示了这个过程。

[java] view plaincopy
 
    1. 清单 3. 静态变量序列化问题代码  
    2.   
    3. private static final long serialVersionUID = 1L;  
    4.   
    5. private String password = "pass";  
    6.   
    7. public String getPassword() {  
    8. return password;  
    9. }  
    10.   
    11. public void setPassword(String password) {  
    12. this.password = password;  
    13. }  
    14.   
    15. private void writeObject(ObjectOutputStream out) {  
    16. try {  
    17. PutField putFields = out.putFields();  
    18. System.out.println("原密码:" + password);  
    19. password = "encryption";//模拟加密  
    20. putFields.put("password", password);  
    21. System.out.println("加密后的密码" + password);  
    22. out.writeFields();  
    23. } catch (IOException e) {  
    24. e.printStackTrace();  
    25. }  
    26. }  
    27.   
    28. private void readObject(ObjectInputStream in) {  
    29. try {  
    30. GetField readFields = in.readFields();  
    31. Object object = readFields.get("password", "");  
    32. System.out.println("要解密的字符串:" + object.toString());  
    33. password = "pass";//模拟解密,需要获得本地的密钥  
    34. } catch (IOException e) {  
    35. e.printStackTrace();  
    36. } catch (ClassNotFoundException e) {  
    37. e.printStackTrace();  
    38. }  
    39.   
    40. }  
    41.   
    42. public static void main(String[] args) {  
    43. try {  
    44. ObjectOutputStream out = new ObjectOutputStream(  
    45. new FileOutputStream("result.obj"));  
    46. out.writeObject(new Test());  
    47. out.close();  
    48.   
    49. ObjectInputStream oin = new ObjectInputStream(new FileInputStream(  
    50. "result.obj"));  
    51. Test t = (Test) oin.readObject();  
    52. System.out.println("解密后的字符串:" + t.getPassword());  
    53. oin.close();  
    54. } catch (FileNotFoundException e) {  
    55. e.printStackTrace();  
    56. } catch (IOException e) {  
    57. e.printStackTrace();  
    58. } catch (ClassNotFoundException e) {  
    59. e.printStackTrace();  
    60. }  
    61. }  
    62. 在清单 3 的 writeObject 方法中,对密码进行了加密,在 readObject 中则对 password 进行解密,只有拥有密钥的客户端,才可以正确的解析出密码,确保了数据的安全。