前言
java序列化與反序列化應(yīng)該是大家都比較熟悉的東西了。今天處理的是一種比較特殊的情況:在本地缺乏相應(yīng)類的情況下结胀,反序列化一個第三方的類赞咙。可能有點繞嘴糟港,其實就是在A環(huán)境下保存了一個類A的序列化之后的信息攀操,然后把這個信息在環(huán)境B下反序列化出來(環(huán)境B中是沒有類A的)。正常的業(yè)務(wù)中很少出現(xiàn)這種用途秸抚,但是在逆向工程或者一些特殊場景下(比如我上篇文章..)還是可能會用到的速和。整體思路很簡單歹垫,就是四個字——”無中生有“。也就是根據(jù)序列化的信息颠放,本地生成一個符合要求的Class县钥。主要用到了動態(tài)字節(jié)碼技術(shù)。
反序列化流程
要想解決這個問題慈迈,還是需要了解一下反序列化的流程的若贮。先貼一個典型的反序列化代碼:
FileInputStream fileInputStream = new FileInputStream(new File("JavaBean.txt"));
ObjectInputStream ois = new ObjectInputStream(fileInputStream);
Object obj = ois.readObject();
咱們先看一下ObjectInputStream的構(gòu)造方法:
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權(quán)限谴麦。咱們最后的處理方式肯定是繼承ObjectInputStream類然后重寫關(guān)鍵方法的,這個校驗需要注意下伸头。
readStreamHeader:這個校驗是對序列化之后的文件的頭文件進(jìn)行校驗匾效,校驗序列化的版本號及Magic標(biāo)記。
介紹完了構(gòu)造方法恤磷,現(xiàn)在可以看反序列化方法了面哼,也就是readObject這個方法。readObject里邊邏輯很簡單扫步,做了個簡單校驗魔策,然后調(diào)用了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);
}
}
繼續(xù)看readOrdinaryObject
private Object readOrdinaryObject(boolean unshared)
throws IOException
{
//重復(fù)檢查標(biāo)記是否為Object
if (bin.readByte() != TC_OBJECT) {
throw new InternalError();
}
//下面重點講解
ObjectStreamClass desc = readClassDesc(false);
desc.checkDeserialize();
//獲取描述對應(yīng)的類
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 {
//根據(jù)描述數(shù)據(jù)中的構(gòu)造函數(shù)游岳,利用反射創(chuàng)建對象政敢,構(gòu)造函數(shù)的規(guī)則在序列化時已經(jīng)說明
obj = desc.isInstantiable() ? desc.newInstance() : null;
} catch (Exception ex) {
throw (IOException) new InvalidClassException(
desc.forClass().getName(),
"unable to create instance").initCause(ex);
}
...
//兩種接口的不同實現(xiàn)
if (desc.isExternalizable()) {
readExternalData((Externalizable) obj, desc);
} else {
//實現(xiàn)Serializable接口的調(diào)用
readSerialData(obj, desc);
}
...
//判斷是否存在readResolve()方法
if (obj != null &&
handles.lookupException(passHandle) == null &&
desc.hasReadResolveMethod())
{
//執(zhí)行并返回替換的對象
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));
}
}
對應(yīng)序列化時,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();
//初始化描述類元信息,注意這里又遞歸調(diào)用readClassDesc访锻。我們序列化的時候是先寫入子類的類元信息褪尝,再寫入父類的;反序列化時朗若,也需要先讀入子類再父類恼五,因此readClassDesc返回的是父類的類元描述信息昌罩,但是具體的初始化類元信息順序還是先初始化父類再子類哭懈。
desc.initNonProxy(readDesc, cl, resolveEx, readClassDesc(false));
...
return desc;
}
readClassDescriptor 方法完成了對ObjectStreamClass對象的填充,重點看一下茎用,內(nèi)部引用了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);
}
//讀取對象屬性個數(shù)
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);
}
}
//根據(jù)屬性類型垂券,初始化屬性讀取的緩存位置
computeFieldOffsets();
}
這一段代碼就是讀取序列化數(shù)據(jù)指定位置的字節(jié)花盐,每個位置的字節(jié)都有特定的含義,然后對ObjectStreamClass進(jìn)行填充菇爪。返回繼續(xù)看readNonProxyDesc方法算芯。當(dāng)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;
}
}
}
其實就是根據(jù)ObjectStreamClass里邊存儲的解析出來的Class路徑,從本地加載該路徑凳宙。到這就能看出來了熙揍,假如我們本地不存在這個Class的話就會報出ClassNotFoundException的異常。也就是說氏涩,我們需要處理的方法就是resolveClass届囚。不過都看到這了,咱們先把反序列化的流程看完是尖,后續(xù)再考慮怎么處理ClassNotFoundException的異常意系。
回到readNonProxyDesc方法,校驗完成之后又調(diào)用了
desc.initNonProxy(readDesc, cl, resolveEx, readClassDesc(false));
利用剛才解析出來的readDesc將desc初始化完畢饺汹。initNonProxy里邊基本就是將readDesc 字段的值復(fù)制到
desc中昔字,只不過是多了一層校驗。到這就徹底完成了ObjectStreamClass的解析讀取首繁,咱們回到readOrdinaryObject繼續(xù)看作郭。
ObjectStreamClass解析完成之后,調(diào)用ObjectStreamClass.newInstance()生成了一個空的目標(biāo)對象弦疮。ObjectStreamClass.newInstance()底層調(diào)用了是剛才加載出來的Class的Constructor的newInstance夹攒。到這反序列化的容器對象已經(jīng)生成,只不過里邊的字段還在等待填充胁塞。
填充的代碼是這兩句
if (desc.isExternalizable()) {
readExternalData((Externalizable) obj, desc);
} else {
readSerialData(obj, desc);
}
我們這只考慮實現(xiàn)了Serializable接口的也就是readSerialData(obj, desc);
private void readSerialData(Object obj, ObjectStreamClass desc)
throws IOException
{
// 獲取要序列化的類咏尝,包括實現(xiàn)了 Serializable 接口的父類
ObjectStreamClass.ClassDataSlot[] slots = desc.getClassDataLayout();
for (int i = 0; i < slots.length; i++) {
ObjectStreamClass slotDesc = slots[i].desc;
....
//執(zhí)行數(shù)據(jù)填充
defaultReadFields(obj, slotDesc);
....
}
}
private void defaultReadFields(Object obj, ObjectStreamClass desc)
throws IOException
{
....
int objHandle = passHandle;
//獲取類的需要填充數(shù)據(jù)的字段數(shù)組
ObjectStreamField[] fields = desc.getFields(false);
//生成對應(yīng)數(shù)量的值的數(shù)組
Object[] objVals = new Object[desc.getNumObjFields()];
int numPrimFields = fields.length - objVals.length;
//遍歷字段數(shù)組
for (int i = 0; i < objVals.length; i++) {
ObjectStreamField f = fields[numPrimFields + i];
// 關(guān)鍵點在這,對于字段的值其實遞歸調(diào)用了readObject0方法啸罢,所以咱們只需要針對readObject0做處理编检,不用擔(dān)心嵌套Serializable的情況
objVals[i] = readObject0(f.isUnshared());
if (f.getField() != null) {
handles.markDependency(objHandle, passHandle);
}
}
if (obj != null) {
//利用反射完成了值的填充
desc.setObjFieldValues(obj, objVals);
}
passHandle = objHandle;
}
好了其實到這java反序列化的源碼咱們就看的差不多了∪挪牛總結(jié)一下:
java反序列化的整體流程并不復(fù)雜允懂,首先通過分析序列化信息,讀取目標(biāo)類的類名衩匣、字段蕾总、serialVersionUID等關(guān)鍵信息粥航,讀取之后判斷讀取到的類本地是否存在,也就是在這當(dāng)這個類本地不存在時虛擬機(jī)會拋出一個ClassNotFoundException生百,這也就是咱們需要處理的關(guān)鍵點递雀。校驗完成之后,會利用反射生成一個空的目標(biāo)對象,然后通過遞歸調(diào)用readObject0方法,給這個對象的字段賦值妻坝,完成了一次反序列化流程。
瞞天過海杠输,無中生有
了解了反序列化流程之后,后續(xù)需要做的就十分清楚了秕衙。我們就是要繼承ObjectInputStream蠢甲,然后重寫resolveClass方法,在這個方法中通過Java的動態(tài)字節(jié)碼技術(shù)据忘,根據(jù)方法中傳入的ObjectStreamClass對象(包含目標(biāo)類的類信息)動態(tài)生成一個目標(biāo)類鹦牛。
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 方法根據(jù)傳入的對象,動態(tài)生成了目標(biāo)類的字節(jié)碼信息勇吊,并加載進(jìn)了內(nèi)存曼追。
Java的動態(tài)字節(jié)碼技術(shù),有很多方式實現(xiàn)汉规,比如我之前寫到的關(guān)于ASM技術(shù)的文章ASM——運行時/編譯時動態(tài)修改class源碼礼殊。不過ASM的api偏底層,需要對JVM虛擬機(jī)了解的深一些针史。在這里我采用了javassist框架晶伦,api封裝的更面向?qū)ο笠恍褂闷饋黹T檻較低啄枕。
private Class createClass(ObjectStreamClass desc) throws Exception {
//獲取類的全路徑
String name = desc.getName();
//獲取類的字段數(shù)組
ObjectStreamField[] fields = desc.getFields();
ClassPool pool = ClassPool.getDefault();
//根據(jù)類的全路徑生成一個空類的字節(jié)碼信息
CtClass cc = pool.makeClass(name);
CtClass interf = pool.getCtClass(Serializable.class.getName());
CtClass[] classes = new CtClass[]{interf};
//設(shè)置這個類實現(xiàn)Serializable接口
cc.setInterfaces(classes);
//遍歷字段數(shù)組婚陪,添加到剛生成的類的字節(jié)碼信息中
for (ObjectStreamField field : fields) {
String name1 = field.getName();
String name2 = field.getType().getName();
CtField param = new CtField(pool.get(name2), name1, cc);
//統(tǒng)一設(shè)置public修飾
param.setModifiers(Modifier.PUBLIC);
//添加到字節(jié)碼信息中
cc.addField(param);
}
//必須添加一個構(gòu)造參數(shù)
CtConstructor cons = new CtConstructor(new CtClass[]{}, cc);
//設(shè)置構(gòu)造函數(shù)的方法體
cons.setBody("{}");
//將構(gòu)造函數(shù)添加到字節(jié)碼信息中
cc.addConstructor(cons);
//生成serialVersionUID相關(guān)字段,并添加到字節(jié)碼信息中
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();
}
這里有幾點需要注意的:
- 生成的類除了字段數(shù)組里邊的字段频祝,還需要生成serialVersionUID字段泌参。
- 記得讓類實現(xiàn)Serializable接口。
- 自動生成的目標(biāo)類對比真正的目標(biāo)類會缺失除了構(gòu)造函數(shù)之外的所有方法常空,并且所有字段都為變成public修飾沽一,不過這個并不影響效果。
然后重新進(jìn)行反序列化操作:
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基本上就算是做完了。我本地只跑過有限的幾個測試用例攘残,可能不能覆蓋所有的情況拙友,但是大體上的思路應(yīng)該是沒問題的为狸。
Android Parcelable 反序列化
上邊說的是純java環(huán)境下的反序列化歼郭,在Android環(huán)境下還有一種序列化情況:實現(xiàn)Parcelable接口的類。在這簡單說一下Parcelable的反序列化過程辐棒。Parcelable的序列化與反序列化其實對比java的Serializable方式的序列化與反序列化病曾,你會發(fā)現(xiàn)Parcelable的序列化和反序列化的操作全部都是由自己實現(xiàn)的,而Serializable的序列化和反序列化則完全是由jdk實現(xiàn)的漾根,jdk這樣設(shè)計有個比較大的好處就是代碼侵入性低泰涂,Serializable接口是個空接口,只需要聲明實現(xiàn)一下辐怕,但是并不需要過多的改動代碼逼蒙。下面看個Parcelable的例子
public class Album implements Parcelable {
/**
* 負(fù)責(zé)反序列化
*/
private static final Creator<Album> CREATOR = new Creator<Album>() {
/**
* 從序列化對象中,獲取原始的對象
* @param source
* @return
*/
@Override
public Album createFromParcel(Parcel source) {
return new Album(source);
}
/**
* 創(chuàng)建指定長度的原始對象數(shù)組
* @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();
}
/**
* 描述
* 返回的是內(nèi)容的描述信息
* 只針對一些特殊的需要描述信息的對象,需要返回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 的實現(xiàn)方法還是稍微有點復(fù)雜的寄疏。里邊執(zhí)行序列化操作的方法是writeToParcel方法是牢,將對象的字段信息用固定的順序?qū)懭肓薖arcel 中。反序列化操作則是用參數(shù)為Parcel的構(gòu)造方法完成的陕截。這個構(gòu)造函數(shù)的方法體其實就是對應(yīng)著writeToParcel中的寫入順序驳棱,將信息再次從Parcel中讀出并賦值到類的字段中。writeToParcel和參數(shù)為Parcel的構(gòu)造方法中的讀寫順序必須是一一對應(yīng)农曲,絕對不能出錯的社搅。
結(jié)合我之前的文章Android爬取第三方app推送消息,可以知道Parcel中對于Parcelable數(shù)據(jù)存儲時其實只存儲了在writeToParcel方法中寫入的信息乳规。也就是并沒有像Serializable序列化過程中一并存儲下來的類相關(guān)信息形葬。也就導(dǎo)致了我們無法用處理Serializable反序列化的思路來處理Parcelable反序列化。實際上Parcelable反序列化的核心就是字段讀取的順序暮的,但凡一個字段讀取順序沒對上都可能會導(dǎo)致后續(xù)字段讀取的嚴(yán)重錯荷并。所以至少目前為止,是沒有很好的方法能夠批量解決Parcelable反序列化問題的青扔。