2025-02-15

 項目中有個需求:在不修改源代碼的情況下,替換某個類的引用為我們自己的實現。用一個類似的簡單例子來說明:

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; 


    這是一個簡單例子,從它上面可以看到“神奇”的表現,就像以前看很多框架的神奇之處一樣,到頭來都是背後做瞭很多不為人知的事情。同時為瞭這個例子,也學習到瞭很多以前很少關註的知識,這才是最大的收獲。

作者“每天一小步”
 

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *