您的位置:首页 > 编程语言 > Java开发

回首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 Serializab