1. 程式人生 > >gradle學習二 利用javassist api修改class位元組碼

gradle學習二 利用javassist api修改class位元組碼

一 前言

Javassist (Java Programming Assistant) makes Java bytecode manipulation simple. It is a class library for editing bytecodes in Java; it enables Java programs to define a new class at runtime and to modify a class file when the JVM loads it. Unlike other similar bytecode editors, Javassist provides two levels of API: source level and bytecode level. If the users use the source-level API, they can edit a class file without knowledge of the specifications of the Java bytecode. The whole API is designed with only the vocabulary of the Java language. You can even specify inserted bytecode in the form of source text; Javassist compiles it on the fly. On the other hand, the bytecode-level API allows the users to directly edit a class file as other editors.

Javassist 提供了java類庫,用於方便操控Java位元組碼。功能包括:執行時建立java class,修改class。與其他同類工具(asm等)不同的是,Javassist提供了兩個層面的API:

1.java程式碼層

2.位元組碼層

通過java程式碼層,開發者即時對位元組碼不是很熟悉,也可以非常方便快速的完成位元組碼的修改。

二 示例

定義一個Bean類,程式碼如下

public class Bean {

    public static void show(String text) {
        System.out.print("hey man "
+ text); } public static void main(String[] argv) { show("add timing "); } }

直接執行,會列印
hey man add timing

如果想要統計show方法的執行時間,我們可以在寫程式碼的時候直接在show方法體開始和結束的地方加上計時的程式碼,這樣很簡單。但是假如我們期望動態修改show方法,直接修改Test.class類位元組碼,那麼可以通過Javassist來完成。具體方法如下:

import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import javassist.CannotCompileException; import javassist.ClassPool; import javassist.CtClass; import javassist.CtMethod; import javassist.CtNewMethod; import javassist.NotFoundException; public class JassistLearn { public static void main(String[] argv) { try { CtClass clas = ClassPool.getDefault().get("Bean"); addTiming(clas,"show"); } catch (NotFoundException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (CannotCompileException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } private static void addTiming(CtClass clas, String mname) throws NotFoundException, CannotCompileException, IOException { /* Retrieves methods with the specified name among the methods * declared in the class. Multiple methods with different parameters * may be returned.*/ CtMethod mold = clas.getDeclaredMethod(mname); //修改當前方法名稱為一個臨時名稱 String nname = mname+"Impl"; mold.setName(nname); /* Creates a copy of a method with a new name. * This method is provided for creating * a new method based on an existing method. * This is a convenience method for calling * {@link CtMethod#CtMethod(CtMethod, CtClass, ClassMap) this constructor}. * See the description of the constructor for particular behavior of the copying. * 拷貝一個新的方法,內容跟當前方法一致,方法名與當前方法原有名稱一致。後面主要修改新方法的 * 方法體的內容。 * */ CtMethod mnew = CtNewMethod.copy(mold, mname, clas, null); String type = mold.getReturnType().getName(); StringBuffer body = new StringBuffer(); //在新方法體開始處增加一個變數start標識當前執行時間點 body.append("{\nlong start = System.currentTimeMillis();\n"); if (!"void".equals(type)) { body.append(type + " result = "); } //原方法執行,格式為方法名+引數,($$)代表引數 body.append(nname + "($$);\n"); //在原方法執行後列印當前時間點。統計前後時間差可以算出原方法的執行耗時 body.append("System.out.println(\"Call to method " + mname + " took \" +\n (System.currentTimeMillis()-start) + " + "\" ms.\");\n"); if (!"void".equals(type)) { body.append("return result;\n"); } body.append("}"); //將方法體設定到新方法內 mnew.setBody(body.toString()); //將新方法新增到原方法所在類 clas.addMethod(mnew); //更改原方法所在class檔案,重新整理到磁碟,完成位元組碼修改 clas.writeFile("D:/eclipse/server/JavaSistLearn/bin"); } }

程式碼註釋包含了每一步對應的意義,執行過程總結一下主要是:
1. 通過ClassPool找到Bean.class類檔案
2. 修改show方法名稱為一個臨時名稱showImpl
3. 拷貝一個新方法,方法名是show,方法體與showImpl方法的內容相同
4. 修改新方法的方法體,包含三部分:方法開始新增start變數標記當前時刻;執行showImpl;計算當前時間與start的差值,算出showImpl的執行耗時
5. 將新方法新增到Bean.calss檔案中;同步更新Bean.class檔案到磁碟

通過上面五個步驟就完成了Bean.class位元組碼的修改

執行JassistLearn,列印資料如下:

hey man add timing Call to method show took 0 ms.

通過JD-DUI工具檢視Bean class檔案,程式碼如下:

import java.io.PrintStream;

public class Bean
{
  public static void showImpl(String text)
  {
    System.jdField_out_of_type_JavaIoPrintStream.print("hey man " + text);
  }

  public static void main(String[] argv) {
    show("add timing ");
  }

  public static void show(String paramString)
  {
    long l = System.currentTimeMillis();
    showImpl(paramString);
    System.jdField_out_of_type_JavaIoPrintStream.println("Call to method show took " + (System.currentTimeMillis() - l) + " ms.");
  }
}

三 總結

通過示例看到通過javassist api可以很簡便的在執行時修改class位元組碼,並且不要求使用者對位元組碼本身非常熟悉。利用這個特性可以實現aop,安卓應用熱修復等功能。不過最新github上的javassist.jar在使用過程會報關於StackWalker.StackFrame的錯誤,需要使用Jdk1.9。

四 參考