基于javassist處理java字節碼(一)

0 前言

為了用更少的代碼響應多樣的、易變的外部需求,java提供了運行時生成、修改、增強java類字節碼的能力,這一項能力在很多框架(如spring framework)、中間件(如hikariCP)軟件中大放異彩。相比于ASM(assemble的縮寫,名稱來自于C語言的asm關鍵字)、CGLIB(Code Generation LIBrary)等老牌且廣泛流行的字節碼查看和編輯工具,javassist(Java Programming Assistant)提供了更易于學習、使用的接口和方式來處理java字節碼。使用者通過自己非常熟悉的java語言代碼、基于類對象交互方式來操作字節碼,從而屏蔽了底層class文件的結構細節,就像開發普通程序一樣實現字節碼編輯的高級功能。javassist極大的提高了基于字節碼開發的效率,降低了學習曲線,且保證了較高的性能。性能僅略低于ASM,高于CGLIB,遠遠高于JDK自帶的動態代理(dynamic proxy,幾十倍的差距)

1 javassist包

要使用javassist,只要在項目中添加相應的依賴即可,maven依賴(當前最新版本是3.28.0-GA)如下:

        <!-- https://mvnrepository.com/artifact/org.javassist/javassist -->
        <dependency>
            <groupId>org.javassist</groupId>
            <artifactId>javassist</artifactId>
            <version>3.28.0-GA</version>
        </dependency>

2 創建一個類

下面的代碼創建了一個Animal類,并給這個類添加了一個name字段,以及name字段的setter()getter()方法,同時分別添加了一個無參和有參構造函數,添加了一個void printName()方法,實現打印Animal類對象name字段值的功能。最后將創建的類寫入class file文件

package com.javatest.javassist;

import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;
import javassist.CtField;
import javassist.CtMethod;
import javassist.CtNewMethod;
import javassist.Modifier;


public class JavassistMain {

    public static void main(String []args) {

        try {
         // 1. ClassPool相當于一個存儲、管理javassist class字節碼的容器
        ClassPool pool = ClassPool.getDefault();

        // 2. 創建一個空類,類的全限定名為 com.javatest.javassist.Animal
        CtClass cc = pool.makeClass("com.javatest.javassist.Animal");

        // 3. 新增一個字段
        CtField nameField = CtField.make("private String name;", cc);
        cc.addField(nameField);

        // 4. 添加無參的構造函數
        CtConstructor cons = new CtConstructor(new CtClass[]{}, cc);
        // 構造函數的內容
        cons.setBody("{name = \"tiger\";}");
        cc.addConstructor(cons);

        // 5. 添加有參的構造函數
        cons = new CtConstructor(new CtClass[]{pool.get("java.lang.String")}, cc);
        // $0=this / $1,$2,$3... 代表方法參數
        cons.setBody("{$0.name = $1;}");
        cc.addConstructor(cons);

        // 6. 添加字段的getter、setter方法
        cc.addMethod(CtNewMethod.setter("setName", nameField));
        cc.addMethod(CtNewMethod.getter("getName", nameField));

        // 7. 創建一個名為printName方法,無參數,無返回值,輸出name值
        CtMethod ctMethod = new CtMethod(CtClass.voidType, "printName", new CtClass[]{}, cc);
        ctMethod.setModifiers(Modifier.PUBLIC);
        ctMethod.setBody("{System.out.println(name);}");
        cc.addMethod(ctMethod);

        // 8. 生成class file文件,寫入項目當前工作目錄下
        cc.writeFile("./");

        } catch (Exception e) {
            // 
        }

    }
}

執行上面的代碼,會在當前目錄的子目錄com/javatest/javassist下生成一個名為Animal.class的文件,通過反編譯可查看class文件對應的代碼如下:

package com.javatest.javassist;

public class Animal {
    private String name;

    public Animal() {
        this.name = "tiger";
    }

    public Animal(String var1) {
        this.name = var1;
    }

    public void setName(String var1) {
        this.name = var1;
    }

    public String getName() {
        return this.name;
    }

    public void printName() {
        System.out.println(this.name);
    }
}

3 加載使用創建的class

class完成編輯后,我們可以像上面例子一樣將字節碼寫入class file中:

cc.writeFile("./");

也可以轉成字節碼序列,提供給應用程序其他部分使用(比如一個類加載器)或通過網絡發送給一個遠程服務,下面的例子跟上面的效果相同

    // 轉換成字節碼
    byte[] b = cc.toBytecode();
    OutputStream o = new FileOutputStream("./javassist");
    o.write(b);
    o.close();

或者通過當前線程的上下文類加載器直接將CtClass代表的class file加載到JVM中:

    Class clazz = cc.toClass();

這樣我們可以通過反射的方式創建類的實例和調用實例方法:

    Object tiger = clazz.newInstance();
    Method method = clazz.getMethod("printName");
    method.invoke(tiger);

但是通過反射調用一方面編碼比較繁瑣,性能也不理想,更好的方式是先定義一個接口:

package com.javatest.javassist;

public interface AnimalPrinter {

    void printName();
}

然后讓創建的class實現這個接口,在上面的例子中增加創建CtClass后設置它實現的接口:

    CtClass cc = pool.makeClass("com.javatest.javassist.Animal");
    cc.addInterface(pool.get("com.javatest.javassist.AnimalPrinter"));

然后便可以通過接口直接調用

    AnimalPrinter printer = (AnimalPrinter) clazz.newInstance();
    printer.printName();

4 javassist基本使用方法

我們知道,一個java類由類聲明本身、字段、構造函數、方法等元素組成。從上面的基本例子可以看出,javassist為java類的這些組成元素分別設計了相應的類CtClass、CtField、CtConstructor、CtMethod,我們就是通過這些類來處理java class字節碼的。這些類名的前綴Ctcompile time的縮寫,表示這些類代表的是javassist管理的編譯時的字節碼,需要加載到JVM中才能使用。

4.1 ClassPool

ClassPool用來存儲和管理class字節碼對象,它相當于一個容器,里面維護了一個Map,key為class的全限定名,value為CtClass對象。熟悉spring的朋友可以用spring容器這個概念來做類比。

我們可以通過靜態方法ClassPool.getDefault()獲取一個單例的ClassPool對象,也可以通過ClassPool pool = new ClassPool()創建新的ClassPool對象;如果需要,我們還可以創建一個ClassPool鏈,這樣可以重用一些ClassPool的內容,如下所示:

ClassPool parent = new ClassPool();
ClassPool child = new ClassPool(parent)

4.2 CtClass

4.2.1 創建CtClass對象,并添加到ClassPool中

我們可以通過ClassPool的makeClass()系列方法創建一個類的CtClass對象并自動添加到ClassPool中,同樣的,可以通過ClassPool的makeInterface()系列方法創建一個接口的CtClass對象。典型方法舉例如下:

CtClass makeClass(InputStream classfile);
CtClass makeClass(ClassFile classfile);
CtClass makeClass(String classname);
CtClass makeClass(String classname, CtClass superclass);
CtClass makeInterface(String name);
CtClass makeInterface(String name, CtClass superclass);

我們更經常使用ClassPool的get(String classname)方法獲取CtClass對象,get()方法傳入的參數是類的全限定名,ClassPool會先在自己當中查找相應的CtClass對象,如果不存在,則會到ClassPool配置的類搜索路徑(class search path)中查找相應的class file,然后創建CtClass對象并加載到ClassPool中。

當我們像上面的例子中那樣通過ClassPool pool = ClassPool.getDefault()方式獲取ClassPool對象時,pool中已經添加了系統類搜索路徑(system search path),系統類搜索路徑包括JVM platform庫、擴展庫、以及應用程序的CLASSPATH路徑,所以如果為pool添加了系統類搜索路徑,我們可以通過改變應用程序的CLASSPATH從而改變class搜索路徑。我們還可以ClassPool pool = new ClassPool(true)方式在創建ClassPool對象時為pool添加系統類搜索路徑。或者像下面這樣是同樣的效果:

ClassPool pool = new ClassPool();
// 為pool添加系統類搜索路徑
pool.appendSystemPath();

在某些環境下,如Web容器、OSGI等,應用有多個類加載器(ClassLoader),這時可能需要添加相應的類搜索路徑,我們還可以通過ClassPool提供的以下方法添加:

ClassPath appendClassPath(ClassPath cp);
ClassPath insertClassPath(ClassPath cp);
ClassPath appendClassPath(String pathname);
ClassPath insertClassPath(String pathname);

例如,假設我們有一個類實例Aninal cat,我們希望ClassPool能加載cat類加載器相應加載路徑下的class,可以如下為pool添加類搜索路徑:

ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new LoaderClassPath(cat.getClass().getClassLoader()));

4.2.2 CtClass基本操作

我們可以通過CtClass的setSuperclass(CtClass clazz)方法為類設置父類,通過setInterfaces(CtClass[] list)addInterface(CtClass anInterface)方法為類添加實現的接口,通過setModifiers(int mod)方法設置類的修飾符,通過setName()修改類名,例如:

ClassPool pool = ClassPool.getDefault();
CtClass animal = pool.makeClass("com.javatest.javassist.Animal");
CtClass cat = pool.makeClass("com.javatest.javassist.Cat");
cat.setSuperclass(animal);
cat.setModifiers(Modifier.PUBLIC | Modifier.FINAL);
// 將Cat類修改為Dog
cat.setName("Dog");

我們可以通過以下系列方法為CtClass添加和刪除字段、構造器、方法:

addField()
addConstructor()
addMethod()
removeField()
removeConstructor()
removeMethod()

當我們調用了CtClass對象的writeFile()toClass()toBytecode()等方法,javassist會凍結相應的CtClass;或者如果我們的CtClass已經設計好了,也可以主動通過freeze()方法將CtClass凍結,避免意外修改了CtClass。當然,如果我們確實需要重新修改CtClass,可以通過defrost()方法將CtClass解凍;如果創建的CtClass不再使用了,比如已經加載到了JVM中,可以通過detach()方法釋放CtClass在ClassPool中占用的資源。

ClassPool pool = ClassPool.getDefault();
CtClass animal = pool.makeClass("com.javatest.javassist.Animal");

animal.freeze();
// 無法執行,拋出異常
animal.setModifiers(Modifier.FINAL);

animal.defrost();
// 可正常執行
animal.setModifiers(Modifier.FINAL);

// 釋放相關資源
animal.detach();

4.3 CtField

我們可以通過CtField的靜態方法make()或new一個新的CtField實例來創建CtField對象,CtField的基本使用方法和說明如下例子所示:

    ClassPool pool = ClassPool.getDefault();
    CtClass animal = pool.makeClass("com.javatest.javassist.Animal");

    // 創建一個名字為name的field,可以看到跟我們手寫代碼是一模一樣的
    CtField field = CtField.make("private String name;", animal);

    // 下面兩行代碼的效果跟上面是一樣的
    // CtField field = new CtField(pool.get("java.lang.String"), "name", animal);
    // nameField.setModifiers(Modifier.PRIVATE);

    // 下面兩行的效果相當于刪除了Animal類的name字段,添加了一個類型為long的age字段
    // 修改字段的名字
    field.setName("age");
    // 修改字段的類型
    field.setType(CtClass.longType);

    // 添加到CtClass中
    animal.addField(field);
    // 添加到CtClass中, 并初始化值為60
    // animal.addField(field, "60L");
    // animal.addField(field, CtField.Initializer.constant(60L));

4.4 CtConstructor

下面的例子展示了為類添加一個無參構造器的方法,有參構造器只要提供一個參數CtClass列表即可:

    ClassPool pool = ClassPool.getDefault();
    CtClass animal = pool.makeClass("com.javatest.javassist.Animal");

    CtField field = CtField.make("private String name;", animal);
    animal.addField(field);

    // 創建一個無參構造器
    CtConstructor cons = new CtConstructor(new CtClass[]{}, animal);
    // 構造器的方法體,多次調用時,會整體替換已經存在的body內容
    cons.setBody("{name = \"Tom\";}");
    animal.addConstructor(cons);

    // 在構造器的body的最前面添加內容
    cons.insertBeforeBody("System.out.println(\"====this is constructor\");");

從上面的例子可以看出,構造器的body內容以及新插入的代碼跟我們平常開發代碼是一樣的。不過需要注意的是,setBody()的內容需要用{}包裹起來

CtNewConstructor工廠類則提供了一些方便的方法來創建構造函數:

// copy其他類的構造方法
CtConstructor copy(CtConstructor c, CtClass declaring,ClassMap map);
// 默認構造方法
CtConstructor defaultConstructor(CtClass declaring);
// make方法系列
CtConstructor make(String src, CtClass declaring);
CtConstructor make(CtClass[] parameters,CtClass[] exceptions, CtClass declaring);
CtConstructor make(CtClass[] parameters,CtClass[] exceptions,String body, CtClass declaring);
CtConstructor make(CtClass[] parameters,
                    CtClass[] exceptions, int howto,
                    CtMethod body, ConstParameter cparam,
                    CtClass declaring);

下面的例子創建的構造方法與上面是一樣的:

    CtConstructor cons = CtNewConstructor.make("public Animal() {name = \"Tom\";}", animal);
    animal.addConstructor(cons);

4.5 CtMethod

4.5.1 創建CtMethod

跟創建構造器類似,我們可以new CtMethod()或使用CtNewMethod的工廠方法創建新的類,稍不同的是方法需要提供方法名和返回值類型:

    ClassPool pool = ClassPool.getDefault();
    CtClass animal = pool.makeClass("com.javatest.javassist.Animal");

    // 創建一個 void printInfo() 方法
    CtMethod printInfo = new CtMethod(CtClass.voidType, "printInfo", new CtClass[]{}, animal);
    printInfo.setModifiers(Modifier.PUBLIC);
    printInfo.setBody("{System.out.println(\"====this is constructor\");}");
    animal.addMethod(printInfo);

CtNewMethod提供工廠方法主要有:

// 字段的getter、setter方法
CtMethod getter(String methodName, CtField field);
CtMethod setter(String methodName, CtField field);
// 抽象方法
CtMethod abstractMethod(CtClass returnType,
                        String mname,
                        CtClass[] parameters,
                        CtClass[] exceptions,
                        CtClass declaring);
CtMethod copy(CtMethod src, CtClass declaring,ClassMap map);
CtMethod copy(CtMethod src, String name, CtClass declaring,ClassMap map);
// make系列
CtMethod make(String src, CtClass declaring);
CtMethod make(String src, CtClass declaring,String delegateObj, String delegateMethod);
CtMethod make(CtClass returnType,
              String mname, CtClass[] parameters,
              CtClass[] exceptions,
              String body, CtClass declaring);
CtMethod make(int modifiers, CtClass returnType,
              String mname, CtClass[] parameters,
              CtClass[] exceptions,
              String body, CtClass declaring);

4.5.2 編輯CtMethod方法體內容

除了通過setBody()方法或CtNewMethod.make()系列工廠方法一次提供方法的全部內容,CtMethod提供了一系列豐富的用來編輯方法內容的方式,主要的幾個方法如下所示:

// 修改方法名字
setName();
// 添加方法參數
insertParameter();
addParameter();
// 在方法體中插入代碼
insertBefore();
insertAfter();
insertAt();
addCatch();

舉一個簡單的例子

    ClassPool pool = ClassPool.getDefault();
    CtClass animal = pool.makeClass("com.javatest.javassist.Animal");

    // 創建一個 void printInfo() 方法
    CtMethod printInfo = new CtMethod(CtClass.voidType, "printInfo", new CtClass[]{}, animal);
    printInfo.setModifiers(Modifier.PUBLIC);
    printInfo.setBody("{System.out.println(\"====this is a method\");}");
    animal.addMethod(printInfo);

    // 在方法入口處插入代碼
    printInfo.insertBefore("System.out.println(\"inserted at method entry point\");");
    // 在方法所有返回點插入代碼
    printInfo.insertAfter("System.out.println(\"inserted before method return\");");
    // 在指定行插入代碼
    // printInfo.insertAt(10, "System.out.println(\"insert at dedicated line\");");

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容