Javassist基本用法彙總

最近項目需要對基礎架構做增強,需要基於字節碼在不侵入原有代碼的情況下實現, 故把javassist的基本用法過了一遍。這篇博客就是把主要講講爲什麼要用javassist以及javassist的基本用法。

1.爲什麼要使用javassist(上手成本低)

基於字節碼增強的框架有兩個ASM和javassit,下面是兩個框架的特點以及對比

Javassist & ASM 對比

1.Javassist源代碼級API比ASM中實際的字節碼操作更容易使用
2.Javassist在複雜的字節碼級操作上提供了更高級別的抽象層。Javassist源代碼級API只需要很少的字節碼知識,甚至不需要任何實際字節碼知識,因此實現起來更容易、更快。
3.Javassist使用反射機制,這使得它比運行時使用Classworking技術的ASM慢。
總的來說ASM比Javassist快得多,並且提供了更好的性能。Javassist使用Java源代碼的簡化版本,然後將其編譯成字節碼。這使得Javassist非常容易使用,但是它也將字節碼的使用限制在Javassist源代碼的限制之內。
總之,如果有人需要更簡單的方法來動態操作或創建Java類,那麼應該使用Javassist API 。如果需要注重性能地方,應該使用ASM庫。

2. javassist基本用法(基於3.28.0-GA的版本) 

Javassist 是一個開源的分析、編輯和創建Java字節碼的類庫. 其主要優點在於簡單快速. 直接使用 java 編碼的形式, 而不需要了解虛擬機指令, 就能動態改變類的結構, 或者動態生成類.

Javassist中最爲重要的是ClassPool, CtClass, CtMethod以及CtField這幾個類.

ClassPool: 一個基於Hashtable實現的CtClass對象容器, 其中鍵是類名稱, 值是表示該類的CtClass對象
CtClass: CtClass表示類, 一個CtClass(編譯時類)對象可以處理一個class文件, 這些CtClass對象可以從ClassPool獲得
CtMethods: 表示類中的方法
CtFields: 表示類中的字段 

2.1 ClassPool對象

2.1.1 ClassPool的創建

// 獲取ClassPool對象, 使用系統默認類路徑
ClassPool pool = new ClassPool(true);
// 效果與 new ClassPool(true) 一致
ClassPool pool1 = ClassPool.getDefault();

爲減少ClassPool可能導致的內存消耗. 可以從ClassPool中刪除不必要的CtClass對象. 或者每次創建新的ClassPool對象.

// 從ClassPool中刪除CtClass對象
ctClass.detach();
// 也可以每次創建一個新的ClassPool, 而不是ClassPool.getDefault(), 避免內存溢出
ClassPool pool2 = new ClassPool(true);

2.1.2 classpath

通過 ClassPool.getDefault()獲取的ClassPool使用JVM的classpath.在Tomcat等Web服務器運行時, 服務器會使用多個類加載器作爲系統類加載器, 這可能導致ClassPool可能無法找到用戶的類. 這時, ClassPool須添加額外的classpath才能搜索到用戶的類.

// 將classpath插入到指定classpath之前
pool.insertClassPath(new ClassClassPath(this.getClass()));
// 將classpath添加到指定classpath之後
pool.appendClassPath(new ClassClassPath(this.getClass()));
// 將一個目錄作爲classpath
pool.insertClassPath("/xxx/lib");

2.2 CtClass對象

2.2.1 獲取CtClass

// 通過類名獲取 CtClass, 未找到會拋出異常
CtClass ctClass = pool.get("com.kawa.ssist.JustRun");
// 通過類名獲取 CtClass, 未找到返回 null, 不會拋出異常
CtClass ctClass1 = pool.getOrNull("com.kawa.ssist.JustRun"); 

2.2.2 創建CtClass

// 複製一個類
CtClass ctClass2 = pool.getAndRename("com.kawa.ssist.JustRun", "com.kawa.ssist.JustRunq");
// 創建一個新類
CtClass ctClass3 = pool.makeClass("com.kawa.ssist.JustRuna");
// 通過class文件創建一個新類
CtClass ctClass4 = pool.makeClass(new FileInputStream(new File("/home/un/test/JustRun.class")));

2.2.3 CtClass基礎信息

// 類名
String simpleName = ctClass.getSimpleName();
// 類全名
String name = ctClass.getName();
// 包名
String packageName = ctClass.getPackageName();
// 接口
CtClass[] interfaces = ctClass.getInterfaces();
// 繼承類
CtClass superclass = ctClass.getSuperclass();
// 獲取類方法
CtMethod ctMethod = ctClass.getDeclaredMethod("getName()", new CtClass[] {pool.get(String.class.getName()), pool.get(String.class.getName())});
// 獲取類字段
CtField ctField = ctClass.getField("name");
// 判斷數組類型
ctClass.isArray();
// 判斷原生類型
ctClass.isPrimitive();
// 判斷接口類型
ctClass.isInterface();
// 判斷枚舉類型
ctClass.isEnum();
// 判斷註解類型
ctClass.isAnnotation();
// 凍結一個類,使其不可修改
ctClass.freeze () 
// 判斷一個類是否已被凍結
ctClass.isFrozen()
// 刪除類不必要的屬性,以減少內存佔用。調用該方法後,許多方法無法將無法正常使用,慎用
ctClass.prune() 
//解凍一個類,使其可以被修改。如果事先知道一個類會被defrost, 則禁止調用prune方法
ctClass.defrost() 

2.2.4 CtClass類操作

// 添加接口
ctClass.addInterface(...);
// 添加構造器
ctClass.addConstructor(...);
// 添加字段
ctClass.addField(...);
// 添加方法
ctClass.addMethod(...);

2.2.5 CtClass類編譯

// 獲取字節碼文件 需要注意的是一旦調用該方法,則無法繼續修改已經被加載的class
Class clazz = ctClass.toClass();
// 類的字節碼文件
ClassFile classFile = ctClass.getClassFile();
// 編譯成字節碼文件, 使用當前線程上下文類加載器加載類, 如果類已存在或者編譯失敗將拋出異常
byte[] bytes = ctClass.toBytecode();

2.3 CtMethod對象

2.3.1 獲取CtMethod屬性

CtClass ctClass5 = pool.get(TestService.class.getName());
CtMethod ctMethod = ctClass5.getDeclaredMethod("selectOrder");
// 方法名
String methodName = ctMethod.getName();
// 返回類型
CtClass returnType = ctMethod.getReturnType();
// 方法參數, 通過此種方式得到方法參數列表
// 格式: com.kawa.TestService.getOrder(java.lang.String,java.util.List)
ctMethod.getLongName();
// 方法簽名 格式: (Ljava/lang/String;Ljava/util/List;Lcom/test/Order;)Ljava/lang/Integer;
ctMethod.getSignature();

// 獲取方法參數名稱, 可以通過這種方式得到方法真實參數名稱
List<String> argKeys = new ArrayList<>();
MethodInfo methodInfo = ctMethod.getMethodInfo();
CodeAttribute codeAttribute = methodInfo.getCodeAttribute();
LocalVariableAttribute attr = (LocalVariableAttribute) codeAttribute.getAttribute(LocalVariableAttribute.tag);
int len = ctMethod.getParameterTypes().length;
// 非靜態的成員函數的第一個參數是this
int pos = Modifier.isStatic(ctMethod.getModifiers()) ? 0 : 1;
for (int i = pos; i < len; i++) {
    argKeys.add(attr.variableName(i));
}

2.3.2 CtMethod方法體修改

// 在方法體前插入代碼塊
ctMethod.insertBefore("");
// 在方法體後插入代碼塊
ctMethod.insertAfter("");
// 在某行 字節碼 後插入代碼塊
ctMethod.insertAt(10, "");
// 添加參數
ctMethod.addParameter(CtClass);
// 設置方法名
ctMethod.setName("newName");
// 設置方法體 $0=this / $1,$2,$3... 代表方法參數
ctMethod.setBody("{$0.name = $1;}");
//創建一個新的方法
ctMethod.make("kawa",CtClass);

2.3.3 異常塊 addCatch()

在方法中加入try catch塊, 需要注意的是, 必須在插入的代碼中, 加入return值$e代表異常信息.插入的代碼片段必須以throw或return語句結束

CtMethod m = ...;
CtClass etype = ClassPool.getDefault().get("java.io.IOException");
m.addCatch("{ System.out.println($e); throw $e; }", etype);
// 等同於添加如下代碼: 
try {
    // the original method body
} catch (java.io.IOException e) {
    System.out.println(e);
    throw e;
}

2.4 特殊標識

$0

方法調用的目標對象. 它不等於this, 它代表了調用者. 如果方法是靜態的, 則$0爲null.

$1, $2 ..

方法的參數 m.insertBefore("{ System.out.println($1); System.out.println($2); }");

$$

是所有方法參數的簡寫, 主要用在方法調用上. 例如:

// 原方法
move(String a,String b)
move($$)
move($1,$2)
// 如果新增一個方法, 方法含有move的所有參數, 則可以這些寫: 
move($$, context)
move($1, $2, context)

$args

$args 指的是方法所有參數的數組,類似Object[],如果參數中含有基本類型,則會轉成其包裝類型。需要注意的時候,$args[0]對應的是$1,而不是$0,$0!=$args[0],$0=this

$cflow

$cflow意思爲控制流(control flow),是一個只讀的變量,值爲一個方法調用的深度。例

//原方法
int fact(int n) {
    if (n <= 1)
        return n;
    else
        return n * fact(n - 1);
}
//javassist調用
CtMethod cm = ...;
//這裏代表使用了cflow
cm.useCflow("fact");
//這裏用了cflow,說明當深度爲0的時候,就是開始當第一次調用fact的方法的時候,打印方法的第一個參數
cm.insertBefore("if ($cflow(fact) == 0)"
              + "    System.out.println(\"fact \" + $1);");

$_ 與 $r

$_是方法調用的結果;$r是返回結果的類型, 用於強制類型轉換

Object result = ... ;
$_ = ($r)result; 

$w

基本類型的包裝類 Integer i = ($w)5;

$class

一個 java.lang.Class 對象, 表示當前正在修改的類

$sig

類型爲 java.lang.Class 的參數類型數組

$type

一個 java.lang.Class 對象, 表示返回值類型

$proceed

調用表達式中方法的名稱

 

參考博客:https://zhuanlan.zhihu.com/p/349661837

參考文檔:https://www.javassist.org/tutorial/tutorial.html 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章