首页 > 代码库 > Java 序列化深入分析

Java 序列化深入分析

序列化机制介绍

??序列化是指把对象转换成有序字节流,以便在网络上传输或者保存在本地文件中。序列化后的字节流保存了Java对 象的状态以及相关的描述信息。客户端从文件中或网络上获得序列化后的对象字节流后,根据字节流中所保存的对象状态及描述信息,通过反序列化重建对象。本质上讲,序列化就是把实体对象状态按照一定的格式写入到有序字节流,反序列化就是从有序字节流重建对象,恢复对象状态。序列化机制的核心作用就是对象状态的 保存与重建。

Java序列化机制解析

??Java API提供了对序列化的支持,要实现对象的序列化和反序列化,基本上包括两个步骤:

1.声明对象具有可序列化的能力
2.通过Java API实现具体的序列化处理

??在Java语言中,声明对象具有可序列化的能力主要有两种方式:

其一,实现Serializable接口;
其二,实现Externalizable接口。

两者既有区别又有联系。

Serializable接口

??Serializable接口在JDK源码中定义如下:

/*
 * @author  unascribed
 * @see java.io.ObjectOutputStream
 * @see java.io.ObjectInputStream
 * @see java.io.ObjectOutput
 * @see java.io.ObjectInput
 * @see java.io.Externalizable
 * @since   JDK1.1
 */
public interface Serializable {
}

??Java从JDK1.1开始支持对象的序列化机制,有上图的源码可知,Serializable接口没有声明任何方法,实现该接口的Java类不需要对任何方法提供实现(默认情况下,定制序列化时除外),**因此,该接口仅仅是一个mark interface(标记接口)”,实现该接口意味着告知JVM该对象可以序列化。**Java序列化机制要求所有具备序列化的对象必须实现该接口,否则是不能被序列化的,如果对于没有实现该接口的对象进行序列化时,Java API会抛出异常,无法进行序列化。

transient关键字

??瞬时变量,指的是被transient关键字修饰的变量,该关键字表示为瞬时的,即不做持久化处理的,以此来控制属性是否被包含进入序列化的字节流中。

??Serializable接 口提供了默认的序列化行为,在默认情况下,开发人员只需实现该接口,无需进行其他额外的操作,即可实现的对象的序列化。

默认只对对象中非静态的字段以及非瞬时的字段进行序列化,其他的字段是不允许被序列化的。

??这是因为,静态变量是类变量,属于整个类,并非专属于每个对象实例,因此,不序列化静态变量时合理的。瞬时变量,指的是被transient关键字修饰的变量,该关键字表示为瞬时的,即不做持久化处理的,以此来控制属性是否被包含进入序列化的字节流中。因此,在序列化时,排除transient关键字修饰的属性也是合理的。

??这种情况的具体表现就是,在序列化的有序字节流中没有保存不能被序列化的字段的状态,因此,在反序列化时,这些字段状态是不能被重建的。但是有一点需要注 意的是,经过反序列化后的对象,除了对可被序列化的字段状态进行重建之外,其他的没有被序列化的字段作为对象属性的一部分,也在对象重建时得以初始化。但 是这些字段的状态是不被保存的,重建后的这些属性仅仅是系统赋予的默认值,而非保存了对象序列化之前的状态。

实现Serializable接口除了提供默认的序列化方式之外,同样允许开发人员定制序列化,即通过实现以下相同签名的方法来实现:

序列化方法:

private void writeObject(java.io.ObjectOutputStream out) throws IOException

反序列化方法:

private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException;

其中,writeObject方法用于定制序列化,readObject方法用于实现定制反序列化。

serialVersionUID

??每个实现Serializable接口的对象都有一个serialVersionUID,长整型,64位,唯一标示了该序列化对象。在类定义中,可以显示的定义该静态变量,也可以不定义。在不定义的情况下,Java编译器会隐式的生成该变量。强烈建议显示定义。那么,该变量有什么用途呢?反序列化兼容控制,serialVersionUID相同才能进行反序列化。例如:远程主机需要反序列化对象C,如果在本地和远程主机内的C对象持有的serialVersionUID不同,即使两个类其它部分完全一样,也是不能成功反序列化话的,会抛出异常。因此,如果对类做了修改,为了保证版本对序列化兼容,该变量的值保持不变。从另一个角度来讲,不期望不同版本的类对序列化兼容,则改变该变量值。

序列化保存的数据

??那么,到底哪些数据被序列化到了有序字节流中呢?字节流数据包含了non-static和non-transient类型的成员数据、类名、类签名、可序列化的基类以及类中引用的其他对象。

针对于父类,有几点原则:

1.     如果基类实现了Serializable接口,则其子类默认的是可序列化的,不必显示声明;
2.     如果基类没有实现Serializable接口,在反序列化时,会调用基类的无参构造方法来重建基类对象,只不过不会保留基类对象状态。

利用Serializable 实现序列化的实例。

public class User implements Serializable {

    private String userName;
    private String passWord;

    public User(String userName, String passWord) {
        this.userName = userName;
        this.passWord = passWord;
    }

    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }

    public String getPassWord() {
        return passWord;
    }

    public void setPassWord(String passWord) {
        this.passWord = passWord;
    }

    @Override
    public String toString() {
        return "User{" +
                "userName=‘" + userName + ‘\‘‘ +
                ", passWord=‘" + passWord + ‘\‘‘ +
                ‘}‘;
    }
}

测试代码

import java.io.*;

/**
 * @author bridge
 */
public class Client {

    public static void main(String[] args) {
        //构造一个可以序列化对象
        User user = new User("Bridge", "123456");

        //执行序列化
        try {
            ObjectOutputStream out = new ObjectOutputStream(
                    new FileOutputStream("User.dat"));
            out.writeObject(user);
        } catch (IOException e) {
            e.printStackTrace();
        }
        //反序列化
        try {
            ObjectInputStream in = new ObjectInputStream(
                    new FileInputStream("User.dat"));

            User userRecover = (User) in.readObject();
            System.out.println(userRecover);

        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

运行结果

User{userName=‘Bridge‘, passWord=‘123456‘}

Externalizable接口

Externalizable接口继承成自Serializable接口,实现该接口意味着对象本身完全掌控自己的序列化方式。该接口JDK源码如下:

    public interface Externalizable extends java.io.Serializable {

        void writeExternal(ObjectOutput out) throws IOException;

        void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
    }

Externalizable接口定义了两个方法:writeExternal(ObjectOutput out)和readExternal(ObjectInput in)。write方法用于实现定制序列化,read方法用于实现定制反序列化,

定制序列化

??大部分情况下,我们期望对类似于密码等这样的敏感信息进行加密处理,以密文的形式在网 络间传输,增强数据的安全性。但是,通过我们上述的方式进行序列化,默认的处理方式是不能保证密码的密文传输的。因此,针对此类问题,我们必须能够定制对 象的序列化和反序列化过程,只有这样才能将我们的业务逻辑加入其中,以满足实际应用的需要。

基于Serializable接口的定制

??定制序列化和反序列化与上述序列化方式的不同在于:自定义类的实现。
首先,同样,自定义的类要实现Serializable接口,这是序列化处理的前提。不同的是,在定制序列化时,需要根据我们的实际需要,重写writeObject和readObject方法,完成序列化和反序列化的定制。示例代码如下:

User.java

import sun.misc.BASE64Decoder;
import sun.misc.BASE64Encoder;

import java.io.*;

/**
 * @author bridge
 */
public class User implements Serializable {

    private String userName;
    private String passWord;

    public User(String userName, String passWord) {
        this.userName = userName;
        this.passWord = passWord;
    }


    private void writeObject(ObjectOutputStream out) throws IOException {
        ObjectOutputStream.PutField putField = out.putFields();
        System.out.println("原密码为" + passWord);
        passWord = getBase64(passWord);//模拟加密
        putField.put("passWord", passWord);
        putField.put("userName", userName);
        out.writeFields();
        System.out.println("正在进行序列持久化");
    }

    private void readObject(ObjectInputStream in) throws IOException,
            ClassNotFoundException {
        ObjectInputStream.GetField getField = in.readFields();
        userName = (String) getField.get("userName", "");
        passWord = (String) getField.get("passWord", "");
        passWord = getFromBase64(passWord);//解密
        System.out.println("读取持久化对象");
    }


    // 加密
    public static String getBase64(String str) {
        byte[] b = null;
        String s = null;
        try {
            b = str.getBytes("utf-8");
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        if (b != null) {
            s = new BASE64Encoder().encode(b);
        }
        return s;
    }

    // 解密
    public static String getFromBase64(String s) {
        byte[] b = null;
        String result = null;
        if (s != null) {
            BASE64Decoder decoder = new BASE64Decoder();
            try {
                b = decoder.decodeBuffer(s);
                result = new String(b, "utf-8");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return result;
    }

    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }

    public String getPassWord() {
        return passWord;
    }

    public void setPassWord(String passWord) {
        this.passWord = passWord;
    }

    @Override
    public String toString() {
        return "User{" +
                "userName=‘" + userName + ‘\‘‘ +
                ", passWord=‘" + passWord + ‘\‘‘ +
                ‘}‘;
    }
}

测试类Client.java

import java.io.*;

/**
 * @author bridge
 */
public class Client {

    public static void main(String[] args) {
        //构造一个可以序列化对象
        User user = new User("Bridge", "123456");

        //执行序列化
        try {
            ObjectOutputStream out = new ObjectOutputStream(
                    new FileOutputStream("User.dat"));
            out.writeObject(user);
        } catch (IOException e) {
            e.printStackTrace();
        }
        //反序列化
        try {
            ObjectInputStream in = new ObjectInputStream(
                    new FileInputStream("User.dat"));

            User userRecover = (User) in.readObject();
            System.out.println(userRecover);

        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

运行结果

原密码为123456
正在进行序列持久化
读取持久化对象
User{userName=‘Bridge‘, passWord=‘123456‘}

说明:定制序列化过程中,序列化和反序列化读取信息的顺序要保持一致,否则会出现意想不到的后果。

基于Externalizable接口的定制

实现Extenalizable接口的类将完全由自己控制自身的序列化和反序列化。示例代码如下:

import sun.misc.BASE64Decoder;
import sun.misc.BASE64Encoder;

import java.io.*;

/**
 * @author bridge
 */
public class User1 implements Externalizable {

    private String userName;
    private String passWord;

    /**
     * 必须定义无参构造函数
     */
    public User1() {

    }

    public User1(String userName, String passWord) {
        this.userName = userName;
        this.passWord = passWord;
    }


    @Override
    public void writeExternal(ObjectOutput out) throws IOException {

        System.out.println("自定义序列化过程");
        out.writeObject(userName);
        out.writeObject(getBase64(passWord));
    }


    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        System.out.println("自定义反序列化过程");
        userName = (String) in.readObject();
        passWord = getFromBase64((String) in.readObject());
    }


    // 加密
    public static String getBase64(String str) {
        byte[] b = null;
        String s = null;
        try {
            b = str.getBytes("utf-8");
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        if (b != null) {
            s = new BASE64Encoder().encode(b);
        }
        return s;
    }

    // 解密
    public static String getFromBase64(String s) {
        byte[] b = null;
        String result = null;
        if (s != null) {
            BASE64Decoder decoder = new BASE64Decoder();
            try {
                b = decoder.decodeBuffer(s);
                result = new String(b, "utf-8");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return result;
    }

    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }

    public String getPassWord() {
        return passWord;
    }

    public void setPassWord(String passWord) {
        this.passWord = passWord;
    }

    @Override
    public String toString() {
        return "User{" +
                "userName=‘" + userName + ‘\‘‘ +
                ", passWord=‘" + passWord + ‘\‘‘ +
                ‘}‘;
    }


}

测试结果

自定义序列化过程
自定义反序列化过程
User{userName=‘Bridge‘, passWord=‘123456‘}

两种定制方法的区别

??特别注意, 利用Serializable序列化对象在反序列化时,对象完全以它保存下来的二进制位为基础恢复,不存在构建器调用。而对一个 Externalizable 对象,所有普通的默认构建行为都会发生(包括在字段定义时的初始化),而且会调用 readExternal() 。所有默认的构建行为都会进行,否则很难在自己的 Externalizable 对象中产生正确的行为,因此在利用Externalizable 接口实现序列化定制时,需要提供默认的无参构造方法,否则会抛出如下异常:

java.io.InvalidClassException: no valid constructor

序列化需要注意的问题

网络传输的安全性

??对象进行序列化之后转化成有序的字节流在网络上进行传输,如果通过默认的序列化方式, 则代码都是以明文的方式进行传输。这种情况下,部分字段的安全性是不能保障的,特别是像密码这样的安全敏感的信息。因此,如果您需要对部分字段信息进行特 殊的处理,那么应当选择定制对象的序列化方式,例如对密码等敏感信息进行加密处理。

类自身封装的安全性

??对对象进行序列化时,类中所定义的被private、final等 访问控制符所修饰的字段是直接忽略这些访问控制符而直接进行序列化的,因此,原本在本地定义的想要一次控制字段的访问权限的工作都是不起作用的。对于序列 化后的有序字节流来说一切都是可见的,而且是可重建的。这在一定程度上削弱了字段的安全性。因此,如果您需要特别处理这些信息,您可以选择相应的方式对这 些属性进行加密或者其他可行的处理,以尽量保持数据的安全性。

<script type="text/javascript"> $(function () { $(‘pre.prettyprint code‘).each(function () { var lines = $(this).text().split(‘\n‘).length; var $numbering = $(‘
    ‘).addClass(‘pre-numbering‘).hide(); $(this).addClass(‘has-numbering‘).parent().append($numbering); for (i = 1; i <= lines; i++) { $numbering.append($(‘
  • ‘).text(i)); }; $numbering.fadeIn(1700); }); }); </script>

    Java 序列化深入分析