jdk序列化与反序列化底层机制

唠嗑

好久没有写博客了,懈怠了,周一到周五工作,没时间写,周末想着放松放松,也不想写,一拖再拖。

最近意识到正式工作快满一年了(稳定运营的游戏部门,进行玩法开发),想回头想一想这一年有什么进步,却发现好像除了对业务更加熟悉一些,技术上并不知道有哪些明显的提升,问了下与我一起入职的同事,也有这样的困惑。这也许是业务部门的通病,公司论坛中也有很多人表示:一直维护老系统,如何提升技术,如何晋级等。还记得有个观点大概是这样的:

优化项目代码,对重复的代码抽象出通用的模块

我们现在的项目中确实有许多不优雅的、重复的代码,虽然我知道它好像有点问题,但是我暂时没有能力去解决这个问题。

前两天在研究一个并发的问题时,搜到了一个大佬的博客,发现大佬写了很多基础性的文章,意识到别人的成功并不是偶然,所以我还是先从基础入手吧,地基打好了再去尝试解决上层的问题。

序列化与反序列化

这篇文章的主要内容是jdk的序列化机制,主要是与Serializable有关的内容,序列化主要用在数据存储和网络传输。其实项目中并没有用到Serializable这个序列化机制,用的主要是protobuf,JDK自带的性能和拓展性上逊色了一些。

java对象在运行时都是在内存中,程序结束就消失了,如果我们想要在下次启动程序的时候恢复这些对象,就需要把内存中的这些对象的成员变量数据保存到一个地方,比如说文件中,然后下次启动时从文件里读取并放入内存,这边就涉及到java内存对象到文件内容的相互转化,就是序列化。当然你可以有很多不同的序列化协议,比如对内容加密,对内容压缩(传输快,占用空间小),JDK就提供了这样的一种机制。

下面看下如何使用JDK的序列化机制,将User对象保存进文件,然后再读出来。

// User对象
public class User implements Serializable {
    public String name;
    public int age;

    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }
    // 省略getter setter
}

public static void main(String[] args) {
    User user1 = new User("cfk", 24);
    // 写入
    try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(new File("user.txt")))) {
        out.writeObject(user1);
    } catch (IOException e) {
        logger.error("write error.", e);
    }
    // 读取
    User user2 = null;
    try (ObjectInputStream in = new ObjectInputStream(new FileInputStream(new File("user.txt")))) {
        user2 = (User) in.readObject();
    } catch (Exception e) {
        logger.error("read error.", e);
    }
    if (user2 != null) {
        System.out.println("age:" + user2.getAge());
        System.out.println("name:" + user2.getName());
        System.out.println("isEqual:" + (user1 == user2));
    }
}
输出结果
age:24
name:cfk
isEqual:false

user.txt文件的内容:

  • ���sr�com.chenfeikun.User~�s:�!���I�ageL�namet�Ljava/lang/String;xp�t�cfk

上面是一个简单的使用示例,但是还有很多疑问在里面:

  1. Serializable接口的作用
  2. serialVersionUID的作用,为什么User里没有定义也可以序列化成功
  3. 反序列化时使用成员变量与User一模一样的Student类会成功吗
  4. 序列化后修改User类,再反序列化,会发生什么
  5. transient修饰符有什么用

后面会通过源码和实验来一一回答这些问题。

Serializable

看下接口的定义

public interface Serializable {}

接口没有定义任何的方法,那User为什么必须实现这个接口呢,那么JDK采用什么方式来进行序列化呢?这些来看下ObjectOutputStreamwriteObject方法。

public final void writeObject(Object obj) throws IOException {
    if (enableOverride) { // 给一些ObjectOutputStream的子类用的
        writeObjectOverride(obj);
        return;
    }
    try { // 如果是ObjectOutputStream,则默认该分支
        writeObject0(obj, false);
    } catch (IOException ex) {
        // ignore
    }
}
// writeObject0方法
private void writeObject0(Object obj, boolean unshared) throws IOException {
    boolean oldMode = bout.setBlockDataMode(false);
    depth++;
    try {
        // 省略一些代码
        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) {
            // 如果不是上面列出的几种类型,就必须是Serializable的实例对象
            // 否则抛出异常
            writeOrdinaryObject(obj, desc, unshared);
        } else {
            if (extendedDebugInfo) {
                throw new NotSerializableException(
                    cl.getName() + "\n" + debugInfoStack.toString());
            } else {
                throw new NotSerializableException(cl.getName());
            }
        }
    } finally {
        depth--;
        bout.setBlockDataMode(oldMode);
    }
}

上面的这些代码已经可以解释为什么需要序列化的对象需要继承Serializable接口,下面继续深入下去,看看默认的序列化方法是什么,User.txt保存了什么内容。

private void writeOrdinaryObject(Object obj,
    ObjectStreamClass desc, boolean unshared) throws IOException
{
    //省略debug日志
    try {
        desc.checkSerialize();
        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();
        }
    }
}
//writeClassDesc方法
private void writeClassDesc(ObjectStreamClass desc, boolean unshared)
        throws IOException {
    if (desc == null) {
        // 省略
    } else {
        writeNonProxyDesc(desc, unshared);//最终选择这个分支
    }
}
//writeNonProxyDesc方法
private void writeNonProxyDesc(ObjectStreamClass desc, boolean unshared)
        throws IOException {
    bout.writeByte(TC_CLASSDESC);//写入类描述信息开始标记
    handles.assign(unshared ? null : desc);
    if (protocol == PROTOCOL_VERSION_1) {
        desc.writeNonProxy(this);
    } else {
        // 进入该分支
        writeClassDescriptor(desc);//写入类信息,包括serialVersionUID
    }
    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); // 写入父类信息
}
// writeClassDescriptor最终调用了
void writeNonProxy(ObjectOutputStream out) throws IOException {
    out.writeUTF(name);
    out.writeLong(getSerialVersionUID());//写入serialVersionUID
    // ignore
    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()) {
            out.writeTypeString(f.getTypeString());
        }
    }
}

上面的部分粗略的描述了下User.txt文件中的一部分内容:类相关的描述信息,其实具体往文件写了什么并不重要,这只是JDK自己设计的一个协议,就像http,tcp等也有自己的协议,哪一部分对应了什么,都是协议的一部分。

让我们自己设计其实也很容易想明白,需要写入类的描述信息(类名,成员变量名,数据类型等),父类的描述信息,对象的成员变量值等,下面来看下成员变量是如何写入文件的。

在上面的writeOrdinaryObject方法中,写完类相关的信息以后,就开始写入成员变量的数据了。

//writeSerialData写入成员变量的方法
private void writeSerialData(Object obj, ObjectStreamClass desc)
        throws IOException {
    OectStreamClass.ClassDataSlot[] slots = desc.getClassDataLayout();
    for (int i = 0; i < slots.length; i++) {
        ObjectStreamClass slotDesc = slots[i].desc;
        if (slotDesc.hasWriteObjectMethod()) { // 如果有writeObjecr方法,则调用
            // ignore
                slotDesc.invokeWriteObject(obj, this);
                bout.setBlockDataMode(false);
                bout.writeByte(TC_ENDBLOCKDATA);
        } else { // 否则使用默认的序列化方法
        // 因为我们的User对象没有实现writeObject方法,所以进入该分支
            defaultWriteFields(obj, slotDesc); // 不介绍了,将变量的具体值写入
        }
    }
}

ObjectStreamClass desc对象介绍

上面的方法中有一个一直出现的desc变量,它保存了类的一些信息:

  1. serialVersionUID的值,如果不存在则会根据类的一些信息,计算出一个值(因此类发生改变,这个值就会变化),如果存在则直接使用我们定义的数值。
  2. 是否实现了writeObjectreadObject方法,从而判断选择默认的序列化方法还是选择自定义的序列化方法
  3. 有哪些成员变量,剔除了transient和statci修饰的变量

与这些信息相对应的代码如下:

// 获取serialVersionUID的方法
// 首先查看类自己有没有定义
private static Long getDeclaredSUID(Class<?> cl) {
    try {
        // 没有获取到会抛异常
        Field f = cl.getDeclaredField("serialVersionUID");
        int mask = Modifier.STATIC | Modifier.FINAL;
        if ((f.getModifiers() & mask) == mask) {
            f.setAccessible(true);
            return Long.valueOf(f.getLong(null));
        }
    } catch (Exception ex) {
    }
    return null;
}
// 如果没有,则计算一个
// 计算过程比较复杂,不看了
// 参与计算的对象包括:类名,方法,接口,变量相关信息等
public long getSerialVersionUID() {
    if (suid == null) {
        suid = AccessController.doPrivileged(
            new PrivilegedAction<Long>() {
                public Long run() {
                    return computeDefaultSUID(cl);
                }
            }
        );
    }
    return suid.longValue();
}

// 何时判断有没有writeObjecet和readObject方法
// 在创建desc对象时的构造函数里
writeObjectMethod = getPrivateMethod(cl, "writeObject",
        new Class<?>[] { ObjectOutputStream.class },
        Void.TYPE);
readObjectMethod = getPrivateMethod(cl, "readObject",
        new Class<?>[] { ObjectInputStream.class },
        Void.TYPE);

// 获取成员变量的方法
private static ObjectStreamField[] getDefaultSerialFields(Class<?> cl) {
    // 参数cl就是需要序列化的对象的class
    Field[] clFields = cl.getDeclaredFields();
    ArrayList<ObjectStreamField> list = new ArrayList<>();
    int mask = Modifier.STATIC | Modifier.TRANSIENT;
    for (int i = 0; i < clFields.length; i++) {
        // 剔除transient和statci修饰的变量
        if ((clFields[i].getModifiers() & mask) == 0) {
            list.add(new ObjectStreamField(clFields[i], false, true));
        }
    }
    int size = list.size();
    return (size == 0) ? NO_FIELDS :
        list.toArray(new ObjectStreamField[size]);
}

既然文件里存储了SerialVersionUID,那么显然读取的时候会依靠它来判断是否能够反序列化。如果User没有定义SerialVersionUID,那么通过类的一些信息可以计算出一个uid,不难想象的是:如果类的定义发生了一些改变,uid的值就会变,反序列化是会失败的。
除了uid,前面也提到了,User.txt会写入序列化对象的类名,所以反序列化也不可能变成Student

反序列化时使用成员变量与User一模一样的Student类会成功吗?

因此这个问题的答案应该是不会成功。下面通过代码看下如果修改User类的定义会怎么样。

代码验证

验证步骤:

  1. 把上面的User对象序列化后写入文件
  2. 修改User类,增加一个private变量(删除效果一样)
  3. 用新的User类反序列化

反序列化代码如下:

// 序列化代码与最上面的一样,反序列化增加了一行print新变量school
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream(new File("user.txt")))) {
    User user2 = (User) in.readObject();
    System.out.println("age:" + user2.getAge());
    System.out.println("name:" + user2.getName());
    System.out.println("school:" + user2.getSchool());
} catch (Exception e) {
    logger.error("read error.", e);
}

验证结果分为自定义serialVersionUid和不定义serialVersionUid两种情况

  • 不定义uid

运行后报错,serialVersionUid不同

java.io.InvalidClassException: com.chenfeikun.User; local class incompatible: stream classdesc serialVersionUID = 1261513276839669673, local class serialVersionUID = -8884757256612786415
    at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:699)
    at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1885)
    at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1751)
    at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2042)
    at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1573)
    at java.io.ObjectInputStream.readObject(ObjectInputStream.java:431)
    at com.chenfeikun.Test1.main(Test1.java:21)
  • 定义uid

运行正常,新的变量为null

age:24
name:cfk
school:null

serialVersionUid需要自己定义吗?

我想是需要的,因为无法预知以后的需求,对象很有可能改变,如果使用JDK帮忙计算的uid值,就没有拓展性,只有自定义了serialVersionUid才能够支持对象修改后,依然可以正常的序列化与反序列化。

writeObject与readObject

如果不想使用默认的序列化方式,也可以通过实现这两个方法来定义自己的序列化过程。直接看JDK源码中的例子吧,就拿大家都用过的ArrayList来看看如何实现。

从上面的分析可以知道statictransient修饰的变量不会进行序列化,那么ArrayList的变量就只剩下一个了

private int size;

而ArrayList存储数据的数组是用transient修饰的

transient Object[] elementData;

如果列表序列化时不存储列表中的数据,那么序列化毫无意义,答案就在writeObject和readObject里。

private void writeObject(java.io.ObjectOutputStream s)
    throws java.io.IOException{
    // modCount用来防止并发修改,以前分析ArrayList源码的文章介绍过
    int expectedModCount = modCount;
    s.defaultWriteObject();
    s.writeInt(size);// 写入数组中数据的实际个数
    for (int i=0; i<size; i++) {
        // 通过for循环把数据写入
        s.writeObject(elementData[i]);
    }
    if (modCount != expectedModCount) {
        throw new ConcurrentModificationException();
    }
}
// readObject方法
// 差不多就把write方法反过来
private void readObject(java.io.ObjectInputStream s)
    throws java.io.IOException, ClassNotFoundException {
    elementData = EMPTY_ELEMENTDATA;// 初始化空数组
    s.defaultReadObject();
    s.readInt();
    if (size > 0) {
        int capacity = calculateCapacity(elementData, size);
        SharedSecrets.getJavaOISAccess().checkArray(s, Object[].class, capacity);
            ensureCapacityInternal(size);
        Object[] a = elementData;
        for (int i=0; i<size; i++) {
            a[i] = s.readObject();
        }
    }
}

ArrayList这样设计有什么好处呢?

因为数组可能没有塞满数据,如果无脑的全部序列化,会浪费很多没用的空间,所以自己实现writeObject方法只把有用的数据进行序列化。

Externalizable vs Serializable

前面的代码中,写入成员变量时我们只分析了一个分支,其实还有另外一个分支,回顾一下。

if (desc.isExternalizable() && !desc.isProxy()) {
    writeExternalData((Externalizable) obj);//这个没介绍
} else {
    writeSerialData(obj, desc);//前面介绍了这个
}

上面的分支就是实现了Externalizable接口的对象会走的分支,两者的区别是:Externalizable强制要求自己实现序列化的过程,而Serializable有默认的序列化方式。
可以看下其接口定义:

// 继承了Serializable
public interface Externalizable extends java.io.Serializable {
    void writeExternal(ObjectOutput out) throws IOException;

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

其他没有太多区别了。

总结

  1. 需要继承Serializable接口
  2. 对想要序列化的对象定义serialVersionUID值,支持一定的兼容性
  3. 如果不定义serialVersionUID,JDK会自动计算一个(根据类相关信息)
  4. static和transient修饰的变量不会序列化
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,039评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,223评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,916评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,009评论 1 291
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,030评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,011评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,934评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,754评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,202评论 1 309
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,433评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,590评论 1 346
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,321评论 5 342
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,917评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,568评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,738评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,583评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,482评论 2 352