【二】Java 序列化与反序列化

参考:Java 序列化的高级认识

一、概念

把对象转换为字节序列的过程称为对象的序列化

把字节序列恢复为对象的过程称为对象的反序列化

对象的序列化主要有两种用途:

1) 把对象的字节序列永久地保存到硬盘上,通常存放在一个文件中;

2) 在网络上传送对象的字节序列。

二、JDK类库中的序列化API

java.io.ObjectOutputStream代表对象输出流,它的writeObject(Object obj)方法可对参数指定的obj对象进行序列化,把得到的字节序列写到一个目标输出流中。

java.io.ObjectInputStream代表对象输入流,它的readObject()方法从一个源输入流中读取字节序列,再把它们反序列化为一个对象,并将其返回。

1.只有实现了Serializable和Externalizable接口的类的对象才能被序列化

Externalizable接口继承自 Serializable接口,实现Externalizable接口的类完全由自身来控制序列化的行为,而仅实现Serializable接口的类可以 采用默认的序列化方式 。

例子:

package com.sid.io;

import java.io.Serializable;

public class TestModel implements Serializable {
    private static final long serialVersionUID= -1L;

    private String name;
    private int age;

    public static long getSerialVersionUID() {
        return serialVersionUID;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}
package com.sid.io;

import java.io.*;

public class SerializableMain {
    public static void main(String[] args) throws Exception {
        serializable();
        deserializable();
    }

    public static void serializable() throws Exception{
        TestModel testModel = new TestModel();
        testModel.setAge(22);
        testModel.setName("sid");

        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(new File("Serializable.txt")));
        oos.writeObject(testModel);
        oos.close();
    }

    public static void deserializable() throws Exception{
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("Serializable.txt")));
        TestModel testModel =(TestModel) ois.readObject();
        System.out.println(testModel.getAge());
        System.out.println(testModel.getName());
        ois.close();
    }
}

结果

2.transient关键字修饰

不想序列化某些字段,则该字段用transient关键字修饰

比如:把上诉TestModel类的name字段用transient修饰

再跑一边SerializableMain结果:

父类的序列化与 Transient 关键字

情境:一个子类实现了 Serializable 接口,它的父类都没有实现 Serializable 接口,序列化该子类对象,然后反序列化后输出父类定义的某变量的数值,该变量数值与序列化时的数值不同。

解决要想将父类对象也序列化,就需要让父类也实现Serializable 接口。如果父类不实现的话的,就 需要有默认的无参的构造函数。在父类没有实现 Serializable 接口时,虚拟机是不会序列化父对象的,而一个 Java 对象的构造必须先有父对象,才有子对象,反序列化也不例外。所以反序列化时,为了构造父对象,只能调用父类的无参构造函数作为默认的父对象。因此当我们取父对象的变量值时,它的值是调用父类无参构造函数后的值。如果你考虑到这种序列化的情况,在父类无参构造函数中对变量进行初始化,否则的话,父类变量值都是默认声明的值,如 int 型的默认是 0,string 型的默认是 null。

Transient 关键字的作用是控制变量的序列化,在变量声明前加上该关键字,可以阻止该变量被序列化到文件中,在被反序列化后,transient 变量的值被设为初始值,如 int 型的是 0,对象型的是 null。

特性使用案例

我们熟悉使用 Transient 关键字可以使得字段不被序列化,那么还有别的方法吗?根据父类对象序列化的规则,我们可以将不需要被序列化的字段抽取出来放到父类中,子类实现 Serializable 接口,父类不实现,根据父类序列化规则,父类的字段数据将不被序列化,形成类图如图 2 所示。

图 2. 案例程序类图

图 2. 案例程序类图

上图中可以看出,attr1、attr2、attr3、attr5 都不会被序列化,放在父类中的好处在于当有另外一个 Child 类时,attr1、attr2、attr3 依然不会被序列化,不用重复抒写 transient,代码简洁。

3.serialVersionUID有什么用

注意看TestModel类里面有一个serialVersionUID

字​面​意​思​上​是​序​列​化​的​版​本​号​,凡是实现Serializable接口的类都有一个表示序列化版本标识符的静态变量

虚拟机是否允许反序列化,不仅取决于类路径和功能代码是否一致,一个非常重要的一点是两个类的序列化 ID 是否一致(就是 private static final long serialVersionUID = 1L)。虽然两个类的功能代码、package、类名完全一致,但是序列化 ID 不同,他们无法相互序列化和反序列化。

由于实现了Serializable接口如果没有显示在类中定义静态final的serialVersionUID, Java会自动给这个class进行一个摘要算法,类似于指纹算法,只要这个文件 多一个空格,得到的UID就会截然不同的。一旦SerialversionUID 跟之前不匹配,反序列化就无法成功。

所以我们需要自己定义serialVersionUID,序列化的那一方的该类用的serialVersionUID要与反序列化的那一方的该类用的serialVersionUID一样

显式地定义serialVersionUID有两种用途:

       1、 在某些场合,希望类的不同版本对序列化兼容,因此需要确保类的不同版本具有相同的serialVersionUID;
  2、 在某些场合,不希望类的不同版本对序列化兼容,因此需要确保类的不同版本具有不同的serialVersionUID。

4.静态变量序列化问题

public class Test implements Serializable {
 
    private static final long serialVersionUID = 1L;
 
    public static int staticVar = 5;
 
    public static void main(String[] args) {
        try {
            //初始时staticVar为5
            ObjectOutputStream out = new ObjectOutputStream(
                    new FileOutputStream("result.obj"));
            out.writeObject(new Test());
            out.close();
 
            //序列化后修改为10
            Test.staticVar = 10;
 
            ObjectInputStream oin = new ObjectInputStream(new FileInputStream(
                    "result.obj"));
            Test t = (Test) oin.readObject();
            oin.close();
             
            //再读取,通过t.staticVar打印新的值
            System.out.println(t.staticVar);
             
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

将对象序列化后,修改静态变量的数值,再将序列化对象读取出来,然后通过读取出来的对象获得静态变量的数值并打印出来。这个 System.out.println(t.staticVar) 语句输出的是 10 。

原因在于序列化时,并不保存静态变量,序列化保存的是对象的状态,静态变量属于类的状态,因此 序列化并不保存静态变量

 5.对敏感字段加密

情境:服务器端给客户端发送序列化对象数据,对象中有一些数据是敏感的,比如密码字符串等,希望对该密码字段在序列化时,进行加密,而客户端如果拥有解密的密钥,只有在客户端进行反序列化时,才可以对密码进行读取,这样可以一定程度保证序列化对象的数据安全。

解决:在序列化过程中,虚拟机会试图调用对象类里的 writeObject 和 readObject 方法,进行用户自定义的序列化和反序列化,如果没有这样的方法,则默认调用是 ObjectOutputStream 的 defaultWriteObject 方法以及 ObjectInputStream 的 defaultReadObject 方法。用户自定义的 writeObject 和 readObject 方法可以允许用户控制序列化的过程,比如可以在序列化的过程中动态改变序列化的数值。基于这个原理,可以在实际应用中得到使用,用于敏感字段的加密工作,清单 3 展示了这个过程。

清单 3. 静态变量序列化问题代码

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

private static final long serialVersionUID = 1L;

 

   private String password = "pass";

 

   public String getPassword() {

       return password;

   }

 

   public void setPassword(String password) {

       this.password = password;

   }

 

   private void writeObject(ObjectOutputStream out) {

       try {

           PutField putFields = out.putFields();

           System.out.println("原密码:" + password);

           password = "encryption";//模拟加密

           putFields.put("password", password);

           System.out.println("加密后的密码" + password);

           out.writeFields();

       } catch (IOException e) {

           e.printStackTrace();

       }

   }

 

   private void readObject(ObjectInputStream in) {

       try {

           GetField readFields = in.readFields();

           Object object = readFields.get("password", "");

           System.out.println("要解密的字符串:" + object.toString());

           password = "pass";//模拟解密,需要获得本地的密钥

       } catch (IOException e) {

           e.printStackTrace();

       } catch (ClassNotFoundException e) {

           e.printStackTrace();

       }

 

   }

 

   public static void main(String[] args) {

       try {

           ObjectOutputStream out = new ObjectOutputStream(

                   new FileOutputStream("result.obj"));

           out.writeObject(new Test());

           out.close();

 

           ObjectInputStream oin = new ObjectInputStream(new FileInputStream(

                   "result.obj"));

           Test t = (Test) oin.readObject();

           System.out.println("解密后的字符串:" + t.getPassword());

           oin.close();

       } catch (FileNotFoundException e) {

           e.printStackTrace();

       } catch (IOException e) {

           e.printStackTrace();

       } catch (ClassNotFoundException e) {

           e.printStackTrace();

       }

   }

在清单 3 的 writeObject 方法中,对密码进行了加密,在 readObject 中则对 password 进行解密,只有拥有密钥的客户端,才可以正确的解析出密码,确保了数据的安全。执行清单 3 后控制台输出如图 3 所示。

图 3. 数据加密演示

图 3. 数据加密演示

特性使用案例

RMI 技术是完全基于 Java 序列化技术的,服务器端接口调用所需要的参数对象来至于客户端,它们通过网络相互传输。这就涉及 RMI 的安全传输的问题。一些敏感的字段,如用户名密码(用户登录时需要对密码进行传输),我们希望对其进行加密,这时,就可以采用本节介绍的方法在客户端对密码进行加密,服务器端进行解密,确保数据传输的安全性。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章