概念:一种将Java对象的状态转换为字节数组,以便存储或传输的机制,以后,仍可以将字节数组转换回Java对象原有的状态。
思想:冻结,屏蔽平台的差异性。
1、我们所知道的序列化:
Serializable接口,这是一个标记接口,任何实现了该接口的对象都可以被序列化。JavaAPI中一共有14个类实现了该接口。
ObjectOutputStream用来将对象写到文件中
ObjectInputStream 用来从文件中读取对象
还需要知道的:被static和transient修饰的变量不会被存储。为什么?
static的字段可能被其他对象修改。
Transient字段只存在于内存中,不能序列化到文件中。
2、序列化类允许重构吗?
· 将新字段添加到类中
· 将字段从 static改为非 static
· 将字段从 transient改为非 transient
public class Person implements Serializable {
private static String nation = "CN";
private transient String state = "children";
private String firstName;
private String lastName;
private int age;
private Person spouse;
public static String getNation() {
return nation;
}
public static void setNation(String nation) {
Person.nation = nation;
}
public String getState() {
return state;
}
public void setState(String state) {
this.state = state;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public Person getSpouse() {
return spouse;
}
public void setSpouse(Person spouse) {
this.spouse = spouse;
}
@Override
public String toString() {
return "Person [age=" + age + ",firstName=" + firstName
+ ", lastName=" + lastName + ",spouse=" + spouse.getFirstName() + "]";
}
}
测试代码:
importjava.io.FileInputStream;
importjava.io.FileNotFoundException;
importjava.io.FileOutputStream;
importjava.io.IOException;
importjava.io.ObjectInputStream;
importjava.io.ObjectOutputStream;
importjunit.framework.Assert;
importorg.junit.BeforeClass;
import org.junit.Test;
public class TestPerson {
@BeforeClass
public static voidsetUpBeforeClass() throws Exception {
}
@Test public void objectToFile(){
Personp = new Person("Rex","Lei",21);
ObjectOutputStreamoos = null;
try {
oos= newObjectOutputStream(new FileOutputStream("d:\\object.txt"));
oos.writeObject(p);
oos.close();
}catch(FileNotFoundException e) {
e.printStackTrace();
}catch (IOException e){
e.printStackTrace();
}
}
@Test public void fileToObject(){
ObjectInputStreamois = null;
try {
ois= newObjectInputStream(new FileInputStream("d:\\object.txt"));
Personp = (Person) ois.readObject();
Assert.assertEquals("Rex",p.getFirstName());
Assert.assertEquals("Lei",p.getLastName());
Assert.assertEquals(21,p.getAge());
System.out.println(p.getState());
System.out.println(p.getNation());
ois.close();
}catch(FileNotFoundException e) {
e.printStackTrace();
}catch (IOException e){
e.printStackTrace();
}catch(ClassNotFoundException e) {
e.printStackTrace();
}
}
}
Private static final longserialVersionUID = 7371833696369476427L;你知道这个字段是干嘛的吗?
Jdk home的bin目录下的serialver工具的使用:
D:\Java\JDK\JDK1.6x\jdk1.6.0_10\bin>serialver
use: serialver [-classpathclasspath] [-show] [classname...]
D:\Java\JDK\JDK1.6x\jdk1.6.0_10\bin>javacPerson.java
D:\Java\JDK\JDK1.6x\jdk1.6.0_10\bin>serialverPerson
Person: static final long serialVersionUID =7371833696369476427L;
序列化使用一个 hash,该hash是根据给定源文件中几乎所有东西 —方法名称、字段名称、字段类型、访问修改方法等 —计算出来的,序列化将该 hash值与序列化流中的 hash值相比较。
为了使Java 运行时相信两种类型实际上是一样的,第二版和随后版本的 Person
必须与第一版有相同的序列化版本 hash(存储为private static final serialVersionUID
字段)。因此,我们需要 serialVersionUID
字段,它是通过对原始(或 V1)版本的 Person
类运行JDK serialver
命令计算出的。
一旦有了 Person
的 serialVersionUID
,不仅可以从原始对象 Person
的序列化数据创建 PersonV2
对象(当出现新字段时,新字段被设为缺省值,最常见的是“null”),还可以反过来做:即从 PersonV2
的数据通过反序列化得到 Person(serialVersionUID一致就OK)
。
3.序列化安全吗?
当通过 RMI进行远程方法调用时,通过连接发送的对象中的任何 private字段几乎都是以明文的方式出现在套接字流中,这显然容易招致哪怕最简单的安全问题。
幸运的是,序列化允许 “hook”序列化过程,并在序列化之前和反序列化之后保护(或模糊化)字段数据。可以通过在 Serializable
对象上提供一个 writeObject
方法来做到这一点。
Oos.writeObject(obj);
Obj.writeObject(this);
通过writeObject方法对敏感字段进行处理。
private voidwriteObject(java.io.ObjectOutputStream stream)
throws java.io.IOException
{
// "Encrypt"/obscure thesensitive 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; }
通过使用 writeObject
和 readObject
可以实现密码加密和签名管理。有没有更好的方式?continue…
4、序列化的数据可以被签名和密封
importjava.io.FileInputStream;
importjava.io.FileNotFoundException;
importjava.io.FileOutputStream;
importjava.io.IOException;
importjava.io.ObjectInputStream;
importjava.io.ObjectOutputStream;
importjava.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
importjavax.crypto.BadPaddingException;
importjavax.crypto.Cipher;
importjavax.crypto.IllegalBlockSizeException;
importjavax.crypto.KeyGenerator;
importjavax.crypto.NoSuchPaddingException;
importjavax.crypto.SealedObject;
importjavax.crypto.SecretKey;
importjunit.framework.Assert;
importorg.junit.BeforeClass;
import org.junit.Test;
public class TestPerson {
static SecretKey secretKey = null;
@BeforeClass
public static voidsetUpBeforeClass() throws Exception {
//生成DES加密的密钥
secretKey = KeyGenerator.getInstance("DES").generateKey();
}
@Test public void objectToFile(){
Personp = new Person("Rex","Lei",21);
ObjectOutputStreamoos = null;
try {
//得到加密器实例并初始化
Cipherencrypter = Cipher.getInstance("DES");
encrypter.init(Cipher.ENCRYPT_MODE, secretKey);
SealedObjectwrapper = new SealedObject(p,encrypter);
oos= newObjectOutputStream(new FileOutputStream("d:\\object.txt"));
oos.writeObject(wrapper);
oos.close();
}catch(FileNotFoundException e) {
e.printStackTrace();
}catch (IOException e){
e.printStackTrace();
}catch(IllegalBlockSizeException e) {
e.printStackTrace();
}catch(NoSuchAlgorithmException e) {
e.printStackTrace();
}catch(NoSuchPaddingException e) {
e.printStackTrace();
}catch(InvalidKeyException e) {
e.printStackTrace();
}
fileToObject();
}
@Test public void fileToObject(){
ObjectInputStreamois = null;
try {
ois= newObjectInputStream(new FileInputStream("d:\\object.txt"));
//得到解密器实例并初始化
Cipherdecrypter = Cipher.getInstance("DES");
decrypter.init(Cipher.DECRYPT_MODE, secretKey);
SealedObject sealedObject = (SealedObject)ois.readObject();
Person p = (Person)sealedObject.getObject(decrypter);
Assert.assertEquals("Rex",p.getFirstName());
Assert.assertEquals("Lei",p.getLastName());
Assert.assertEquals(21,p.getAge());
System.out.println(p.getState());
System.out.println(p.getNation());
ois.close();
}catch(FileNotFoundException e) {
e.printStackTrace();
}catch (IOException e){
e.printStackTrace();
}catch(ClassNotFoundException e) {
e.printStackTrace();
}catch(NoSuchAlgorithmException e) {
e.printStackTrace();
}catch(NoSuchPaddingException e) {
e.printStackTrace();
}catch(InvalidKeyException e) {
e.printStackTrace();
}catch(IllegalBlockSizeException e) {
e.printStackTrace();
}catch (BadPaddingExceptione) {
e.printStackTrace();
}
}
}
实际环境中,密钥要单独管理。
5、序列化允许代理放入流中
private Object writeReplace() throws java.io.ObjectStreamException { return new PersonProxy(this); }
class PersonProxy implements java.io.Serializable { 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(); } } public String data; 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
方法将旧类序列化成新版本。
6、序列化的数据如何验证字段
对于序列化的对象,这意味着验证字段,以确保在反序列化之后它们仍具有正确的值,“以防万一”。为此,可以实现 ObjectInputValidation
接口,并覆盖 validateObject()
方法。如果调用该方法时发现某处有错误,则抛出一个 InvalidObjectException
。
7、序列化的存储规则
@Test public void testStoreRule(){
Person p = new Person("Rex","Lei",21);
ObjectOutputStream oos = null;
try {
oos = new ObjectOutputStream(new FileOutputStream("d:\\object.txt"));
oos.writeObject(p);
oos.flush();
System.out.println(new File("D:\\object.txt").length());
oos.writeObject(p);
System.out.println(new File("D:\\object.txt").length());
oos.close();
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("d:\\object.txt"));
//从文件依次读出两个文件
Person p1 = (Person) ois.readObject();
Person p2 = (Person) ois.readObject();
ois.close();
System.out.println(p1==p2);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
输出:
98
103
true
怎么样?和预想的相同吗?
Java 序列化机制为了节省磁盘空间,具有特定的存储规则,当写入文件的为同一对象时,并不会再将对象的内容进行存储,而只是再次存储一份引用,上面增加的 5 字节的存储空间就是新增引用和一些控制信息的空间。反序列化时,恢复引用关系,使得清单 3 中的 t1 和 t2 指向唯一的对象,二者相等,输出 true。该存储规则极大的节省了存储空间。
@Test public void testStore2(){
Person p = new Person("Rex","Lei",21);
ObjectOutputStream oos = null;
try {
oos = new ObjectOutputStream(new FileOutputStream("d:\\object.txt"));
oos.writeObject(p);
oos.flush();
p.setAge(22);
oos.writeObject(p);
oos.close();
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("d:\\object.txt"));
//从文件依次读出两个文件
Person p1 = (Person) ois.readObject();
Person p2 = (Person) ois.readObject();
ois.close();
System.out.println(p1.getAge());
System.out.println(p2.getAge());
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
上面代码的目的是希望将 Person对象两次保存到 d:\\object.txt文件中,写入一次以后修改对象属性值再次保存第二次,然后从 d:\\object.txt中再依次读出两个对象,输出这两个对象的age属性值。案例代码的目的原本是希望一次性传输对象修改前后的状态。
输出:
21
21
结果两个输出的都是 21, 原因就是第一次写入对象以后,第二次再试图写的时候,虚拟机根据引用关系知道已经有一个相同对象已经写入文件,因此只保存第二次写的引用,所以读取时,都是第一次保存的对象。读者在使用一个文件多次 writeObject 需要特别注意这个问题。