美文网首页程序员
jdk序列化与反序列化底层机制

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

作者: _kkk | 来源:发表于2020-06-14 22:59 被阅读0次

    唠嗑

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

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

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

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

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

    序列化与反序列化

    这篇文章的主要内容是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修饰的变量不会序列化

    相关文章

      网友评论

        本文标题:jdk序列化与反序列化底层机制

        本文链接:https://www.haomeiwen.com/subject/wplqxktx.html