Java 字節碼以二進制的形式存儲在 .class 文件中,每一個 .class 文件包含一個 Java 類或接口。Javaassist 就是一個用來 處理 Java 字節碼的類庫。它可以在一個已經編譯好的類中添加新的方法,或者是修改已有的方法,並且不需要對字節碼方面有深入的了解。同時也可以去生成一個新的類對象,通過完全手動的方式。
首先需要引入jar包:
<dependency> <groupId>org.javassist</groupId> <artifactId>javassist</artifactId> <version>3.26.0-GA</version> </dependency>
1. 使用 Javassist 創建一個 class 文件
編寫創建對象的類:
package com.rickiyang.learn.javassist; import javassist.*; /** * @author rickiyang * @date 2019-08-06 * @Desc */ public class CreatePerson { /** * 創建一個Person 對象 * * @throws Exception */ public static void createPseson() throws Exception { ClassPool pool = ClassPool.getDefault(); // 1. 創建一個空類 CtClass cc = pool.makeClass("com.rickiyang.learn.javassist.Person"); // 2. 新增一個字段 private String name; // 字段名為name CtField param = new CtField(pool.get("java.lang.String"), "name", cc); // 訪問級別是 private param.setModifiers(Modifier.PRIVATE); // 初始值是 "xiaoming" cc.addField(param, CtField.Initializer.constant("xiaoming")); // 3. 生成 getter、setter 方法 cc.addMethod(CtNewMethod.setter("setName", param)); cc.addMethod(CtNewMethod.getter("getName", param)); // 4. 添加無參的構造函數 CtConstructor cons = new CtConstructor(new CtClass[]{}, cc); cons.setBody("{name = \"xiaohong\";}"); 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. 創建一個名為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); //這里會將這個創建的類對象編譯為.class文件 cc.writeFile("/Users/yangyue/workspace/springboot-learn/java-agent/src/main/java/"); } public static void main(String[] args) { try { createPseson(); } catch (Exception e) { e.printStackTrace(); } } }
執行上面的 main 函數之后,會在指定的目錄內生成 Person.class 文件:
// // Source code recreated from a .class file by IntelliJ IDEA // (powered by Fernflower decompiler) // package com.rickiyang.learn.javassist; public class Person { private String name = "xiaoming"; public void setName(String var1) { this.name = var1; } public String getName() { return this.name; } public Person() { this.name = "xiaohong"; } public Person(String var1) { this.name = var1; } public void printName() { System.out.println(this.name); } }
跟咱們預想的一樣。
在 Javassist 中,類 Javaassit.CtClass
表示 class 文件。一個 GtClass (編譯時類)對象可以處理一個 class 文件,ClassPool
是 CtClass
對象的容器。它按需讀取類文件來構造 CtClass
對象,並且保存 CtClass
對象以便以后使用。
需要注意的是 ClassPool 會在內存中維護所有被它創建過的 CtClass,當 CtClass 數量過多時,會占用大量的內存,API中給出的解決方案是 有意識的調用CtClass
的detach()
方法以釋放內存。
ClassPool
需要關注的方法:
- getDefault : 返回默認的
ClassPool
是單例模式的,一般通過該方法創建我們的ClassPool; - appendClassPath, insertClassPath : 將一個
ClassPath
加到類搜索路徑的末尾位置 或 插入到起始位置。通常通過該方法寫入額外的類搜索路徑,以解決多個類加載器環境中找不到類的尷尬; - toClass : 將修改后的CtClass加載至當前線程的上下文類加載器中,CtClass的
toClass
方法是通過調用本方法實現。需要注意的是一旦調用該方法,則無法繼續修改已經被加載的class; - get , getCtClass : 根據類路徑名獲取該類的CtClass對象,用於后續的編輯。
CtClass
需要關注的方法:
- freeze : 凍結一個類,使其不可修改;
- isFrozen : 判斷一個類是否已被凍結;
- prune : 刪除類不必要的屬性,以減少內存占用。調用該方法后,許多方法無法將無法正常使用,慎用;
- defrost : 解凍一個類,使其可以被修改。如果事先知道一個類會被defrost, 則禁止調用 prune 方法;
- detach : 將該class從ClassPool中刪除;
- writeFile : 根據CtClass生成
.class
文件; - toClass : 通過類加載器加載該CtClass。
上面我們創建一個新的方法使用了CtMethod
類。CtMthod代表類中的某個方法,可以通過CtClass提供的API獲取或者CtNewMethod新建,通過CtMethod對象可以實現對方法的修改。
CtMethod
中的一些重要方法:
- insertBefore : 在方法的起始位置插入代碼;
- insterAfter : 在方法的所有 return 語句前插入代碼以確保語句能夠被執行,除非遇到exception;
- insertAt : 在指定的位置插入代碼;
- setBody : 將方法的內容設置為要寫入的代碼,當方法被 abstract修飾時,該修飾符被移除;
- make : 創建一個新的方法。
注意到在上面代碼中的:setBody()的時候我們使用了一些符號:
// $0=this / $1,$2,$3... 代表方法參數 cons.setBody("{$0.name = $1;}");
具體還有很多的符號可以使用,但是不同符號在不同的場景下會有不同的含義,所以在這里就不在贅述,可以看javassist 的說明文檔。http://www.javassist.org/tutorial/tutorial2.html
2. 調用生成的類對象
1. 通過反射的方式調用
上面的案例是創建一個類對象然后輸出該對象編譯完之后的 .class 文件。那如果我們想調用生成的類對象中的屬性或者方法應該怎么去做呢?javassist也提供了相應的api,生成類對象的代碼還是和第一段一樣,將最后寫入文件的代碼替換為如下:
// 這里不寫入文件,直接實例化 Object person = cc.toClass().newInstance(); // 設置值 Method setName = person.getClass().getMethod("setName", String.class); setName.invoke(person, "cunhua"); // 輸出值 Method execute = person.getClass().getMethod("printName"); execute.invoke(person);
然后執行main方法就可以看到調用了 printName
方法。
2. 通過讀取 .class 文件的方式調用
ClassPool pool = ClassPool.getDefault(); // 設置類路徑 pool.appendClassPath("/Users/yangyue/workspace/springboot-learn/java-agent/src/main/java/"); CtClass ctClass = pool.get("com.rickiyang.learn.javassist.Person"); Object person = ctClass.toClass().newInstance(); // ...... 下面和通過反射的方式一樣去使用
3. 通過接口的方式
上面兩種其實都是通過反射的方式去調用,問題在於我們的工程中其實並沒有這個類對象,所以反射的方式比較麻煩,並且開銷也很大。那么如果你的類對象可以抽象為一些方法得合集,就可以考慮為該類生成一個接口類。這樣在newInstance()
的時候我們就可以強轉為接口,可以將反射的那一套省略掉了。
還拿上面的Person
類來說,新建一個PersonI
接口類:
package com.rickiyang.learn.javassist; /** * @author rickiyang * @date 2019-08-07 * @Desc */ public interface PersonI { void setName(String name); String getName(); void printName(); }
實現部分的代碼如下:
ClassPool pool = ClassPool.getDefault(); pool.appendClassPath("/Users/yangyue/workspace/springboot-learn/java-agent/src/main/java/"); // 獲取接口 CtClass codeClassI = pool.get("com.rickiyang.learn.javassist.PersonI"); // 獲取上面生成的類 CtClass ctClass = pool.get("com.rickiyang.learn.javassist.Person"); // 使代碼生成的類,實現 PersonI 接口 ctClass.setInterfaces(new CtClass[]{codeClassI}); // 以下通過接口直接調用 強轉 PersonI person = (PersonI)ctClass.toClass().newInstance(); System.out.println(person.getName()); person.setName("xiaolv"); person.printName();
使用起來很輕松。
2. 修改現有的類對象
前面說到新增一個類對象。這個使用場景目前還沒有遇到過,一般會遇到的使用場景應該是修改已有的類。比如常見的日志切面,權限切面。我們利用javassist來實現這個功能。
有如下類對象:
package com.rickiyang.learn.javassist; /** * @author rickiyang * @date 2019-08-07 * @Desc */ public class PersonService { public void getPerson(){ System.out.println("get Person"); } public void personFly(){ System.out.println("oh my god,I can fly"); } }
然后對他進行修改:
package com.rickiyang.learn.javassist; import javassist.ClassPool; import javassist.CtClass; import javassist.CtMethod; import javassist.Modifier; import java.lang.reflect.Method; /** * @author rickiyang * @date 2019-08-07 * @Desc */ public class UpdatePerson { public static void update() throws Exception { ClassPool pool = ClassPool.getDefault(); CtClass cc = pool.get("com.rickiyang.learn.javassist.PersonService"); CtMethod personFly = cc.getDeclaredMethod("personFly"); personFly.insertBefore("System.out.println(\"起飛之前准備降落傘\");"); personFly.insertAfter("System.out.println(\"成功落地。。。。\");"); //新增一個方法 CtMethod ctMethod = new CtMethod(CtClass.voidType, "joinFriend", new CtClass[]{}, cc); ctMethod.setModifiers(Modifier.PUBLIC); ctMethod.setBody("{System.out.println(\"i want to be your friend\");}"); cc.addMethod(ctMethod); Object person = cc.toClass().newInstance(); // 調用 personFly 方法 Method personFlyMethod = person.getClass().getMethod("personFly"); personFlyMethod.invoke(person); //調用 joinFriend 方法 Method execute = person.getClass().getMethod("joinFriend"); execute.invoke(person); } public static void main(String[] args) { try { update(); } catch (Exception e) { e.printStackTrace(); } } }
在personFly
方法前后加上了打印日志。然后新增了一個方法joinFriend
。執行main函數可以發現已經添加上了。
另外需要注意的是:上面的insertBefore()
和 setBody()
中的語句,如果你是單行語句可以直接用雙引號,但是有多行語句的情況下,你需要將多行語句用{}
括起來。javassist只接受單個語句或用大括號括起來的語句塊。
作者: rickiyang