源码分析毕昇JDK快速反序列化原理

样子的木偶
样子的木偶
发布于 2023-11-29 / 65 阅读
0
0

源码分析毕昇JDK快速反序列化原理

反序列化原理

对象的反序列化主要通过readObject方法实现,其主要流程是从字节流中读取出对象的类型及value信息。

对于普通的对象,通过readOrdinaryObject进行读取。
对于数组,通过readArray进行读取。
对于string类型,通过readString进行读取。
对于enum类型,通过readEnum进行反序列化。

readOrdinaryObject主要分为两个部分:readClassDesc和readSerialData
readClassDesc用来读取序列化对象的descriptor,包含对象的field name和signature。
readSerialData读取继承Serialize接口的field value,包括primitive类型和obj类型。在对obj value进行反序列化时,会重新调用回readObject方法。
readExternalData是用来读取继承Externalize接口的序列化对象的field value

readOrdinaryObject在读取完对象的descriptor后,紧接着会执行passHandle = handles.assign(obj)方法,将obj存入handles中的objs[]数组,在读取完对象的data后,会执行handles.finish(passHandle)。下次再读取到相同对象时,会直接从handles(readHandle)中读取。

readClassDesc:

image.png

reacClassDesc用来读取对象的descriptor信息。根据对象的不同,可能走以下几个路径:

  • readNull: 对象为null走此路径。

  • readNonProxyDesc:对象为一个普通非动态代理类对象。我们的优化主要在这里进行实现:

  • readHandle:当对象已经被反序列化过一次,则直接从handle中通过lookupObject方法进行查询。

  • readProxyDesc:动态代理类的对象走此路径

以下是openJDK中的读取descriptor信息的方法

    protected ObjectStreamClass readClassDescriptor()
        throws IOException, ClassNotFoundException
    {
        ObjectStreamClass desc = new ObjectStreamClass();
        desc.readNonProxy(this);
        return desc;
    }

在每一次读取时都会创建一个ObjectStreamClass对象,通过调用readNonProxy来获取name/suid/isProxy等信息

        name = in.readUTF();
        suid = Long.valueOf(in.readLong());
        isProxy = false;

最后通过readTypeString()读取出signature

        for (int i = 0; i < numFields; i++) {
            char tcode = (char) in.readByte();
            String fname = in.readUTF();
            String signature = ((tcode == 'L') || (tcode == '[')) ?
                in.readTypeString() : new String(new char[] { tcode });
            try {
                fields[i] = new ObjectStreamField(fname, signature, false);
            } catch (RuntimeException e) {
                throw (IOException) new InvalidClassException(name,
                    "invalid descriptor for field " + fname).initCause(e);
            }
        }

然后创建ObjectStreamField

            try {
                fields[i] = new ObjectStreamField(fname, signature, false);
            } catch (RuntimeException e) {
                throw (IOException) new InvalidClassException(name,
                    "invalid descriptor for field " + fname).initCause(e);
            }

优化后

毕昇JDK readClassDescriptor()优化后代码如下:

并没有直接创建ObjectStreamClass而是先通过userFastSerializer判断是否存在同一个class descriptor如果存在则直接从缓存中进行读取

 protected ObjectStreamClass readClassDescriptor()
        throws IOException, ClassNotFoundException
    {
        // fastSerializer
        if (useFastSerializer) {
            String name = readUTF();
            Class<?> cl = null;
            ObjectStreamClass desc = new ObjectStreamClass(name);
            try {
                // In order to match this method, we add an annotateClass method in
                // writeClassDescriptor.
                cl = resolveClass(desc);
            } catch (ClassNotFoundException ex) {
                // resolveClass is just used to obtain Class which required by lookup method
                // and it will be called again later, so we don't throw ClassNotFoundException here.
                return desc;
            }
            if (cl != null) {
                desc = ObjectStreamClass.lookup(cl, true);
            }
            return desc;
        }

        // Default deserialization. If the Class cannot be found, throw ClassNotFoundException.
        ObjectStreamClass desc = new ObjectStreamClass();
        desc.readNonProxy(this);
        return desc;
    }

但是这里也会又一些兼容性的问题

在这里的userFastSerializer实际上是由public native boolean getUseFastSerializer();

获取native方法会给我们程序效率带来提高,几乎没有什么毛病,实际不然,使用native方法会带来潜在的安全隐患,因为本机方法执行实际的机器代码,它有权使用主机系统的所有资源,本机执行代码不收java执行环境的限制,因此可能会导致病毒入侵。与此同时,因为使用本机方法,调用dll动态链接库,具体实现代码都是在dll文件中的,因为本机方法都是依赖于CPU和操作系统,因此dll文件在本质上,是不可移植的,从而丧失了程序的可移植性。


评论