Java反序列化本地不存在的类

前言

java序列化与反序列化应该是大家都比较熟悉的东西了。今天处理的是一种比较特殊的情况:在本地缺乏相应类的情况下,反序列化一个第三方的类。可能有点绕嘴,其实就是在A环境下保存了一个类A的序列化之后的信息,然后把这个信息在环境B下反序列化出来(环境B中是没有类A的)。正常的业务中很少出现这种用途,但是在逆向工程或者一些特殊场景下(比如我上篇文章..)还是可能会用到的。整体思路很简单,就是四个字——”无中生有“。也就是根据序列化的信息,本地生成一个符合要求的Class。主要用到了动态字节码技术。

反序列化流程

要想解决这个问题,还是需要了解一下反序列化的流程的。先贴一个典型的反序列化代码:

FileInputStream fileInputStream = new FileInputStream(new File("JavaBean.txt"));
ObjectInputStream ois = new ObjectInputStream(fileInputStream);
Object obj = ois.readObject();

咱们先看一下ObjectInputStream的构造方法:

 public ObjectInputStream(InputStream in) throws IOException {
        verifySubclass();
        bin = new BlockDataInputStream(in);
        handles = new HandleTable(10);
        vlist = new ValidationList();
        enableOverride = false;
        readStreamHeader();
        bin.setBlockDataMode(true);
    }

这里边除了初始化之外还做了两个校验工作。

verifySubclass:校验类是否是ObjectInputStream子类,如果是的话需要校验子类是否具有SUBCLASS_IMPLEMENTATION_PERMISSION权限。咱们最后的处理方式肯定是继承ObjectInputStream类然后重写关键方法的,这个校验需要注意下。

readStreamHeader:这个校验是对序列化之后的文件的头文件进行校验,校验序列化的版本号及Magic标记。

介绍完了构造方法,现在可以看反序列化方法了,也就是readObject这个方法。readObject里边逻辑很简单,做了个简单校验,然后调用了readObject0。直接来看readObject0

 private Object readObject0(boolean unshared) throws IOException {
        ....
        depth++;
        try {
            switch (tc) {
               ....

                case TC_OBJECT:
                    return checkResolve(readOrdinaryObject(unshared));

               ....
                default:
                    throw new StreamCorruptedException(
                        String.format("invalid type code: %02X", tc));
            }
        } finally {
            depth--;
            bin.setBlockDataMode(oldMode);
        }
    }

继续看readOrdinaryObject

private Object readOrdinaryObject(boolean unshared)
        throws IOException
    {
            //重复检查标记是否为Object
        if (bin.readByte() != TC_OBJECT) {
            throw new InternalError();
        }
        
                //下面重点讲解
        ObjectStreamClass desc = readClassDesc(false);
        desc.checkDeserialize();

                //获取描述对应的类
        Class<?> cl = desc.forClass();
        //排除String、Class、ObjectStreamClass这三个类,序列化时就做了特殊处理
        if (cl == String.class || cl == Class.class
                || cl == ObjectStreamClass.class) {
            throw new InvalidClassException("invalid class descriptor");
        }

        Object obj;
        try {
            //根据描述数据中的构造函数,利用反射创建对象,构造函数的规则在序列化时已经说明
            obj = desc.isInstantiable() ? desc.newInstance() : null;
        } catch (Exception ex) {
            throw (IOException) new InvalidClassException(
                desc.forClass().getName(),
                "unable to create instance").initCause(ex);
        }
        
        ...
        //两种接口的不同实现
        if (desc.isExternalizable()) {
            readExternalData((Externalizable) obj, desc);
        } else {
            //实现Serializable接口的调用
            readSerialData(obj, desc);
        }
        ...
                //判断是否存在readResolve()方法
        if (obj != null &&
            handles.lookupException(passHandle) == null &&
            desc.hasReadResolveMethod())
        {
            //执行并返回替换的对象
            Object rep = desc.invokeReadResolve(obj);
            if (unshared && rep.getClass().isArray()) {
                rep = cloneArray(rep);
            }
            if (rep != obj) {
                handles.setObject(passHandle, obj = rep);
            }
        }

        return obj;
    }

重点看readClassDesc

 private ObjectStreamClass readClassDesc(boolean unshared)
        throws IOException
    {
        byte tc = bin.peekByte();
        switch (tc) {
            case TC_NULL:
                return (ObjectStreamClass) readNull();

            case TC_REFERENCE:
                return (ObjectStreamClass) readHandle(unshared);

            case TC_PROXYCLASSDESC:
                return readProxyDesc(unshared);

            case TC_CLASSDESC:
                return readNonProxyDesc(unshared);

            default:
                throw new StreamCorruptedException(
                    String.format("invalid type code: %02X", tc));
        }
    }

对应序列化时,null,reference,proxy和nonProxy的情况,这里主要分析非代理对象的反序列化。

private ObjectStreamClass readNonProxyDesc(boolean unshared)
        throws IOException
    {
            //检查写入的是否为非代理对象
        if (bin.readByte() != TC_CLASSDESC) {
            throw new InternalError();
        }

        ObjectStreamClass desc = new ObjectStreamClass();
        ...
        ObjectStreamClass readDesc = null;
        try {
                //获取序列化时保存的描述类元信息,按照序列化时的顺序读取 
            readDesc = readClassDescriptor();
        ...

        Class cl = null;
        ...
        final boolean checksRequired = isCustomSubclass();
        try {
            //初始化加载描述类代表的序列化类
            if ((cl = resolveClass(readDesc)) == null) 
            ...
        skipCustomData();
        //初始化描述类元信息,注意这里又递归调用readClassDesc。我们序列化的时候是先写入子类的类元信息,再写入父类的;反序列化时,也需要先读入子类再父类,因此readClassDesc返回的是父类的类元描述信息,但是具体的初始化类元信息顺序还是先初始化父类再子类。
        desc.initNonProxy(readDesc, cl, resolveEx, readClassDesc(false));
        ...
        return desc;
    }

readClassDescriptor 方法完成了对ObjectStreamClass对象的填充,重点看一下,内部引用了ObjectStreamClass的readNonProxy方法:

void readNonProxy(ObjectInputStream in)
        throws IOException, ClassNotFoundException
    {
       //读取类名
        name = in.readUTF();
    //读取类型序列号
        suid = Long.valueOf(in.readLong());
        isProxy = false;
        //读取对象序列化的方式,writeObject方法,Serializable,Externalizable,还是SC_BLOCK_DATA
        byte flags = in.readByte();
        hasWriteObjectData =
            ((flags & ObjectStreamConstants.SC_WRITE_METHOD) != 0);
        hasBlockExternalData =
            ((flags & ObjectStreamConstants.SC_BLOCK_DATA) != 0);
        externalizable =
            ((flags & ObjectStreamConstants.SC_EXTERNALIZABLE) != 0);
        boolean sflag =
            ((flags & ObjectStreamConstants.SC_SERIALIZABLE) != 0);
        if (externalizable && sflag) {
            throw new InvalidClassException(
                name, "serializable and externalizable flags conflict");
        }
        serializable = externalizable || sflag;
        isEnum = ((flags & ObjectStreamConstants.SC_ENUM) != 0);
        if (isEnum && suid.longValue() != 0L) {
            throw new InvalidClassException(name,
                "enum descriptor has non-zero serialVersionUID: " + suid);
        }
        //读取对象属性个数
        int numFields = in.readShort();
        if (isEnum && numFields != 0) {
            throw new InvalidClassException(name,
                "enum descriptor has non-zero field count: " + numFields);
        }
        fields = (numFields > 0) ?
            new ObjectStreamField[numFields] : NO_FIELDS;
                //读取对象属性类型,及name
        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);
            }
        }
        //根据属性类型,初始化属性读取的缓存位置
        computeFieldOffsets();
    }

这一段代码就是读取序列化数据指定位置的字节,每个位置的字节都有特定的含义,然后对ObjectStreamClass进行填充。返回继续看readNonProxyDesc方法。当ObjectStreamClass填充完之后做了一个校验

if ((cl = resolveClass(readDesc)) == null) 

重点看这个校验方法

 protected Class<?> resolveClass(ObjectStreamClass desc)
        throws IOException, ClassNotFoundException
    {
        String name = desc.getName();
        try {
            return Class.forName(name, false, latestUserDefinedLoader());
        } catch (ClassNotFoundException ex) {
            Class<?> cl = primClasses.get(name);
            if (cl != null) {
                return cl;
            } else {
                throw ex;
            }
        }
    }

其实就是根据ObjectStreamClass里边存储的解析出来的Class路径,从本地加载该路径。到这就能看出来了,假如我们本地不存在这个Class的话就会报出ClassNotFoundException的异常。也就是说,我们需要处理的方法就是resolveClass。不过都看到这了,咱们先把反序列化的流程看完,后续再考虑怎么处理ClassNotFoundException的异常。

回到readNonProxyDesc方法,校验完成之后又调用了

desc.initNonProxy(readDesc, cl, resolveEx, readClassDesc(false));

利用刚才解析出来的readDesc将desc初始化完毕。initNonProxy里边基本就是将readDesc 字段的值复制到

desc中,只不过是多了一层校验。到这就彻底完成了ObjectStreamClass的解析读取,咱们回到readOrdinaryObject继续看。

ObjectStreamClass解析完成之后,调用ObjectStreamClass.newInstance()生成了一个空的目标对象。ObjectStreamClass.newInstance()底层调用了是刚才加载出来的Class的Constructor的newInstance。到这反序列化的容器对象已经生成,只不过里边的字段还在等待填充。
填充的代码是这两句

 if (desc.isExternalizable()) {
     readExternalData((Externalizable) obj, desc);
 } else {
     readSerialData(obj, desc);
 }

我们这只考虑实现了Serializable接口的也就是readSerialData(obj, desc);

private void readSerialData(Object obj, ObjectStreamClass desc)
        throws IOException
    {
        // 获取要序列化的类,包括实现了 Serializable 接口的父类
        ObjectStreamClass.ClassDataSlot[] slots = desc.getClassDataLayout();
        for (int i = 0; i < slots.length; i++) {
            ObjectStreamClass slotDesc = slots[i].desc;
           ....
           //执行数据填充
           defaultReadFields(obj, slotDesc);
           ....
        }
    }

 private void defaultReadFields(Object obj, ObjectStreamClass desc)
        throws IOException
    {
        ....
        int objHandle = passHandle;
        //获取类的需要填充数据的字段数组
        ObjectStreamField[] fields = desc.getFields(false);
        //生成对应数量的值的数组
        Object[] objVals = new Object[desc.getNumObjFields()];
        int numPrimFields = fields.length - objVals.length;
        //遍历字段数组
        for (int i = 0; i < objVals.length; i++) {
            ObjectStreamField f = fields[numPrimFields + i];
            // 关键点在这,对于字段的值其实递归调用了readObject0方法,所以咱们只需要针对readObject0做处理,不用担心嵌套Serializable的情况
            objVals[i] = readObject0(f.isUnshared());
            if (f.getField() != null) {
                handles.markDependency(objHandle, passHandle);
            }
        }
        if (obj != null) {
            //利用反射完成了值的填充
            desc.setObjFieldValues(obj, objVals);
        }
        passHandle = objHandle;
    }

好了其实到这java反序列化的源码咱们就看的差不多了。总结一下:
java反序列化的整体流程并不复杂,首先通过分析序列化信息,读取目标类的类名、字段、serialVersionUID等关键信息,读取之后判断读取到的类本地是否存在,也就是在这当这个类本地不存在时虚拟机会抛出一个ClassNotFoundException,这也就是咱们需要处理的关键点。校验完成之后,会利用反射生成一个空的目标对象,然后通过递归调用readObject0方法,给这个对象的字段赋值,完成了一次反序列化流程。

瞒天过海,无中生有

了解了反序列化流程之后,后续需要做的就十分清楚了。我们就是要继承ObjectInputStream,然后重写resolveClass方法,在这个方法中通过Java的动态字节码技术,根据方法中传入的ObjectStreamClass对象(包含目标类的类信息)动态生成一个目标类。

public class MyObjectInputStream extends ObjectInputStream{
    public MyObjectInputStream(InputStream in) throws IOException {
        super(in);
    }

    protected Class<?> resolveClass(ObjectStreamClass desc)
            throws IOException, ClassNotFoundException {
        String name = desc.getName();
        try {
            return Class.forName(name, false, sun.misc.VM.latestUserDefinedLoader());
        } catch (ClassNotFoundException ex) {
            try {
                return createClass(desc);
            } catch (Exception e) {
                e.printStackTrace();
                return null;
            }
        }
    }
}

createClass 方法根据传入的对象,动态生成了目标类的字节码信息,并加载进了内存。
Java的动态字节码技术,有很多方式实现,比如我之前写到的关于ASM技术的文章ASM——运行时/编译时动态修改class源码。不过ASM的api偏底层,需要对JVM虚拟机了解的深一些。在这里我采用了javassist框架,api封装的更面向对象一些,使用起来门槛较低。

    private Class createClass(ObjectStreamClass desc) throws Exception {
        //获取类的全路径
        String name = desc.getName();
        //获取类的字段数组
        ObjectStreamField[] fields = desc.getFields();
        ClassPool pool = ClassPool.getDefault();
        //根据类的全路径生成一个空类的字节码信息
        CtClass cc = pool.makeClass(name);
        CtClass interf = pool.getCtClass(Serializable.class.getName());
        CtClass[] classes = new CtClass[]{interf};
        //设置这个类实现Serializable接口
        cc.setInterfaces(classes);
        //遍历字段数组,添加到刚生成的类的字节码信息中
        for (ObjectStreamField field : fields) {
            String name1 = field.getName();
            String name2 = field.getType().getName();
            CtField param = new CtField(pool.get(name2), name1, cc);
            //统一设置public修饰
            param.setModifiers(Modifier.PUBLIC);
            //添加到字节码信息中
            cc.addField(param);
        }
        //必须添加一个构造参数
        CtConstructor cons = new CtConstructor(new CtClass[]{}, cc);
        //设置构造函数的方法体
        cons.setBody("{}");
        //将构造函数添加到字节码信息中
        cc.addConstructor(cons);
        //生成serialVersionUID相关字段,并添加到字节码信息中
        CtField param = new CtField(pool.get("long"), "serialVersionUID", cc);
        param.setModifiers(Modifier.PUBLIC | Modifier.STATIC | Modifier.FINAL);
        cc.addField(param, CtField.Initializer.constant(desc.getSerialVersionUID()));
        //返回刚刚生成的Class
        return cc.toClass();
    }

这里有几点需要注意的:

  1. 生成的类除了字段数组里边的字段,还需要生成serialVersionUID字段。
  2. 记得让类实现Serializable接口。
  3. 自动生成的目标类对比真正的目标类会缺失除了构造函数之外的所有方法,并且所有字段都为变成public修饰,不过这个并不影响效果。

然后重新进行反序列化操作:

FileInputStream fileInputStream = new FileInputStream(new File("JavaBean.txt"));
ObjectInputStream ois = new MyObjectInputStream(fileInputStream);
Object obj = ois.readObject();
System.out.println(new Gson().toJson(obj ));

这个时候即使你本地没有JavaBean类,也能成功的将序列化的信息反序列化出来,并且利用Gson打印出来。
到这java反序列化一个本地不存在的class基本上就算是做完了。我本地只跑过有限的几个测试用例,可能不能覆盖所有的情况,但是大体上的思路应该是没问题的。

Android Parcelable 反序列化

上边说的是纯java环境下的反序列化,在Android环境下还有一种序列化情况:实现Parcelable接口的类。在这简单说一下Parcelable的反序列化过程。Parcelable的序列化与反序列化其实对比java的Serializable方式的序列化与反序列化,你会发现Parcelable的序列化和反序列化的操作全部都是由自己实现的,而Serializable的序列化和反序列化则完全是由jdk实现的,jdk这样设计有个比较大的好处就是代码侵入性低,Serializable接口是个空接口,只需要声明实现一下,但是并不需要过多的改动代码。下面看个Parcelable的例子

public class Album implements Parcelable {

    /**
     * 负责反序列化
     */
    private static final Creator<Album> CREATOR = new Creator<Album>() {
        /**
         * 从序列化对象中,获取原始的对象
         * @param source
         * @return
         */
        @Override
        public Album createFromParcel(Parcel source) {
            return new Album(source);
        }

        /**
         * 创建指定长度的原始对象数组
         * @param size
         * @return
         */
        @Override
        public Album[] newArray(int size) {
            return new Album[0];
        }
    };



    private final String mId;
    private final String mCoverPath;
    private final String mDisplayName;
    private final long mCount;


    Album(String id, String coverPath, String displayName, long count) {
        mId = id;
        mCoverPath = coverPath;
        mDisplayName = displayName;
        mCount = count;
    }

    Album(Parcel source) {
        mId = source.readString();
        mCoverPath = source.readString();
        mDisplayName = source.readString();
        mCount = source.readLong();
    }

    /**
     * 描述
     * 返回的是内容的描述信息
     * 只针对一些特殊的需要描述信息的对象,需要返回1,其他情况返回0就可以
     *
     * @return
     */
    @Override
    public int describeContents() {
        return 0;
    }

    /**
     * 序列化
     *
     * @param dest
     * @param flags
     */
    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeString(mId);
        dest.writeString(mCoverPath);
        dest.writeString(mDisplayName);
        dest.writeLong(mCount);
    }

可以看到Parcelable 的实现方法还是稍微有点复杂的。里边执行序列化操作的方法是writeToParcel方法,将对象的字段信息用固定的顺序写入了Parcel 中。反序列化操作则是用参数为Parcel的构造方法完成的。这个构造函数的方法体其实就是对应着writeToParcel中的写入顺序,将信息再次从Parcel中读出并赋值到类的字段中。writeToParcel和参数为Parcel的构造方法中的读写顺序必须是一一对应,绝对不能出错的。
结合我之前的文章Android爬取第三方app推送消息,可以知道Parcel中对于Parcelable数据存储时其实只存储了在writeToParcel方法中写入的信息。也就是并没有像Serializable序列化过程中一并存储下来的类相关信息。也就导致了我们无法用处理Serializable反序列化的思路来处理Parcelable反序列化。实际上Parcelable反序列化的核心就是字段读取的顺序,但凡一个字段读取顺序没对上都可能会导致后续字段读取的严重错。所以至少目前为止,是没有很好的方法能够批量解决Parcelable反序列化问题的。

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