詳解Java字節碼編程之非常好用的javassist

一、Javassist入門

(一)Javassist是什麼

Javassist是可以動態編輯Java字節碼的類庫。它可以在Java程序運行時定義一個新的類,並加載到JVM中;還可以在JVM加載時修改一個類文件。Javassist使用戶不必關心字節碼相關的規范也是可以編輯類文件的。

(二)Javassist核心API

在Javassist中每個需要編輯的class都對應一個CtCLass實例,CtClass的含義是編譯時的類(compile time class),這些類會存儲在Class Pool中(Class poll是一個存儲CtClass對象的容器)。
CtClass中的CtField和CtMethod分別對應Java中的字段和方法。通過CtClass對象即可對類新增字段和修改方法等操作瞭。

image.png

(三)簡單示例

為瞭減少演示的復雜度,示例以及之後的操作,都在Maven項目下進行,因為我們可以直接引入依賴就可以達到我們導包的目的,很方便,不用再去下載jar包,然後自己手動導入瞭。

1、創建一個maven項目

如果你使用的是IDEA,可以像我一樣;如果是其他工具,可以自行百度,或者按照自己的經驗來創建即可。

image.png

2、創建一個測試類,代碼如下:

package com.ssdmbbl.javassist;

import javassist.*;

import java.io.IOException;
/**
 * @author zhenghui
 * @description: Javassist使用演示測試
 * @date 2021/4/6 6:38 下午
 */
public class JavassistTest {
    public static void main(String[] args) throws CannotCompileException, IOException {
        ClassPool cp = ClassPool.getDefault();
        CtClass ctClass = cp.makeClass("com.ssdmbbl.javassist.Hello");
        ctClass.writeFile("./");
    }
}

當運行這個代碼的時候,可以看到已經在項目的根目錄下創建瞭一個“com.ssdmbbl.javassist”包,在這個包下創建瞭“Hello.java”的java文件。

image.png

內容如下:

package com.ssdmbbl.javassist;

public class Hello {
    public Hello() {
    }
}

二、Javassist操作字節碼示例

回想一下,咱們如果對一個Java正常操作的話,大概存在哪些操作呢?

  • 1、咱們會對一個類添加字段;
  • 2、咱們會對一個類添加方法;

好像沒其他的瞭吧。其餘的就是在方法裡寫代碼瞭唄。

(一)新增一個方法

咱們繼續套用上面簡單示例的代碼,在此基礎之上進行新增一個方法。

新增方法的名字為”hello1″,傳遞兩個參數分別為int和double類型,並且沒有返回值。

package com.ssdmbbl.javassist;

import javassist.*;

import java.io.IOException;
import java.net.URL;

/**
 * @author zhenghui
 * @description: Javassist使用演示測試
 * @date 2021/4/6 6:38 下午
 */
public class JavassistTest2 {


    public static void main(String[] args) throws CannotCompileException, IOException {
		
        //找到本文件的路徑,與之保存在一起
        URL resource = JavassistTest2.class.getClassLoader().getResource("");
        String file = resource.getFile();
        System.out.println("文件存儲路徑:"+file);

        ClassPool cp = ClassPool.getDefault();
        CtClass ctClass = cp.makeClass("com.ssdmbbl.javassist.Hello");

        //創建一個類名為"hello",傳遞參數的順序為(int,double),沒有返回值的類
        /*
        CtMethod(...)源代碼:
        public CtMethod(CtClass returnType,//這個方法的返回值類型,
                        String mname, //(method name)方法的名字是什麼
                        CtClass[] parameters, //方法傳入的參數類型是什麼
                        CtClass declaring //添加到哪個類中
                        ) {....}
         */
        CtMethod ctMethod = new CtMethod(CtClass.voidType, "hello", new CtClass[]{CtClass.intType, CtClass.doubleType}, ctClass);
		//設置hello方法的權限為public
        ctMethod.setModifiers(Modifier.PUBLIC);
        
        //向ctClass中添加這個方法
        ctClass.addMethod(ctMethod);
        
        //寫入本地
        ctClass.writeFile(file);
    }
}

執行後,就可以查看生成的代碼瞭:

可以看到,我們並沒有指定參數的名字,也會給生成var1、var2依次類推的名字。

var1和var2其實class變量表中存放的名字。

package com.ssdmbbl.javassist;

public class Hello {
    public void hello1(int var1, double var2) {
    }

    public Hello() {
    }
}

可以設置的返回值類型:

public static CtClass booleanType;
public static CtClass charType;
public static CtClass byteType;
public static CtClass shortType;
public static CtClass intType;
public static CtClass longType;
public static CtClass floatType;
public static CtClass doubleType;
public static CtClass voidType;

(二)新增一個變量

URL resource = JavassistTest2.class.getClassLoader().getResource("");
String file = resource.getFile();
System.out.println("文件存儲路徑:"+file);

ClassPool cp = ClassPool.getDefault();
CtClass ctClass = cp.makeClass("com.ssdmbbl.javassist.Hello");

//添加一個hello1的方法
CtMethod ctMethod = new CtMethod(CtClass.voidType, "hello1", new CtClass[]{CtClass.intType, CtClass.doubleType}, ctClass);
ctMethod.setModifiers(Modifier.PUBLIC);
ctClass.addMethod(ctMethod);

//添加一個int類型的,名字為value的變量
CtField ctField = new CtField(CtClass.intType,"value",ctClass);
ctField.setModifiers(Modifier.PRIVATE);
ctClass.addField(ctField);


ctClass.writeFile(file);

那麼執行後的內容就是如下瞭:

package com.ssdmbbl.javassist;

public class Hello {
    private int value;

    public void hello1(int var1, double var2) {
    }

    public Hello() {
    }
}

(三)給變量新增get和set方法

代碼修改如下:

   public static void main(String[] args) throws CannotCompileException, IOException {

        URL resource = JavassistTest2.class.getClassLoader().getResource("");
        String file = resource.getFile();
        System.out.println("文件存儲路徑:"+file);

        ClassPool cp = ClassPool.getDefault();
        CtClass ctClass = cp.makeClass("com.ssdmbbl.javassist.Hello");

        //添加一個hello1的方法
        CtMethod ctMethod = new CtMethod(CtClass.voidType, "hello1", new CtClass[]{CtClass.intType, CtClass.doubleType}, ctClass);
        ctMethod.setModifiers(Modifier.PUBLIC);
        ctClass.addMethod(ctMethod);

        //添加一個int類型的,名字為value的變量
        CtField ctField = new CtField(CtClass.intType,"value",ctClass);
        ctField.setModifiers(Modifier.PRIVATE);
        ctClass.addField(ctField);

        //為value變量添加set方法
        CtMethod setValue = new CtMethod(CtClass.voidType, "setValue", new CtClass[]{CtClass.intType}, ctClass);
        setValue.setModifiers(Modifier.PUBLIC);
        ctClass.addMethod(setValue);

        //為value變量添加get方法
        CtMethod getValue = new CtMethod(CtClass.intType, "getValue", new CtClass[]{}, ctClass);
        getValue.setModifiers(Modifier.PUBLIC);
        ctClass.addMethod(getValue);


        ctClass.writeFile(file);
    }

執行效果:

package com.ssdmbbl.javassist;

public class Hello {
    private int value;

    public void hello1(int var1, double var2) {
    }

    public void setValue(int var1) {
    }

    public int getValue() {
    }

    public Hello() {
    }
}

(四)給方法內部添加代碼

你是不是很好奇,set和get方法內部並沒有代碼,當程序運行的時候,肯定會出錯的。
下面就來看一下。

我們預想的結果:

    private int value;

    public void setValue(int var1) {
        this.value = var1
    }

    public int getValue() {
        return this.value;
    }

修改如下:

//為value變量添加set方法
CtMethod setValue = new CtMethod(CtClass.voidType, "setValue", new CtClass[]{CtClass.intType}, ctClass);
setValue.setModifiers(Modifier.PUBLIC);
//設置方法體
setValue.setBody("this.value = var1;");

ctClass.addMethod(setValue);

//為value變量添加get方法
CtMethod getValue = new CtMethod(CtClass.intType, "getValue", new CtClass[]{}, ctClass);
getValue.setModifiers(Modifier.PUBLIC);
//設置方法體
getValue.setBody("return this.value;");
ctClass.addMethod(getValue);

很倒黴,說找不到這個var1這個變量

報錯的堆棧信息如下:

Exception in thread “main” javassist.CannotCompileException: [source error] no such field: var1
at javassist.CtBehavior.setBody(CtBehavior.java:474)
at javassist.CtBehavior.setBody(CtBehavior.java:440)
at com.ssdmbbl.javassist.JavassistTest2.main(JavassistTest2.java:41)
Caused by: compile error: no such field: var1

這個原因我們前面其實提到瞭,因為在編譯的時候,會把變量名抹掉,傳遞的參數會依次在局部變量表中的順序。

如果傳遞:

public void test(int a,int b,int c){
    ...
}

那麼a,b,c就對應本地變量表中的1,2,3的位置。

 —— ——
|a|1|
|b|2|
|c|3|
 —— ——

那麼我們獲取變量時就不能使用原始的名字瞭,在Javassist中訪問方法中的參數使用的是$1, 2 , 2, 2,…,而不是直接使用原始的名字。

我們修改如下:

//為value變量添加set方法
CtMethod setValue = new CtMethod(CtClass.voidType, "setValue", new CtClass[]{CtClass.intType}, ctClass);
setValue.setModifiers(Modifier.PUBLIC);
//設置方法體
setValue.setBody("this.value = $1;");

ctClass.addMethod(setValue);

//為value變量添加get方法
CtMethod getValue = new CtMethod(CtClass.intType, "getValue", new CtClass[]{}, ctClass);
getValue.setModifiers(Modifier.PUBLIC);
//設置方法體
getValue.setBody("return this.value;");
ctClass.addMethod(getValue);

結果成功瞭:

public class Hello {
    private int value;

    public void hello1(int var1, double var2) {
    }

    public void setValue(int var1) {
        this.value = var1;
    }

    public int getValue() {
        return this.value;
    }

    public Hello() {
    }
}

再來一個:修改hello1方法體,使傳遞的兩個參數相加然後賦值給value;

//添加一個hello1的方法
CtMethod ctMethod = new CtMethod(CtClass.voidType, "hello1", new CtClass[]{CtClass.intType, CtClass.doubleType}, ctClass);
ctMethod.setModifiers(Modifier.PUBLIC);
ctMethod.setBody("this.value = $1 + $2;");
ctClass.addMethod(ctMethod);

執行結果如下:

因為我們value是int,$1是int,$2是double,所以做瞭強制轉型處理。

public void hello1(int var1, double var2) {
    this.value = (int)((double)var1 + var2);
}

(五)在方法體的前後分別插入代碼

測試代碼如下:

//添加一個hello1的方法
CtMethod ctMethod = new CtMethod(CtClass.voidType, "hello1", new CtClass[]{CtClass.intType, CtClass.doubleType}, ctClass);
ctMethod.setModifiers(Modifier.PUBLIC);
ctMethod.setBody("this.value = $1 + $2;");
ctMethod.insertBefore("System.out.println(\"我在前面插入瞭:\"+$1);");
ctMethod.insertAfter("System.out.println(\"我在最後插入瞭:\"+$1);");
ctClass.addMethod(ctMethod);

結果如下:

public void hello1(int var1, double var2) {
    System.out.println("我在前面插入瞭:" + var1);
    this.value = (int)((double)var1 + var2);
    Object var5 = null;
    System.out.println("我在最後插入瞭:" + var1);
}

三、Javassist中的一些特殊參數示例講解

在官方文檔中看到有幾個比較特殊的標識符,還有幾個比較特殊的標識符需要瞭解。

標識符 作用
$0、$1、$2、 3 、 3、 3、… this和方法參數(1-N是方法參數的順序)
$args 方法參數數組,類型為Object[]
$$ 所有方法參數,例如:m($$)相當於m($1,$2,…)
$cflow(…) control flow 變量
$r 返回結果的類型,在強制轉換表達式中使用。
$w 包裝器類型,在強制轉換表達式中使用。
$_ 返回的結果值
$sig 類型為java.lang.Class的參數類型對象數組
$type 類型為java.lang.Class的返回值類型
$class 類型為java.lang.Class的正在修改的類

下面咱們來一起分別來看一下,分析一下,怎麼用,有什麼用吧。

隻介紹幾個常見和常用到的吧。
有興趣的話,剩下的可以看官方文檔:http://www.javassist.org/tutorial/tutorial2.html

(一)$0,$1,$2,…

這個其實咱們已經在上面用到過瞭,再來細說一下吧。$0代表this,$1, 2 , 2, 2,…,依次對應方法中參數的順序。
如果有:

public void test(int a,int b,int c){}

那麼如果想引用a和b和c的話,需要這樣:

ctMethod.setBody("return $1 + $2 + $3;");

對瞭還有:靜態方法是沒有$0的,所以靜態方法下$0是不可用的。

(二)$args

$args變量表示所有參數的數組,它是一個Object類型的數組(new Object[]{…}),如果參數中有原始類型的參數,會被轉換成對應的包裝類型。比如原始數據類型為int,則會被轉換成java.lang.Integer,然後存儲在args中。

例如我們測試代碼如下:

CtMethod ctMethod = new CtMethod(CtClass.voidType, "hello1", new CtClass[]{CtClass.intType, CtClass.doubleType}, ctClass);
ctMethod.setModifiers(Modifier.PUBLIC);
ctMethod.setBody("System.out.println($args);");

編譯後的結果如下:

public void hello1(int var1, double var2) {
    System.out.println(new Object[]{new Integer(var1), new Double(var2)});
}

(三)$$

變量 是 所 有 參 數 的 縮 寫 , 參 數 用 逗 號 分 割 , 例 如 : m ( 是所有參數的縮寫,參數用逗號分割,例如:m( 是所有參數的縮寫,參數用逗號分割,例如:m()相當於:m($1,$2,$3,…)。

如何使用呢?

我們經常做一些代碼優化,把較為復雜的方法內部的邏輯,提煉出來,提煉到一個公共的方法裡。

或者說一個方法調用另一個方法,這兩個方法傳遞的參數是一樣的時候就用到瞭。

例如原java:

public Object m1(String name,String age){
	...省略10000行代碼邏輯
}

提煉後的:

提煉出來的代碼,我們也可以在其他地方使用(所謂的公共的代碼)。

public Object m1(String name,String age){
    ...省略20行代碼邏輯
    return m2(name,age);
}

private Object m2(String name,String age){
	...
}

那麼我們就造一個這個場景來說明一下用法吧。

簡單點來說,就是一個方法調用另一個方法,傳遞全參數時。

CtMethod hello2 = new CtMethod(CtClass.voidType, "hello2", new CtClass[]{CtClass.intType, CtClass.doubleType}, ctClass);
hello2.setModifiers(Modifier.PUBLIC);
hello2.setBody("this.value = $1 + $2;");
ctClass.addMethod(hello2);

//添加一個hello1的方法
CtMethod hello1 = new CtMethod(CtClass.voidType, "hello1", new CtClass[]{CtClass.intType, CtClass.doubleType}, ctClass);
hello1.setModifiers(Modifier.PUBLIC);
hello1.setBody("this.value = $1 + $2;");
hello1.insertAfter("hello2($$);");

可以看到我們hello1調用hello2時,需要傳遞全部參數。此時即可寫成$$方法,當然瞭也可以寫成hello2($1,$2)。

編譯後的結果:

public void hello2(int var1, double var2) {
    this.value = (int)((double)var1 + var2);
}

public void hello1(int var1, double var2) {
    this.value = (int)((double)var1 + var2);
    Object var5 = null;
    this.hello2(var1, var2);
}

(四)$cflow(…)

$cflow 的全名為:control flow,這是一個隻讀變量,返回指定方法遞歸調用的深度。

我們以計算n的斐波拉契數列為例,來演示一下如何使用。

我們正確的遞歸算法代碼如下:

public int f(int n){
	if(n <= 1){
    	return n;
    }
    return f(n-1) + f(n - 2)
 }

對於上面這段代碼,我們可以這樣寫:

CtMethod f = new CtMethod(CtClass.intType,"f", new CtClass[]{CtClass.intType}, ctClass);
f.setBody("{if($1 <= 1){" +
          "     return $1;" +
          "}" +
          "return f($1 - 1) + f( $1 - 2);}");
f.setModifiers(Modifier.PUBLIC);

編譯後的:

public int f(int var1) {
    return var1 <= 1 ? var1 : this.f(var1 - 1) + this.f(var1 - 2);
}

那麼我們隻想在遞歸到前n次的時候打印log,我們該怎麼做呢。

例如,我們下面寫的是”$cflow(f) == 1″時

CtMethod f = new CtMethod(CtClass.intType,"f", new CtClass[]{CtClass.intType}, ctClass);
f.setBody("{if($1 <= 1){" +
          "     return $1;" +
          "}" +
          "return f($1 - 1) + f( $1 - 2);}");
f.setModifiers(Modifier.PUBLIC);

//在代碼body的前面插入
f.useCflow("f");
f.insertBefore("if($cflow(f) == 1){" +
               "               System.out.println(\"我執行瞭,此時的n是:\"+$1);" +
               "           }");

編譯後的代碼:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package com.ssdmbbl.javassist;

import javassist.runtime.Cflow;

public class Hello {
    public static Cflow _cflow$0 = new Cflow();

    public int f(int var1) {
        if (_cflow$0.value() == 1) {
            System.out.println("我執行到來第2次,此時的n是:" + var1);
        }

        boolean var6 = false;

        int var10000;
        try {
            var6 = true;
            _cflow$0.enter();
            if (var1 <= 1) {
                var10000 = var1;
                var6 = false;
            } else {
                var10000 = this.f(var1 - 1) + this.f(var1 - 2);
                var6 = false;
            }
        } finally {
            if (var6) {
                boolean var3 = false;
                _cflow$0.exit();
            }
        }

        int var8 = var10000;
        _cflow$0.exit();
        return var8;
    }

    public Hello() {
    }
}

通過查看源碼,發先關鍵的一個地方是調用瞭Cflow對象的enter方法:

public static Cflow _cflow$0 = new Cflow();

...
    
    
_cflow$0.enter();

點進enter()的內部發現,調用瞭get().inc()方法:

public class Cflow extends ThreadLocal<Cflow.Depth> {
    protected static class Depth {
        private int depth;
        Depth() { depth = 0; }
        int value() { return depth; }
        void inc() { ++depth; }
        void dec() { --depth; }
    }

    @Override
    protected synchronized Depth initialValue() {
        return new Depth();
    }

    /**
     * Increments the counter.
     */
    public void enter() { get().inc(); }

    /**
     * Decrements the counter.
     */
    public void exit() { get().dec(); }

    /**
     * Returns the value of the counter.
     */
    public int value() { return get().value(); }
}

而inc()方法控制著一個全局變量的增加操作。

void inc() { ++depth; }

boolean var6 = false;相當於是一個開關,控制著是否開始和結束。

當參數var1<=1時,即結束

if (var1 <= 1) {
    var10000 = var1;
    var6 = false;
}

我們可以使用反射來驗證一下,測試代碼如下:

package com.ssdmbbl.javassist;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

/**
 * @author zhenghui
 * @description:
 * @date 2021/4/8 10:20 上午
 */
public class Test {

    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InstantiationException, InvocationTargetException {

        Class<?> aClass = Class.forName("com.ssdmbbl.javassist.Hello");

        //初始化這個類
        Object obj = aClass.newInstance();

        //獲取所有的方法
        Method[] methods = aClass.getMethods();
        //遍歷所有的方法
        for (Method method : methods) {
            //當方法為f的時候,進行調用
            if(method.getName().equals("f")){
                //調用並傳遞參數為5,即f(5)
                method.invoke(obj,5);
            }
        }
    }
}

執行的結果:

為什麼是2次呢?原因上面也說瞭:if(var1 <= 1){…},所以是

我執行瞭,此時的n是:4
我執行瞭,此時的n是:3

相關鏈接

1、javassist的API接口文檔

http://www.javassist.org/html/index.html

2、javassist的github開源地址

https://github.com/jboss-javassist/javassist

3、javassist的官網

http://www.javassist.org/

4、javassist官方的英文教程

http://www.javassist.org/tutorial/tutorial.html

到此這篇關於詳解Java字節碼編程之非常好用的javassist 的文章就介紹到這瞭,更多相關Java javassist 內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀:

    None Found