Java 序列化

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/jeikerxiao/article/details/80320454

前言

对于Java的序列化,一直只知道只需要实现Serializbale这个接口就可以了。停留在了表面,在项目中使用Dubbo时,发现对象需要实现Serializbale,涉及到了序列化问题。就补了一下Java序列化的底层实现。

1.What

  • Java序列化 是指把Java对象保存为二进制字节码的过程;
  • Java反序列化 是指把二进制字节码重新转换成Java对象的过程。

那么为什么需要序列化呢?

  • 第一种情况是:一般情况下Java对象的声明周期都比Java虚拟机的要短,实际应用中我们希望在JVM停止运行之后能够持久化指定的对象,这时候就需要把对象进行序列化之后保存。

  • 第二种情况是:需要把Java对象通过网络进行传输的时候。因为数据只能够以二进制的形式在网络中进行传输,因此当把对象通过网络发送出去之前需要先序列化成二进制数据,在接收端读到二进制数据之后反序列化成Java对象。

2.How

演示demo

使用一个demo演示一下:

import java.io.*;

public class Student implements Serializable {

    private static final long serialVersionUID = -7067671538866851177L;

    private Integer id;
    private String name;
    private Integer age;

    public Integer getId() {
        return id;
    }
    public void setId(Integer id) {
        this.id = id;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public Integer getAge() {
        return age;
    }
    public void setAge(Integer age) {
        this.age = age;
    }

    public static void main(String[] args) throws Exception {
        // 1. 序列化:对象->文件
        FileOutputStream fileOutputStream = new FileOutputStream("student.out");
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);

        Student writeStudent = new Student();
        writeStudent.setId(1);
        writeStudent.setName("xiao");
        writeStudent.setAge(25);

        objectOutputStream.writeObject(writeStudent);
        objectOutputStream.flush();
        objectOutputStream.close();
        // 2.反序列化:文件->对象
        FileInputStream fileInputStream = new FileInputStream("student.out");
        ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
        Student readStudent = (Student) objectInputStream.readObject();

        System.out.println("sutudent id :" + readStudent.getId());
        System.out.println("sutudent name :" + readStudent.getName());
        System.out.println("sutudent age :" + readStudent.getAge());
    }
}

输出结果:

sutudent id :1
sutudent name :xiao
sutudent age :25

查看二进制文件

使用二进制编辑器(如: Synalyze It Pro Mac)查看 student.out 文件

这里写图片描述

3.Why

1.调用ObjectOutputStream.writeObject()和ObjectInputStream.readObject()之后究竟做了什么?

1. Serializable 接口

查看 Serializable 接口,可以发现这个接口没有任何方法或字段。

/*
 * @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 {
}

查看注释可以看到:

The serialization interface has no methods or fields
and serves only to identify the semantics of being serializable.

翻译:序列化接口没有方法或字段,并仅用于识别可序列化的语义。

2. ObjectOutputStream 对象输出流

根据我们的demo代码,和根据Serializable注释查看 java.io.ObjectOutputStream

ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);

在调用wroteObject()进行序列化之前会先调用ObjectOutputStream的构造函数生成一个ObjectOutputStream对象,构造函数如下:

public ObjectOutputStream(OutputStream out) throws IOException {
    verifySubclass();
    // 1.创建一个bout,表示底层的块数据输出流容器
    bout = new BlockDataOutputStream(out);

    // ...

    // 2.写入文件头
    writeStreamHeader(); 
    // 3.flush数据(提交数据)
    bout.setBlockDataMode(true);

    // ...
}

构造函数中首先会把bout对绑定到底层的字节数据容器,接着会调用writeStreamHeader()方法,该方法实现如下:

protected void writeStreamHeader() throws IOException {
    bout.writeShort(STREAM_MAGIC);
    bout.writeShort(STREAM_VERSION);
}

查看写入的字符:


/**
 * Magic number 写入 stream header.
 */
final static short STREAM_MAGIC = (short)0xaced;

/**
 * 版本号 写入 stream header.
 */
final static short STREAM_VERSION = 5;

在writeStreamHeader()方法中首先会往底层字节容器中写入表示序列化的Magic Number以及版本号。

到此时,流中已经有4个字节: AC ED (Magic number) 00 05 (版本号)。

3.writeObject()写入对象

我们的demo代码:

objectOutputStream.writeObject(writeStudent);

查看 writeObject():

public final void writeObject(Object obj) throws IOException {
    // ...

    try {
        // 正常调用 writeObject0() 方法进行序列化
        writeObject0(obj, false);
    } catch (IOException ex) {
        // ...
    }
}

查看 writeObject0() :

private void writeObject0(Object obj, boolean unshared)
    throws IOException
{
    // ...
    try {
        // ...
        Object orig = obj;
        // 获取要序列化的对象
        Class<?> cl = obj.getClass();
        ObjectStreamClass desc;
        for (;;) {
            Class<?> repCl;
            // 创建描述cl的ObjectStreamClass对象
            desc = ObjectStreamClass.lookup(cl, true);
              // ...
        }
        if (enableReplace) {
            Object rep = replaceObject(obj);
            if (rep != obj && rep != null) {
                cl = rep.getClass();
                desc = ObjectStreamClass.lookup(cl, true);
            }
            obj = rep;
        }

        // ...

        // 根据不同的类型,进行不同的写入操作
        if (obj instanceof String) {
            writeString((String) obj, unshared);
        } else if (cl.isArray()) {
            writeArray(obj, desc, unshared);
        } else if (obj instanceof Enum) {
            writeEnum((Enum<?>) obj, desc, unshared);
        } else if (obj instanceof Serializable) {
            writeOrdinaryObject(obj, desc, unshared);
        } else {
              // ...
        }
    } finally {
        depth--;
        bout.setBlockDataMode(oldMode);
    }
}

从上面代码里面可以看到,程序主要做了两件事:

  1. 生成一个描述被序列化对象的类的类元信息的ObjectStreamClass对象。
  2. 根据传入的需要序列化的对象的实际类型进行不同的序列化操作。从代码里面可以很明显的看到,对于String类型、数组类型和Enum可以直接进行序列化。如果被序列化对象实现了Serializable对象,则会调用writeOrdinaryObject()方法进行序列化。

这里解答了一个问题:

Serializbale 接口是个空的接口,并没有定义任何方法,为什么需要序列化的接口只要实现Serializbale 接口就能够进行序列化?

答案:

Serializable 接口这是一个标记,告诉程序所有实现了”Serializable”的对象都需要进行序列化。

接着查看 writeOrdinaryObject() 方法:

private void writeOrdinaryObject(Object obj,
                                 ObjectStreamClass desc,
                                 boolean unshared)
    throws IOException
{
    // ...
    try {
        desc.checkSerialize();
         // 写入TC_OBJECT = (byte)0x73; 表示新对象
        bout.writeByte(TC_OBJECT);
        // 写入类元数据
        writeClassDesc(desc, false);
        handles.assign(unshared ? null : obj);
        if (desc.isExternalizable() && !desc.isProxy()) {
            writeExternalData((Externalizable) obj);
        } else {
            // 写入被序列化的对象的实例数据
            writeSerialData(obj, desc);
        }
    } finally {
        if (extendedDebugInfo) {
            debugInfoStack.pop();
        }
    }
}

在这个方法中首先会往底层字节容器中写入TC_OBJECT,表示这是一个新的Object。

writeClassDesc()方法

接下来会调用writeClassDesc()方法写入被序列化对象的类的类元数据,writeClassDesc()方法实现如下:

private void writeClassDesc(ObjectStreamClass desc, boolean unshared)
    throws IOException
{
    int handle;
    if (desc == null) {
         // 如果desc为null
        writeNull();
    } else if (!unshared && (handle = handles.lookup(desc)) != -1) {
        writeHandle(handle);
    } else if (desc.isProxy()) {
        writeProxyDesc(desc, unshared);
    } else {
        writeNonProxyDesc(desc, unshared);
    }
}

在这个方法中会先判断传入的desc是否为null,如果为null则调用writeNull()方法.

writeNull()方法

/**
 * 写入 null code 到 stream.
 */
private void writeNull() throws IOException {
    // TC_NULL = (byte)0x70;
    // 表示对一个Object引用的描述的结束
    bout.writeByte(TC_NULL);
}

writeNonProxyDesc()方法

如果不为null,则一般情况下接下来会调用writeNonProxyDesc()方法,该方法实现如下:

private void writeNonProxyDesc(ObjectStreamClass desc, boolean unshared)
    throws IOException
{
     // TC_CLASSDESC =    (byte)0x72;
     // 表示一个新的Class描述符
    bout.writeByte(TC_CLASSDESC);
    handles.assign(unshared ? null : desc);

    if (protocol == PROTOCOL_VERSION_1) {
        // do not invoke class descriptor write hook with old protocol
        desc.writeNonProxy(this);
    } else {
        writeClassDescriptor(desc);
    }

    Class<?> cl = desc.forClass();
    bout.setBlockDataMode(true);
    if (cl != null && isCustomSubclass()) {
        ReflectUtil.checkPackageAccess(cl);
    }
    annotateClass(cl);
    bout.setBlockDataMode(false);
    bout.writeByte(TC_ENDBLOCKDATA);

    writeClassDesc(desc.getSuperDesc(), false);
}

在这个方法中首先会写入一个字节的TC_CLASSDESC,这个字节表示接下来的数据是一个新的Class描述符,接着会调用writeNonProxy()方法写入实际的类元信息,writeNonProxy()实现如下:

writeNonProxy() 方法:

void writeNonProxy(ObjectOutputStream out) throws IOException {
    // 写入类的名字
    out.writeUTF(name);
    // 写入类的序列号
    out.writeLong(getSerialVersionUID());
     // 获取类的标识
    byte flags = 0;
    if (externalizable) {
        flags |= ObjectStreamConstants.SC_EXTERNALIZABLE;
        int protocol = out.getProtocolVersion();
        if (protocol != ObjectStreamConstants.PROTOCOL_VERSION_1) {
            flags |= ObjectStreamConstants.SC_BLOCK_DATA;
        }
    } else if (serializable) {
        flags |= ObjectStreamConstants.SC_SERIALIZABLE;
    }
    if (hasWriteObjectData) {
        flags |= ObjectStreamConstants.SC_WRITE_METHOD;
    }
    if (isEnum) {
        flags |= ObjectStreamConstants.SC_ENUM;
    }
    // 写入类的flag
    out.writeByte(flags);
    // 写入对象的字段的个数
    out.writeShort(fields.length);
    for (int i = 0; i < fields.length; i++) {
        ObjectStreamField f = fields[i];
        out.writeByte(f.getTypeCode());
        out.writeUTF(f.getName());
        if (!f.isPrimitive()) {
              // 如果不是原始类型,即是对象或者Interface
            // 则会写入表示对象或者类的类型字符串
            out.writeTypeString(f.getTypeString());
        }
    }
}

writeNonProxy()方法中会按照以下几个过程来写入数据:

  1. 调用writeUTF()方法写入对象所属类的名字,对于本例中name = com.beautyboss.slogen.TestObject.对于writeUTF()这个方法,在写入实际的数据之前会先写入name的字节数,代码如下:
void writeUTF(String s, long utflen) throws IOException {
    if (utflen > 0xFFFFL) {
        throw new UTFDataFormatException();
    }
    // 写入两个字节的s的长度
    writeShort((int) utflen);
    if (utflen == (long) s.length()) {
        writeBytes(s);
    } else {
        writeUTFBody(s);
    }
}
  1. 接下来会调用writeLong()方法写入类的序列号UID,UID是通过getSerialVersionUID()方法来获取。

  2. 接着会判断被序列化的对象所属类的flag,并写入底层字节容器中(占用两个字节)。类的flag分为以下几类:

    1. final static byte SC_EXTERNALIZABLE = 0x04;表示该类为Externalizable类,即实现了Externalizable接口。
    2. final static byte SC_SERIALIZABLE = 0x02;表示该类实现了Serializable接口。
    3. final static byte SC_WRITE_METHOD = 0x01;表示该类实现了Serializable接口且自定义了writeObject()方法。
    4. final static byte SC_ENUM = 0x10;表示该类是个Enum类型。

    对于本例中flag = 0x02表示只是Serializable类型。

2.sutdent.out 文件中的二进制分别代表什么意思?

这里写图片描述

aced        Stream Magic
0005        序列化版本号

73          标志位:TC_OBJECT,表示接下来是个新的Object
72          标志位:TC_CLASSDESC,表示接下来是对Class的描述
001C        类名的长度为28
636F 6D2E 6A65 696B 6572 2E73 7368 2E6D 6F64 656C 2E53 7475 6465 6E74
            com.jeiekr.ssh.model.Student(类名)
9DEA 962E 78B3 F297 
            序列号
02          flag,可序列化
0003        Student的字段的个数,为3

4C          TypeCode:L,表示是个Class或者Interface
0003        字段名长度,占3个字节
61 67 65    字段名(age)

74          标志位:TC_STRING,表示后面的数据是个字符串
0013        类名长度,占19个字节
4C6A 6176 612F 6C61 6E67 2F49 6E74 6567 6572 3B
            Ljava/lang/Integer;

4C          TypeCode:L,表示是个Class或者Interface
0002        字段名长度,占2个字节
69 64       字段名(id)     

71          标志位:TC_REFERENCE,表示数据类型已经写入流(Integer007E        指向已经写入过的对象类型
0001        指向1号对象->Ljava/lang/Integer;

4C          TypeCode:L,表示是个Class或者Interface
0004        字段名长度,占4个字节
6E 61 6D 65 字段名(name)   

74          标志位:TC_STRING,表示后面的数据是个字符串
0012        类名长度,占18个字节
4C6A 6176 612F 6C61 6E67 2F53 7472 696E 67 3B
            Ljava/lang/String;

78          标志位:TC_ENDBLOCKDATA,对象的数据块描述的结束

70          标志位:TC_NULL,没有对象引用

73          标志位:TC_OBJECT,新对象
72          标志位:TC_CLASSDESC,新类描述
0011        类名长度,占17个字节
6A61 7661 2E6C 616E 672E 496E 7465 6765 72
            java.lang.Integer
12E2 A0A4 F781 8738 
            序列号
02          flag,表示可序列化
0001        字段个数,149          TypeCode,I,表示int类型      
0005        字段名长度,5个字节
76 61 6C 75 65  
            value
78          标志位:TC_ENDBLOCKDATA,对象的数据块描述的结束

72          标志位:TC_CLASSDESC,新类描述
0010        类名长度,占16个字节
6A61 7661 2E6C 616E 672E 4E75 6D62 6572
            java.lang.Number
86AC951D0B94E08B
            序列号
02          flag,表示可序列化
0000        字段个数,078          标志位:TC_ENDBLOCKDATA,对象的数据块描述的结束

70          标志位:TC_NULL,Null object reference.
0000 0019   age的值:25

73          标志位,TC_OBJECT:表示接下来是个新的Object
71          标志位:TC_REFERENCE,表示数据类型已经写入流(Integer007E        指向已经写入过的对象类型
0004        指向4号对象->java.lang.Number
0000 0001   id的值:1

74          标志位:TC_STRING,表示后面的数据是个字符串
0004        类名长度,占4个字节
7869 616F   name的值:xiao

4.Other

static 和 transient 字段不能被序列化。

阅读更多
想对作者说点什么?

博主推荐

换一批

没有更多推荐了,返回首页