Java序列化深入

概念:一种将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 需要特别注意这个问题。


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值