最近項目需要對基礎架構做增強,需要基於字節碼在不侵入原有代碼的情況下實現, 故把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