Markdown版本筆記 | 我的GitHub首頁 | 我的博客 | 我的微信 | 我的郵箱 |
---|---|---|---|---|
MyAndroidBlogs | baiqiantao | baiqiantao | bqt20094 | baiqiantao@sina.com |
目錄
簡介
基本功能演示
運行期修改類
原始類
修改未加載過的類
修改已加載過的類
獲取類基本信息
創建一個新類
重新生成類的字節碼文件
原始類
給類添加方法
給類添加屬性
修改類的方法
修改注解的值
實現動態代理
簡介
Java bytecode engineering toolkit since 1999
Javassist是一個開源的分析、編輯和創建Java字節碼的類庫
。是由日本的 Shigeru Chiba 所創建的,它已加入了開放源代碼 JBoss 應用服務器項目,通過使用 Javassist 對字節碼操作為 JBoss 實現動態"AOP"框架。
關於java字節碼的處理,目前有很多工具,如bcel,ASM
。不過這些都需要直接跟虛擬機指令
打交道。如果你不想了解虛擬機指令,可以采用javassist。javassist是jboss的一個子項目,其主要的優點在於簡單、快速。直接使用java編碼的形式,而不需要了解虛擬機指令,就能動態改變類的結構,或者動態生成類
。
官方簡介
Javassist(Java Programming Assistant)使Java字節碼
操作變得簡單。它是一個用於在Java中編輯字節碼的類庫;它使Java程序能夠在運行時
定義新類,並在JVM加載時修改類文件。
Javassist (Java Programming Assistant) makes Java
bytecode
manipulation simple. It is a class library for editing bytecodes in Java; it enables Java programs to define a new class atruntime
and to modify a class file when the JVM loads it.
與其他類似的字節碼編輯器不同,Javassist提供兩個級別的API:源級別和字節碼級別
。如果用戶使用源級API,他們可以在不了解Java字節碼規范
的情況下編輯class文件。整個API僅使用Java語言的詞匯表進行設計。您甚至可以以源文本的形式指定插入的字節碼; Javassist即時
編譯它。另一方面,字節碼級API允許用戶直接
編輯class文件作為其他編輯器。
Unlike other similar bytecode editors, Javassist provides two levels of API:
source level and bytecode level
. If the users use the source- level API, they can edit a class file without knowledge ofthe specifications of the Java bytecode
. The whole API is designed with only the vocabulary of the Java language. You can even specify inserted bytecode in the form of source text; Javassist compiles iton the fly
. On the other hand, the bytecode-level API allows the users todirectly
edit a class file as other editors.
基本功能演示
PS:這個框架在Android中可用,目前還沒發現兼容性問題!
運行期修改類
原始類
package com.bqt.test;
public class Person {
public void hello(String s) {
System.out.println(s);
}
}
修改未加載過的類
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get("com.bqt.test.Person");
CtMethod cm = cc.getDeclaredMethod("hello", new CtClass[] { pool.get("java.lang.String") });
cm.setBody("{" + "System.out.println(\"你好:\" + $1);" + "}");
cc.writeFile("d:/test");//保存到指定目錄
cc.toClass(); //加載修改后的類,注意:必須保證調用前此類未加載
new Person().hello("包青天");
運行結果為:
你好:包青天
注意,如果使用 JDK9 以下的JDK ,同時使用 3.20.0-GA 以上版本的 Javassist,調用 toClass 方法會報 StackWalker 異常
生成的類經反編譯后的源碼為:
package com.bqt.test;
import java.io.PrintStream;
public class Person {
public void hello(String paramString) {
System.out.println("你好" + paramString);
}
}
修改已加載過的類
同個 Class 是不能在同個 ClassLoader 中加載兩次的,所以在輸出 CtClass 的時候需要注意下。
例如上例中,如果在調用toClass()
前 Person 類已經加載過了,則直接報異常:
new Person().hello("包青天");
ClassPool.getDefault().get("com.bqt.test.Person").toClass();
Exception in thread "main" javassist.CannotCompileException: by java.lang.LinkageError: loader (instance of sun/misc/Launcher$AppClassLoader): attempted duplicate class definition for name: "com/bqt/test/Person"
at javassist.ClassPool.toClass(ClassPool.java:1170)
at javassist.ClassPool.toClass(ClassPool.java:1113)
at javassist.ClassPool.toClass(ClassPool.java:1071)
at javassist.CtClass.toClass(CtClass.java:1264)
at com.bqt.test.Main.main(Main.java:9)
Caused by: java.lang.LinkageError: loader (instance of sun/misc/Launcher$AppClassLoader): attempted duplicate class definition for name: "com/bqt/test/Person"
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(Unknown Source)
at java.lang.ClassLoader.defineClass(Unknown Source)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
at java.lang.reflect.Method.invoke(Unknown Source)
at javassist.ClassPool.toClass2(ClassPool.java:1183)
at javassist.ClassPool.toClass(ClassPool.java:1164)
... 4 more
解決方法:指定一個未加載的ClassLoader
為了方便,Javassist 也提供一個 Classloader 供使用,例如
new Person().hello("包青天");
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get("com.bqt.test.Person");
CtMethod cm = cc.getDeclaredMethod("hello", new CtClass[] { pool.get("java.lang.String") });
cm.setBody("{" + "System.out.println(\"你好鴨:\" + $1);" + "}");
cc.writeFile("d:/test");//保存到指定目錄
Translator translator = new Translator() {
@Override
public void start(ClassPool classPool) throws NotFoundException, CannotCompileException {
System.out.println("start");
}
@Override
public void onLoad(ClassPool classPool, String paramString) throws NotFoundException, CannotCompileException {
System.out.println("onLoad:" + paramString); //com.bqt.test.Person
new Person().hello("白乾濤"); //調用的是原始類的方法
}
};
Loader classLoader = new Loader(pool); //Javassist 提供的 Classloader
classLoader.addTranslator(pool, translator); //監聽 ClassLoader 的生命周期
Class clazz = classLoader.loadClass("com.bqt.test.Person");
new Person().hello("白乾濤2"); //調用的是原始類的方法
clazz.getDeclaredMethod("hello", String.class).invoke(clazz.newInstance(), "你妹"); //調用的是新類的方法
Class clazz2 = Class.forName("com.bqt.test.Person");
clazz2.getDeclaredMethod("hello", String.class).invoke(clazz2.newInstance(), "你妹2"); //調用原始類的方法
打印日志:
包青天
start
onLoad:com.bqt.test.Person
白乾濤
白乾濤2
你好鴨:你妹
你妹2
獲取類基本信息
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get("com.bqt.test.Person");
byte[] bytes = cc.toBytecode();//得到字節碼
System.out.println(bytes.length);
System.out.println(cc.getName());//獲取類名
System.out.println(cc.getSimpleName());//獲取簡要類名
System.out.println(cc.getSuperclass().getName());//獲取父類
System.out.println(Arrays.toString(cc.getInterfaces()));//獲取接口
for (CtConstructor con : cc.getConstructors()) {//獲取構造方法
System.out.println("構造方法 "+con.getLongName());
}
for (CtMethod method : cc.getMethods()) {//獲取方法
System.out.println(method.getLongName());
}
打印內容:
562
com.bqt.test.Person
Person
java.lang.Object
[]
構造方法 com.bqt.test.Person()
java.lang.Object.equals(java.lang.Object)
java.lang.Object.finalize()
com.bqt.test.Person.hello2(java.lang.String)
java.lang.Object.toString()
java.lang.Object.getClass()
java.lang.Object.notifyAll()
java.lang.Object.hashCode()
java.lang.Object.wait()
java.lang.Object.notify()
com.bqt.test.Person.hello(java.lang.String)
java.lang.Object.wait(long)
java.lang.Object.wait(long,int)
java.lang.Object.clone()
獲取注解信息
Object[] annotations = cf.getAnnotations(); //獲取類、方法、字段等上面定義的注解信息
SerializedName annotation = (SerializedName) annotations[0]; //遍歷判斷注解類型
System.out.println(annotation.value()); //獲取注解的值
創建一個新類
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.makeClass("com.bqt.test.User");
//創建屬性
cc.addField(CtField.make("private int id;", cc));
cc.addField(CtField.make("private String name;", cc));
//創建方法
cc.addMethod(CtMethod.make("public String getName(){return name;}", cc));
cc.addMethod(CtMethod.make("public void setName(String name){this.name = name;}", cc));
//添加構造器
CtConstructor constructor = new CtConstructor(new CtClass[] { CtClass.intType, pool.get("java.lang.String") }, cc);
constructor.setBody("{this.id=id;this.name=name;}");
cc.addConstructor(constructor);
cc.writeFile("d:/test");
生成的類經反編譯后的源碼為:
package com.bqt.test;
public class User {
private int id;
private String name;
public User(int paramInt, String paramString) {
this.id = this.id;
this.name = this.name;
}
public String getName() {
return this.name;
}
public void setName(String paramString) {
this.name = paramString;
}
}
重新生成類的字節碼文件
原始類
package com.bqt.test;
public class Person {
public int hello(String s) {
return s.length();
}
public String hello2(String s) {
return s;
}
}
給類添加方法
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get("com.bqt.test.Person");
//方式一
CtMethod cm1 = CtMethod.make("public int add1(int a, String b){return a+b.length();}", cc);//第一種方式,完整的方法以字符串形式傳遞過去
cc.addMethod(cm1); //cc.removeMethod(cm3) 刪除一個方法
//方式二
CtClass[] parameters = new CtClass[] { CtClass.intType, pool.get("java.lang.String") };
CtMethod cm2 = new CtMethod(CtClass.intType, "add2", parameters, cc);//第二種方式,返回值類型,方法名,參數,對象
cm2.setModifiers(Modifier.PUBLIC);//訪問范圍
cm2.setBody("{return $1+$2.length();}");//方法體
cc.addMethod(cm2);
cc.writeFile("D:/test");//保存到指定位置
生成的類經反編譯后的源碼為:
package com.bqt.test;
public class Person {
public int hello(String s) {
return s.length();
}
public String hello2(String s) {
return s;
}
public int add1(int paramInt, String paramString) {
return paramInt + paramString.length();
}
public int add2(int paramInt, String paramString) {
return paramInt + paramString.length();
}
}
給類添加屬性
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get("com.bqt.test.Person");
//方式一
CtField cf = new CtField(CtClass.intType, "age", cc);
cf.setModifiers(Modifier.PRIVATE);
cc.addField(cf);
cc.addMethod(CtNewMethod.getter("getAge", cf));
cc.addMethod(CtNewMethod.setter("setAge", cf));
//方式二
CtField cf2 = CtField.make("private String name;", cc);
cc.addField(cf2);
cc.addMethod(CtNewMethod.getter("getName", cf2)); //快捷的添加get/set方法
cc.addMethod(CtNewMethod.setter("setName", cf2));
cc.writeFile("D:/test");//保存到指定位置
生成的類經反編譯后的源碼為:
package com.bqt.test;
public class Person {
private int age;
private String name;
public int hello(String s) {
return s.length();
}
public String hello2(String s) {
return s;
}
public int getAge() {
return this.age;
}
public void setAge(int paramInt) {
this.age = paramInt;
}
public String getName() {
return this.name;
}
public void setName(String paramString) {
this.name = paramString;
}
}
修改類的方法
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get("com.bqt.test.Person");
//方式一
CtMethod cm = cc.getDeclaredMethod("hello", new CtClass[] { pool.get("java.lang.String") });
cm.insertBefore("System.out.println(\"調用前2\");");//調用前
cm.insertBefore("System.out.println(\"調用前1\");");
cm.insertAt(20094, "System.out.println(\"在指定行插入代碼\");");//貌似行號胡亂寫也可以
cm.insertAfter("System.out.println(\"調用后1\");");//調用后
cm.insertAfter("System.out.println(\"調用后2\");");//調用后
//方式二
CtMethod cm2 = cc.getDeclaredMethod("hello2", new CtClass[] { pool.get("java.lang.String") });
cm2.setBody("{" + // 你只需要正常寫代碼邏輯就可以了,復制過來時,一些IDE,比如AS會自動幫你添加轉義字符
"if ($1 == null) {\n" + //$0代表的是this,$1代表方法參數的第一個參數、$2代表方法參數的第二個參數
"\treturn \"\";\n" + //
"}\n" + //
"return \"你好:\" + $1;" + //
"}");
cc.writeFile("D:/test");//保存到指定位置
生成的類經反編譯后的源碼為:
package com.bqt.test;
import java.io.PrintStream;
public class Person {
public int hello(String s) {
System.out.println("調用前1");
System.out.println("調用前2");
System.out.println("在指定行插入代碼");
int i = s.length();
System.out.println("調用后1");
int j = i;
System.out.println("調用后2");
return j;
}
public String hello2(String paramString) {
if (paramString == null) {
return "";
}
return "你好:" + paramString;
}
}
修改注解的值
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get("com.bqt.test.Person");
CtField cf = cc.getField("age");
FieldInfo fInfo = cf.getFieldInfo();
ConstPool cp = cc.getClassFile().getConstPool();
//修改注解的值
Annotation annotation = new Annotation(SerializedName.class.getName(), cp);//獲取注解
annotation.addMemberValue("value", new StringMemberValue("_AGE", cp));//設置注解指定字段的值
AnnotationsAttribute annotationsAttribute = new AnnotationsAttribute(cp, AnnotationsAttribute.visibleTag);//獲取注解信息
annotationsAttribute.setAnnotation(annotation);//設置注解
fInfo.addAttribute(annotationsAttribute);//添加(覆蓋)注解信息
cc.writeFile("D:/test");//保存到指定位置
修改前
class Person {
@SerializedName("AGE")
public int age = 28;
}
修改后
class Person {
@SerializedName("_AGE")
public int age = 28;
}
實現動態代理
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get("com.bqt.test.Person");
ProxyFactory factory = new ProxyFactory();//代理類工廠
factory.setSuperclass(cc.toClass());//設置父類,ProxyFactory將會動態生成一個類,繼承該父類
//設置過濾器,判斷哪些方法調用需要被攔截
factory.setFilter(new MethodFilter() {
@Override
public boolean isHandled(Method m) {
return m.getName().startsWith("hello");
}
});
Class<?> clazz = factory.createClass();//創建代理類型
Person proxy = (Person) clazz.newInstance();//創建代理實例
//設置代理處理方法
((ProxyObject) proxy).setHandler(new MethodHandler() {
@Override
public Object invoke(Object self, Method thisMethod, Method proceed, Object[] args) throws Throwable {
System.out.println(thisMethod.getName() + "被調用了");
try {
Object ret = proceed.invoke(self, args);//thisMethod為被代理方法,proceed為代理方法,self為代理實例,args為方法參數
System.out.println("返回值: " + ret);
return ret;
} finally {
System.out.println(thisMethod.getName() + "調用完畢");
}
}
});
//測試
proxy.hello("包青天");
System.out.println(proxy.getClass().getName()); //com.bqt.test.Person_$$_jvstee7_0
打印日志:
hello被調用了
返回值: 3
hello調用完畢
com.bqt.test.Person_$$_jvstee7_0
2019-1-7