首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

使用 BCEL 直接修改方法字節碼:實戰指南

     編輯:Java 相關知識

前言

在上一篇文章中,我們介紹了通過生成新方法來調用原方法的方式實現字節碼增強。本文將進一步探討如何直接在原方法上插入指令,這種方式更加靈活,也更貼近實際 AOP 開發場景。

目標代碼對比

我們以一個簡單的 StringBuilder 類為例,展示修改前後的對比:

修改前的原始方法

public class StringBuilder {
    private String buildString(int length) {
        String result = "";
        for (int i = 0; i < length; i++) {
            result += (char)(i%26 + 'a');
        }
        System.out.println(result);
        return result;
    }
    // ... main 方法省略
}

修改後的目標方法

public class StringBuilder {
    private String buildString(int length) {
        System.out.println("start start start");
        long starttime = System.currentTimeMillis();

        String result = "";
        for (int i = 0; i < length; i++) {
            result += (char)(i%26 + 'a');
        }
        System.out.println(result);

        System.out.println("Call to buildString$impl took " +
            (System.currentTimeMillis()-starttime) + " ms.");
        System.out.println("end end end end");

        return result;
    }
    // ... main 方法省略
}

核心需求是在方法入口插入計時開始代碼,在返回前插入計時結束並輸出耗時的代碼。

BCEL 核心實現代碼

以下是完整的 BCELTiming2 類,演示如何操作字節碼:

import java.io.FileOutputStream;
import java.util.Iterator;
import org.apache.bcel.Constants;
import org.apache.bcel.classfile.ClassParser;
import org.apache.bcel.classfile.JavaClass;
import org.apache.bcel.classfile.LineNumberTable;
import org.apache.bcel.classfile.LocalVariableTable;
import org.apache.bcel.classfile.Method;
import org.apache.bcel.generic.ClassGen;
import org.apache.bcel.generic.ConstantPoolGen;
import org.apache.bcel.generic.InstructionConstants;
import org.apache.bcel.generic.InstructionFactory;
import org.apache.bcel.generic.InstructionHandle;
import org.apache.bcel.generic.InstructionList;
import org.apache.bcel.generic.InvokeInstruction;
import org.apache.bcel.generic.LocalVariableGen;
import org.apache.bcel.generic.MethodGen;
import org.apache.bcel.generic.ObjectType;
import org.apache.bcel.generic.PUSH;
import org.apache.bcel.generic.Type;

public class BCELTiming2 {

    private static void modifyWrapper(ClassGen cgen, Method method) {
        InstructionFactory ifact = new InstructionFactory(cgen);
        ConstantPoolGen pgen = cgen.getConstantPool();
        String cname = cgen.getClassName();

        // 將原方法包裝為 MethodGen 以便操作
        MethodGen wrapgen = new MethodGen(method, cname, pgen);

        // 先移除原方法,稍後重新添加修改後的版本
        cgen.removeMethod(method);

        // 獲取指令列表
        InstructionList ilist = wrapgen.getInstructionList();

        // 計算局部變量表槽位,非靜態方法 slot 0 存 this
        Type returnType = wrapgen.getReturnType();
        Type[] types = wrapgen.getArgumentTypes();
        int slot = wrapgen.isStatic() ? 0 : 1;
        for (int i = 0; i < types.length; i++) {
            slot += types[i].getSize(); // long/double 佔 2 個槽位
        }

        // 自動分配新局部變量索引(用於存儲 starttime)
        LocalVariableGen lvg = wrapgen.addLocalVariable("starttime", Type.LONG, null, null);

        // ========== 1. 在方法入口插入計時開始代碼 ==========
        InstructionList printlnList = ifact.createPrintln("start test start test start test");
        InvokeInstruction invokestatic = ifact.createInvoke(
            "java.lang.System", "currentTimeMillis", Type.LONG, Type.NO_ARGS, Constants.INVOKESTATIC);
        
        InstructionHandle firstHandle = printlnList.append(invokestatic);
        // 將當前時間存入新分配的局部變量
        printlnList.append(InstructionFactory.createStore(Type.LONG, lvg.getIndex()));
        
        // 插入到方法最開始
        ilist.insert(printlnList);

        // ========== 2. 在方法返回前插入計時結束代碼 ==========
        InstructionHandle insertPosition = null;
        if (returnType.getType() != Type.VOID.getType()) {
            // 有返回值:插在最後一條 return 指令之前
            insertPosition = ilist.getEnd().getPrev();
        } else {
            // void 方法:直接插在最後
            insertPosition = ilist.getEnd();
        }

        InstructionList tempList = new InstructionList();
        // 獲取 System.out
        tempList.append(ifact.createFieldAccess("java.lang.System", "out",
            new ObjectType("java.io.PrintStream"), Constants.GETSTATIC));
        tempList.append(InstructionFactory.DUP);
        tempList.append(InstructionFactory.DUP);

        // 輸出 "Call to method xxx took "
        String text = "Call to method " + wrapgen.getName() + " took ";
        tempList.append(new PUSH(pgen, text));
        tempList.append(ifact.createInvoke("java.io.PrintStream", "print",
            Type.VOID, new Type[]{Type.STRING}, Constants.INVOKEVIRTUAL));

        // 獲取當前時間
        tempList.append(ifact.createInvoke("java.lang.System", "currentTimeMillis",
            Type.LONG, Type.NO_ARGS, Constants.INVOKESTATIC));
        // 獲取之前存儲的 starttime
        tempList.append(InstructionFactory.createLoad(Type.LONG, lvg.getIndex()));
        // 相減得到耗時
        tempList.append(InstructionConstants.LSUB);
        // 輸出耗時
        tempList.append(ifact.createInvoke("java.io.PrintStream", "print",
            Type.VOID, new Type[]{Type.LONG}, Constants.INVOKEVIRTUAL));
        // 輸出 " ms."
        tempList.append(new PUSH(pgen, " ms."));
        tempList.append(ifact.createInvoke("java.io.PrintStream", "println",
            Type.VOID, new Type[]{Type.STRING}, Constants.INVOKEVIRTUAL));

        // 最後輸出結束標記
        tempList.append(ifact.createPrintln("end end end end end end end end end end"));

        // 插入到返回指令之前
        ilist.insert(insertPosition, tempList);

        // ========== 3. 收尾工作 ==========
        wrapgen.stripAttributes(false);
        wrapgen.setMaxStack();
        wrapgen.setMaxLocals();
        cgen.addMethod(wrapgen.getMethod());

        ilist.dispose();
    }

    public static void main(String[] args) {
        // 測試入口:指定 class 文件路徑和方法名
        args = new String[]{
            "D:\\java to eclipse\\javaeclipsestudy\\workspace\\BCELTest\\bin\\StringBuilder.class",
            "buildString"
        };
        String targetClassfile = "StringBuilder.class";

        if (args.length == 2 && args[0].endsWith(".class")) {
            try {
                JavaClass jclas = new ClassParser(args[0]).parse();
                ClassGen cgen = new ClassGen(jclas);
                Method[] methods = jclas.getMethods();

                int index;
                for (index = 0; index < methods.length; index++) {
                    if (methods[index].getName().equals(args[1])) break;
                }

                if (index < methods.length) {
                    modifyWrapper(cgen, methods[index]);
                    FileOutputStream fos = new FileOutputStream(targetClassfile);
                    cgen.getJavaClass().dump(fos);
                    fos.close();
                    System.out.println("修改完成,已輸出至 " + targetClassfile);
                } else {
                    System.err.println("Method " + args[1] + " not found in " + args[0]);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        } else {
            System.out.println("usage: class-file method-name");
        }
    }
}

關鍵技術點解析

1. 局部變量索引計算

非靜態方法的第 0 個槽位存放 this,參數按順序排列,long/double 佔 2 個槽位。BCEL 的 addLocalVariable 會自動分配可用索引,避免衝突。

2. 指令插入策略

  • 方法入口:直接 ilist.insert(printlnList) 插在第一條指令前。
  • 方法出口:需區分有無返回值。有返回值時,最後一條指令是 return,需插在其前一條;void 方法則插在末尾。

3. 棧幀平衡

插入指令後必須調用 setMaxStack() 和 setMaxLocals() 重新計算最大棧深和局部變量表大小,否則驗證器會報錯。

總結

直接修改原方法字節碼比生成包裝方法更高效,且不改變方法簽名。掌握 InstructionList、InstructionFactory、局部變量管理是核心。實際項目中常用於性能監控、日誌埋點、事務管理等切面編程場景。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved