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閱讀 227,837評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,196評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,688評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,654評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,456評論 6 406
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,955評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,044評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,195評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,725評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,608評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,802評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,318評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,048評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,422評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,673評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,424評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,762評論 2 372