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字節碼的。這些類名的前綴Ct
是compile 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\");");