項目中有個需求:在不修改源代碼的情況下,替換某個類的引用為我們自己的實現。用一個類似的簡單例子來說明:
Java代碼
public class CarHolder {
private Car car;
public CarHolder() {
init();
}
private void init() {
car = new Benz();
}
public void displayCarName() {
System.out.println(car.getCarName());
}
}
正常情況下執行這個類,當調用displayCarName這個方法時會得到"Hi, my name is Benz",但需求是當調用displayCarName時需要輸出"Yeah, I am BMW",其實是用BMW這個Car的實現類替換已經預先定義的Benz實現。
需求的本質是修改CarHolder的字節碼,讓其在運行期的行為與源代碼上看起來不一樣。修改字節碼有兩個時機:1. 靜態修改,把java文件編譯後的class文件替換成我們修改後的class文件,classLoader會加載我們的實現;2. 動態修改,通過特殊的classLoader加載源class文件並修改成我們想要的實現,或是在classLoader加載class文件時,通過JDK Instrumentation組件所提供的ClassFileTransformer機制修改字節碼(java.lang.instrument.ClassFileTransformer)。兩種策略的結果是一致的,JVM都能執行修改後的字節碼。
當前可以修改字節碼的組件有十幾種,有些體現在源代碼級別,有些體現在JVM執行指令級別。最終我選擇嘗試下BCEL,就是因為通過它可以熟悉class文件的組織結構和JVM指令集的細節,同時它還被加入到sun的內部JDK中,多個框架都在用它,也是學習的一種契機。
在使用BCEL之前,我翻瞭JVM規范裡相關的內容,大致理解瞭常量池的使用及JVM的常用指令。最終使用的代碼像這樣
Java代碼
public class StaticChangedCode {
public static void main(String[] args) {
try {
//以對象的方式操縱class文件
JavaClass clazz = Repository.lookupClass(CarHolder.class);
ClassGen classGen = new ClassGen(clazz);
//由於是替換舊類型,所以對於當前常量池中沒有的類型/方法/屬性等都得一一加入
//常量池項的引用索引,在方法指令中需要調用
ConstantPoolGen cPoolGen = classGen.getConstantPool();
int value = cPoolGen.addClass("bcel.changeimpl.BMW");
int methodIndex = cPoolGen
.addMethodref("bcel.changeimpl.BMW", "<init>", "()V");
int fieldIndex = cPoolGen
.addFieldref("bcel.changeimpl.CarHolder",
"car", "Lbcel/changeimpl/Car;");
//獲取想要操縱的方法,因為我知道init方法排行第二,所以這裡就寫死瞭
Method sourceMethod = classGen.getMethods()[1];
MethodGen methodGen = new MethodGen(sourceMethod, clazz.getClassName(), cPoolGen);
InstructionList instructionList = methodGen.getInstructionList();
//從原有的指令列表中刪去初始化Benz的那部分指令
InstructionHandle[] handles = instructionList.getInstructionHandles();
InstructionHandle from = handles[1];
InstructionHandle to = handles[4];
instructionList.delete(from, to);
//這裡開始添加初始化BMW的對象
//對象的創建指令有三步:1. new, 在heap上創建對象結構,分配內存;
//2. dup, 在操作數棧上保留對剛創建對象的引用,然後復制此引用;
//3. invokespecial,利用剛復制出的對象引用標識出對象,然後調用它的<init>方法
//經過上面三步後,對象就可以被使用瞭,此時做賦值動作,將當前對象賦給car這個變量
InstructionHandle newHandle = instructionList
.append(handles[0], new NEW(value));
InstructionHandle dumpHandle = instructionList
.append(newHandle, new DUP());
InstructionHandle initHandle = instructionList
.append(dumpHandle, new INVOKESPECIAL(methodIndex));
instructionList.append(initHandle, new PUTFIELD(fieldIndex));
//因為上面經歷過指令修改的過程,所以這裡用新指令的方法去替代舊指令的方法
classGen.replaceMethod(sourceMethod, methodGen.getMethod());
//工作完成,再次生成新的class文件
JavaClass target = classGen.getJavaClass();
target.dump("bin/bcel/changeimpl/CarHolder.class");
} catch (Exception e) {
// TODO: handle exception
}
}
}
之後當你再次運行CarHolder時,它的結果就改變瞭。上面是靜態修改的例子,如果是想要動態修改,那麼就利用自己的ClassFileTransformer,把byte數組轉化成可操縱的對象,然後與上面的流程一樣,最後再返回byte數組給classloader
Java代碼
public byte[] transform(ClassLoader loader, String className,
Class<?> classBeingRedefined, ProtectionDomain protectionDomain,
byte[] classfileBuffer) throws IllegalClassFormatException {
try {
//由byte數組生成JavaClass對象
InputStream inStream = new ByteArrayInputStream(classfileBuffer);
JavaClass jc = new ClassParser(inStream, className).parse();
//這裡與上面流程是一樣
//再次轉化成byte數組,然後給classloader
JavaClass final = ***;
return final.getBytes();
} catch (Exception e) {
// TODO: handle exception
}
return classfileBuffer;
}
這是一個簡單例子,從它上面可以看到“神奇”的表現,就像以前看很多框架的神奇之處一樣,到頭來都是背後做瞭很多不為人知的事情。同時為瞭這個例子,也學習到瞭很多以前很少關註的知識,這才是最大的收獲。
作者“每天一小步”