回首Java——Java序列化机制(Serialization,Deserialization)
2017-11-01 18:28
441 查看
Java 序列化简介
Serialization(序列化)是JDK 1.1 中引入的一组开创性特性之一,是一种将对象以一连串的字节描述的过程,以便存储或传输的机制;反序列化deserialization是一种将这些字节重建成一个对象的过程,以后,仍可以将字节数组转换回 Java 对象原有的状态。实际上,序列化的思想是 “冻结” 对象状态,传输对象状态(写到磁盘、通过网络传输等等),然后 “解冻” 状态,重新获得可用的 Java 对象。所有这些事情的发生有点像是魔术,这要归功于 ObjectInputStream/ObjectOutputStream 类、完全保真的元数据以及程序员愿意用Serializable 标识接口标记他们的类,从而 “参与” 这个过程。
序列化的必要性(为什么要序列化)
当两个进程在进行远程通信时,彼此可以发送各种类型的数据。无论是何种类型的数据,都会以二进制序列的形式在网络上传送。发送方需要把这个对象转换为字节序列,才能在网络上传送;接收方则需要把字节序列再恢复为对象。比如:**在分布式环境中经常需要将Object从这一端网络或设备传递到另一端。
这就需要有一种可以在两端传输数据的协议。**
说的再直接点,序列化的目的就是为了跨进程传递格式化数据。
如何进行序列化
下面具体的举一个例子
/** * 要序列化的类 */ public class Person implements Serializable { /** * 此字段非常重要,它是通过对原始版本的 Person 类运行 JDK serialver命令计算出的。 */ private static final long serialVersionUID = 1L; public Person(String fn, String ln, int a) { this.firstName = fn; this.lastName = ln; this.age = a; } private String firstName; private String lastName; private int age; private Person spouse; // get/set方法...省略 } /** * 对Person对象进行S/D * */ public class PersonSerializeToDisk { private static File file = new File("person.ser"); @Test public void fosDis() { Person p1 = new Person("Ted","Neward",35); Person p2 = new Person("Charlotte","Neward",36); p1.setSpouse(p2); if(file.exists()) { file.delete(); } /** * 将对象写进文件 */ FileOutputStream fos = null; ObjectOutputStream oos = null; try { fos = new FileOutputStream(file); oos = new ObjectOutputStream(fos); oos.writeObject(p1); } catch (Exception e) { e.printStackTrace(); } finally { if(oos != null) { try { oos.close(); } catch (IOException e) { e.printStackTrace(); } } if(fos != null) { try { fos.close(); } catch (IOException e) { e.printStackTrace(); } } } } @Test public void fisDis() { /** * 从文件中读取反序列化对象 */ FileInputStream fis = null; ObjectInputStream ois = null; try { fis = new FileInputStream(file); ois = new ObjectInputStream(fis); Person desPerson = (Person)ois.readObject(); System.out.println("desPerson.getFirstName():" + desPerson.getFirstName()); assertEquals(desPerson.getFirstName(),"Ted"); System.out.println("desPerson.getSpouse().getFirstName():" + desPerson.getSpouse().getFirstName()); assertEquals(desPerson.getSpouse().getFirstName(),"Charlotte"); System.out.println("desPerson.getAge():" + desPerson.getAge()); } catch (Exception e) { e.printStackTrace(); } finally { if(ois != null) { try { ois.close(); } catch (IOException e) { e.printStackTrace(); } } if(fis != null) { try { fis.close(); } catch (IOException e) { e.printStackTrace(); } } } } }
将 Person 序列化后,很容易将对象状态写到磁盘,然后重新读出它,到现在为止,还没有看到什么新鲜的或令人兴奋的事情,但是这是一个很好的出发点。我们将使用上面的Person 对象来展示关于Java对象序列化的几件事。
Java对象序列化的几件事
1.序列化允许重构序列化允许一定数量的类变种,甚至重构之后也是如此,ObjectInputStream 仍可以很好地将其读出来。 java Object Serialization 规范可以自动管理的关键任务是: * 将新字段添加到类中 * 将字段从 static 改为非 static * 将字段从 transient 改为非 transient 取决于所需的向后兼容程度,转换字段形式(从非 static 转换为 static 或从非 transient 转换为 transient)或者删除字段需要额外的消息传递。 ---------------------- **重构序列化类** 既然已经知道序列化允许重构,来看看把新字段添加到 Person 类中.下面把性别新增加上了,然后反序列化一下
enum Gender{ MALE,FEMALE } public class Person implements Serializable { /** * 此字段非常重要,它是通过对原始版本的 Person 类运行 JDK serialver命令计算出的。 */ private static final long serialVersionUID = 1L; public Person(String fn, String ln, int a,Gender g) { this.firstName = fn; this.lastName = ln; this.age = a; this.gender = g; } private String firstName; private String lastName; private int age; private Person spouse; private Gender gender; //get/set方法 ..... @Test public void fisDis() { /** * 从文件中读取反序列化对象 */ FileInputStream fis = null; ObjectInputStream ois = null; try { fis = new FileInputStream(file); ois = new ObjectInputStream(fis); Person desPerson = (Person)ois.readObject(); System.out.println("desPerson.getFirstName():" + desPerson.getFirstName()); assertEquals(desPerson.getFirstName(),"Ted"); System.out.println("desPerson.getSpouse().getFirstName():" + desPerson.getSpouse().getFirstName()); assertEquals(desPerson.getSpouse().getFirstName(),"Charlotte"); System.out.println("desPerson.getGender():" + desPerson.getGender()); } catch (Exception e) { e.printStackTrace(); } finally { if(ois != null) { try { ois.close(); } catch (IOException e) { e.printStackTrace(); } } if(fis != null) { try { fis.close(); } catch (IOException e) { e.printStackTrace(); } } } } }
这里的性别输出结果为
null
序列化使用一个 hash,该 hash 是根据给定源文件中几乎所有东西 — 方法名称、字段名称、字段类型、访问修改方法等 — 计算出来的,序列化将该 hash 值与序列化流中的 hash 值相比较。
为了使 Java 运行时相信两种类型实际上是一样的,第二版和随后版本的 Person 必须与第一版有相同的序列化版本 hash(存储为 private static final serialVersionUID 字段)。因此,我们需要 serialVersionUID 字段,它是通过对原始(或 V1)版本的 Person 类运行 JDK serialver命令计算出的。
一旦有了 Person 的 serialVersionUID,不仅可以从原始对象 Person 的序列化数据创建 PersonV2 对象(当出现新字段时,新字段被设为缺省值,最常见的是“null”),还可以反过来做:即从 PersonV2 的数据通过反序列化得到 Person,这毫不奇怪。
2.序列化并不安全
让 Java 开发人员诧异并感到不快的是,序列化二进制格式完全编写在文档中,并且完全可逆。实际上,只需将二进制序列化流的内容转储到控制台,就足以看清类是什么样子,以及它包含什么内容。
这对于安全性有着不良影响。例如,当通过 RMI 进行远程方法调用时,通过连接发送的对象中的任何 private 字段几乎都是以明文的方式出现在套接字流中,这显然容易招致哪怕最简单的安全问题。
幸运的是,序列化允许 “hook” 序列化过程,并在序列化之前和反序列化之后保护(或模糊化)字段数据。可以通过在 Serializable 对象上提供一个 writeObject 方法来做到这一点。
模糊化序列化数据
假设 Person 类中的敏感数据是 age 字段。毕竟,女士忌谈年龄。 我们可以在序列化之前模糊化该数据,将数位循环左移一位,然后在反序列化之后复位。(您可以开发更安全的算法,当前这个算法只是作为一个例子,这是最简单的一个算法,移位操作)
**WriteObject 和ReadObject方法对于实现了Serializable 接口
的类来说是可选方法。如果实现了,那么在序列化/反序列化的时候,会调用。否则,默认的序列化/反序列化将被执行。在这两个方法里,只需要关心方法所在类本身的字段域,不需要对其父类或子类负责。在这两个方法里,我们还是需要调用ObjectOutputStream的方法,defaultWriteObject/defaultReadObject 以执行Java的默认序列化/反序列化过程。**
为了 “hook” 序列化过程,我们将在 Person 上实现一个 writeObject 方法;为了 “hook” 反序列化过程,我们将在同一个类上实现一个readObject 方法。重要的是这两个方法的细节要正确.
public class Person implements Serializable { /** * 此字段非常重要,它是通过对原始(或 V1)版本的 Person 类运行 JDK serialver命令计算出的。 */ private static final long serialVersionUID = 1L; public Person(String fn, String ln, int a) { this.firstName = fn; this.lastName = ln; this.age = a; } /** * 加密 */ private void writeObject(java.io.ObjectOutputStream stream) throws java.io.IOException { // "Encrypt"/obscure the sensitive data age = age << 2; stream.defaultWriteObject(); } /** * 解密,当不知道加密方法的时候,则不能解密 */ private void readObject(java.io.ObjectInputStream stream) throws java.io.IOException, ClassNotFoundException { stream.defaultReadObject(); // "Decrypt"/de-obscure the sensitive data age = age >> 2; } private String firstName; private String lastName; private int age; private Person spouse; // get/set方法 }
这样,只要不知道加密方法,就不能解密了。如果需要查看被模糊化的数据,总是可以查看序列化数据流/文件。而且,由于该格式被完全文档化,即使不能访问类本身,也仍可以读取序列化流中的内容。
[b]利用Transient关键字模糊关键数据[/b]
这个关键字的用途,大家应该都不陌生。它用来指定可序列化对象中,哪个变量不被序列化。如果你的对象中存放了一些敏感信息,不想让别人看到的话。那么就把存放这个敏感信息的变量声明为Transient. 如下代码例子所示,Employee类中有一个私有变量_salary,我们在序列化时,想忽略这个敏感信息,那将它定义为transient即可。
public class Person implements Serializable { /** * 此字段非常重要,它是通过对原始(或 V1)版本的 Person 类运行 JDK serialver命令计算出的。 */ private static final long serialVersionUID = 1L; private String firstName; private String lastName; private int age; private Person spouse; private transient double salary; public Person(String fn, String ln, int a) { this.firstName = fn; this.lastName = ln; this.age = a; } public Person(String fn, String ln, int a,double s) { this.firstName = fn; this.lastName = ln; this.age = a; this.salary = s; } // get/set方法 }
Console Result:
desPerson.getFirstName():Ted desPerson.getSpouse().getFirstName():Charlotte desPerson.getAge():35 desPerson.getSalary():0.0 只有salary的结果没有被序列化
3.序列化的数据可以被签名和密封
上一个技巧假设您想模糊化序列化数据,而不是对其加密或者确保它不被修改。当然,通过使用 writeObject 和 readObject 可以实现密码加密和签名管理,但其实还有更好的方式。
如果需要对整个对象进行加密和签名,最简单的是将它放在一个 javax.crypto.SealedObject 和/或 java.security.SignedObject 包装器中。两者都是可序列化的,所以将对象包装在 SealedObject 中可以围绕原对象创建一种 “包装盒”。必须有对称密钥才能解密,而且密钥必须单独管理。同样,也可以将 SignedObject 用于数据验证,并且对称密钥也必须单独管理。
你可能注意到这两个类分别存放在了不同的Java package里,虽然他们都对对象的真实性,完整性提供了保证,有人更倾向于在进行Java API设计时将他们放到一起。
结合使用这两种对象,便可以轻松地对序列化数据进行密封和签名,而不必强调关于数字签名验证或加密的细节.这里使用SealedObject进行一下实例说明,从而我们能看到使用他们可以很方便的对可序列化的对象进行加密,从而保证信息安全。
private static File file = new File("person.ser"); private static Key _key = null; @Test public void fosDis() { Person p1 = new Person("Ted","Neward",35,500.55); Person p2 = new Person("Charlotte","Neward",36); p1.setSpouse(p2); if(file.exists()) { file.delete(); } /** * 将对象写进文件 */ FileOutputStream fos = null; ObjectOutputStream oos = null; try { fos = new FileOutputStream(file); oos = new ObjectOutputStream(fos); /** * 加密 */ KeyGenerator keyGenerator = KeyGenerator.getInstance("DESede"); _key = keyGenerator.generateKey(); Cipher cipher = Cipher.getInstance("DESede"); cipher.init(Cipher.ENCRYPT_MODE, _key); SealedObject so = new SealedObject(p1,cipher); oos.writeObject(so); System.out.println("Serialized - "+ p1.toString()); } catch (Exception e) { e.printStackTrace(); } finally { if(oos != null) { try { oos.close(); } catch (IOException e) { e.printStackTrace(); } } if(fos != null) { try { fos.close(); } catch (IOException e) { e.printStackTrace(); } } } } @Test public void fisDis() { /** * 从文件中读取反序列化对象 */ FileInputStream fis = null; ObjectInputStream ois = null; try { fis = new FileInputStream(file); ois = new ObjectInputStream(fis); SealedObject so = (SealedObject)ois.readObject(); Person person = (Person)so.getObject(_key);//获得解密的key System.out.println("Deserialized - "+ person.toString()); } catch (Exception e) { e.printStackTrace(); } finally { if(ois != null) { try { ois.close(); } catch (IOException e) { e.printStackTrace(); } } if(fis != null) { try { fis.close(); } catch (IOException e) { e.printStackTrace(); } } } } public static void main(String[] args) { PersonSerializeToDisk pst = new PersonSerializeToDisk(); pst.fosDis(); pst.fisDis(); }
4.序列化允许将代理放在流中
很多情况下,类中包含一个核心数据元素,通过它可以派生或找到类中的其他字段。在此情况下,没有必要序列化整个对象。可以将字段标记为 transient,但是每当有方法访问一个字段时,类仍然必须显式地产生代码来检查它是否被初始化。
如果首要问题是序列化,那么最好指定一个 flyweight 或代理放在流中。为原始 Person 提供一个 writeReplace 方法,可以序列化不同类型的对象来代替它。类似地,如果反序列化期间发现一个 readResolve 方法,那么将调用该方法,将替代对象提供给调用者。
打包和解包代理
writeReplace 和 readResolve 方法使 Person 类可以将它的所有数据(或其中的核心数据)打包到一个 PersonProxy 中,将它放入到一个流中,然后在反序列化时再进行解包。
有的地方是这样说的: “通过实现writeReplace方法来自动返回一个替代的SealedObject对象不可行,会导致栈溢出。因为SealedObject会对传入的待加密对象进行深Copy。这个操作就是通过序列化完成的。所以,会递归成死循环。 “,这信息我目前没太懂,这一块若后续会涉及到。会补充上。
public class Person implements Serializable { /** * 此字段非常重要,它是通过对原始(或 V1)版本的 Person 类运行 JDK serialver命令计算出的。 */ private static final long serialVersionUID = 1L; public Person(String fn, String ln, int a) { this.firstName = fn; this.lastName = ln; this.age = a; } private Object writeReplace() throws java.io.ObjectStreamException { return new PersonProxy(this); } } public class PersonProxy implements Serializable { private static final long serialVersionUID = 1L; public String data; public PersonProxy(Person orig) { data = orig.getFirstName() + "," + orig.getLastName() + "," + orig.getAge(); if (orig.getSpouse() != null) { Person spouse = orig.getSpouse(); data = data + "," + spouse.getFirstName() + "," + spouse.getLastName() + "," + spouse.getAge(); } } private Object readResolve() throws java.io.ObjectStreamException { String[] pieces = data.split(","); Person result = new Person(pieces[0], pieces[1], Integer.parseInt(pieces[2])); if (pieces.length > 3) { result.setSpouse(new Person(pieces[3], pieces[4], Integer.parseInt(pieces[5]))); result.getSpouse().setSpouse(result); } return result; } }
注意,PersonProxy 必须跟踪 Person 的所有数据。这通常意味着代理需要是 Person 的一个内部类,以便能访问 private 字段。有时候,代理还需要追踪其他对象引用并手动序列化它们,例如 Person 的 spouse。
这种技巧是少数几种不需要读/写平衡的技巧之一。例如,一个类被重构成另一种类型后的版本可以提供一个 readResolve 方法,以便静默地将被序列化的对象转换成新类型。类似地,它可以采用 writeReplace 方法将旧类序列化成新版本。
5.信任,但要验证
认为序列化流中的数据总是与最初写到流中的数据一致,这没有问题。但是,正如一位美国前总统所说的,“信任,但要验证”。
对于序列化的对象,这意味着验证字段,以确保在反序列化之后它们仍具有正确的值,“以防万一”。为此,可以实现 ObjectInputValidation接口,并覆盖 validateObject() 方法。如果调用该方法时发现某处有错误,则抛出一个 InvalidObjectException。
Java 在反序列化的过程中不会对Deserialized的对象进行有效性检查。而且,一旦对象是可序列化的,那就说明对象状态对应的的字节序列可以脱离Java的安全体系存在。关键是这个序列化后的字节序列对用户是可读的,基本是明文显示。所以在反序列化时,为了安全起见,我们最好对得到的数据进行校验。这时,需要我们实现接口java.io.ObjectInputValidation,这样我们可以定义反序列化中的回调函数来进行验证工作。下面举个例子
public class Person implements Serializable, ObjectInputValidation { /** * 此字段非常重要,它是通过对原始(或 V1)版本的 Person 类运行 JDK serialver命令计算出的。 */ private static final long serialVersionUID = 1L; private String firstName; private String lastName; private int age; private Person spouse; private double salary; public Person(String fn, String ln, int a) { this.firstName = fn; this.lastName = ln; this.age = a; } public Person(String fn, String ln, int a, double s) { this.firstName = fn; this.lastName = ln; this.age = a; this.salary = s; } private void readObject(java.io.ObjectInputStream stream) throws java.io.IOException, ClassNotFoundException { stream.defaultReadObject(); stream.registerValidation(this, 0); System.out.println("Customized readObject method called."); } @Override public void validateObject() throws InvalidObjectException { System.out.println("Validation object after deserialization."); if (salary < 0) { throw new InvalidObjectException("The Deserialized object is invalid. Salary can't be negative."); } else { System.out.println("The Deserialized object is valid."); } } //get/set方法 ...... }
这样,如果正确执行,就会进行相应的检查,并是否抛出异常。
6.指定不要序列化
考虑这样一个例子,父类实现了序列化接口,但是子类不想实现,这样的情况,只需要自己手动在子类中writeObject和readObject方法中抛出异常NotSerializableException即可。
private void writeObject(java.io.ObjectOutputStream stream) throws java.io.IOException { throw new NotSerializableException("This class is not serializable"); } private void readObject(java.io.ObjectInputStream stream) throws java.io.IOException, ClassNotFoundException { throw new NotSerializableException("This class is not serializable"); }
最后
Java API在 JDK 1.1 的版本中还引入了 Externalizable 接口,通过实现该接口,用户可以通过WriteExternal和ReadExternal方法对序列化/反序列化的过程进行完全掌控,当然我们也可以对字段进行满足安全考虑的任何处理。实现该接口,灵活性增加了,但意味着用户要自己对序列化/反序列化的过程负责,增加了用户的复杂度,同时序列化/反序列化的性能问题是否会更突出,也是一个需要考虑的问题。我以前做web开发的时候,只知道经理说让所有的entity实现这个接口,一直也没明白为什么,如今回首这些知识的时候,才懂得它的重要性,同时在写这篇博客的时候也解决了我其它方面的很多疑问,比如说hadoop内部的自定义数据类型时候需要重写
readObject()和
writeObject()方法,安全问题等。当你看到这些文字的时候,若日后需要带新人讲解这些知识的时候,一定要讲全面,避免遇到我那种模糊学习模糊工作的情况。
参考:
* http://www.importnew.com/16151.html
* http://www.cnblogs.com/redcreen/articles/1955307.html
* http://blog.csdn.net/technerd/article/details/13094987
相关文章推荐
- java serialization/deserialization (序列化对象自描述)
- Java序列化(Serialization) 机制
- Java Serialization序列化机制-学习笔记
- 怎样做才能让Java 序列化机制 更安全 ? Security principles we follow to make Java Serialization safe.
- java之序列化Serialization 机制
- Java序列化机制和原理
- Java序列化的机制和原理
- Java序列化的机制和思想
- java的Serialization机制
- Java对象序列化(Object Serialization)
- javabean里序列化机制和构造函数的作用20170621
- 深入分析Java的序列化(Serialization)
- Java序列化的机制和原理,以及自定义序列化问题
- Java I/O 序列化机制
- 浅析Java序列化机制
- serialVersionUID JAVA的序列化机制
- 01_java 各种序列化机制的性能比较
- JAVA序列化机制的深入研究
- Java序列化机制和原理
- hadoop序列化机制与java序列化机制对比