javassist 學習筆記

介紹:www.javassist.org/


javassist、ASM 對比
1、javassist是基於源碼級別的API比基於字節碼的ASM簡單。
2、基於javassist開發,不需要了解字節碼的一些知識,而且其封裝的一些工具類可以簡單實現一些高級功能。比如HotSwaper。
3、ASM比javassist性能更快,靈活行也較高。
4、javassist提供者動態代理接口最慢,比JDK自帶的還慢

性能對比
 Framework        First time       Later times
Javassist             257                  5.2
BCEL                     473                  5.5
ASM                      62.4                 1.1 


參考手冊:

1、讀取和輸出字節碼

ClassPool pool = ClassPool.getDefault();
//會從classpath中查詢該類
CtClass cc = pool.get("test.Rectangle");
//設置.Rectangle的父類
cc.setSuperclass(pool.get("test.Point"));
//輸出.Rectangle.class文件到該目錄中
cc.writeFile("c://");
//輸出成二進制格式
//byte[] b=cc.toBytecode();
//輸出並加載class 類,默認加載到當前線程的ClassLoader中,也可以選擇輸出的ClassLoader。
//Class clazz=cc.toClass();

這裏可以看出,Javassist的加載是依靠ClassPool類,輸出方式支持三種。

2、新增Class
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.makeClass("Point");
//新增方法
cc.addMethod(m);
//新增Field
cc.addField(f);

從上面可以看出,對Class的修改主要是依賴於CtClass 類。API也比較清楚和簡單。

3、凍結Class
當CtClass 調用writeFile()、toClass()、toBytecode() 這些方法的時候,Javassist會凍結CtClass Object,對CtClass object的修改將不允許。這個主要是爲了警告開發者該類已經被加載,而JVM是不允許重新加載該類的。如果要突破該限制,方法如下:
CtClasss cc = ...;
    :
cc.writeFile();
cc.defrost();
cc.setSuperclass(...);    // OK since the class is not frozen.

當 ClassPool.doPruning=true的時候,Javassist 在CtClass object被凍結時,會釋放存儲在ClassPool對應的數據。這樣做可以減少javassist的內存消耗。默認情況ClassPool.doPruning=false。例如

CtClasss cc = ...;
cc.stopPruning(true);
    :
cc.writeFile();                             // convert to a class file.
// cc沒有被釋放

提示:當調試時,可以調用debugWriteFile(),該方法不會導致CtClass被釋放。


4、Class 搜索路徑
從上面可以看出Class 的載入是依靠ClassPool,而ClassPool.getDefault() 方法的搜索Classpath 只是搜索JVM的同路徑下的class。當一個程序運行在JBoss或者Tomcat下,ClassPool Object 可能找到用戶的classes。Javassist 提供了四種動態加載classpath的方法。如下

//默認加載方式如pool.insertClassPath(new ClassClassPath(this.getClass()));
ClassPool pool = ClassPool.getDefault();

//從file加載classpath
pool.insertClassPath("/usr/local/javalib")

//從URL中加載
ClassPath cp = new URLClassPath("www.javassist.org", 80, "/java/", "org.javassist.");
pool.insertClassPath(cp);

//從byte[] 中加載
byte[] b = a byte array;
String name = class name;
cp.insertClassPath(new ByteArrayClassPath(name, b));

//可以從輸入流中加載class
InputStream ins = an input stream for reading a class file;
CtClass cc = cp.makeClass(ins);


5、ClassPool
5.1 減少內存溢出
     ClassPool是一個CtClass objects的裝載容器。當加載了CtClass object後,是不會被ClassPool釋放的(默認情況下)。這個是因爲CtClass object 有可能在下個階段會被用到。
     當加載過多的CtClass object的時候,會造成OutOfMemory的異常。爲了避免這個異常,javassist提供幾種方法,一種是在上面提到的 ClassPool.doPruning這個參數,還有一種方法是調用CtClass.detach()方法,可以把CtClass object 從ClassPool中移除。例如:
CtClass cc = ... ;
cc.writeFile();
cc.detach();

另外一中方法是不用默認的ClassPool即不用 ClassPool.getDefault()這個方式來生成。這樣當ClassPool 沒被引用的時候,JVM的垃圾收集會收集該類。例如
//ClassPool(true) 會默認加載Jvm的ClassPath
ClassPool cp = new ClassPool(true);
// if needed, append an extra search path by appendClassPath()

5.2  級聯ClassPools
     javassist支持級聯的ClassPool,即類似於繼承。例如:
ClassPool parent = ClassPool.getDefault();
ClassPool child = new ClassPool(parent);
child.insertClassPath("./classes");

5.3 修改已有Class的name以創建一個新的Class
當調用setName方法時,會直接修改已有的Class的類名,如果再次使用舊的類名,則會重新在classpath路徑下加載。例如:
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get("Point");
cc.setName("Pair");
//重新在classpath加載
CtClass cc1 = pool.get("Point");

對於一個被凍結(Frozen)的CtClass object ,是不可以修改class name的,如果需要修改,則可以重新加載,例如:
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get("Point");
cc.writeFile();
//cc.setName("Pair");    wrong since writeFile() has been called.
CtClass cc2 = pool.getAndRename("Point", "Pair");
 

6、Class loader
     上面也提到,javassist同個Class是不能在同個ClassLoader中加載兩次的。所以在輸出CtClass的時候需要注意下,例如:
// 當Hello未加載的時候,下面是可以運行的。
ClassPool cp = ClassPool.getDefault();
CtClass cc = cp.get("Hello");
Class c = cc.toClass();
//下面這種情況,由於Hello2已加載,所以會出錯
Hello2 h=new Hello2();
CtClass cc2 = cp.get("Hello2");
Class c2 = cc.toClass();//這裏會拋出java.lang.LinkageError 異常
//解決加載問題,可以指定一個未加載的ClassLoader
Class c3 = cc.toClass(new MyClassLoader());

6.1 使用javassist.Loader
從上面可以看到,如果在同一個ClassLoader加載兩次Class拋出異常,爲了方便javassist也提供一個Classloader供使用,例如
 ClassPool pool = ClassPool.getDefault();
 Loader cl = new Loader(pool);
 CtClass ct = pool.get("test.Rectangle");
ct.setSuperclass(pool.get("test.Point"));
Class c = cl.loadClass("test.Rectangle");
Object rect = c.newInstance();
         :
爲了方便監聽Javassist自帶的ClassLoader的生命週期,javassist也提供了一個listener,可以監聽ClassLoader的生命週期,例如:
//Translator 爲監聽器
public class MyTranslator implements Translator {
    void start(ClassPool pool)
        throws NotFoundException, CannotCompileException {}
    void onLoad(ClassPool pool, String classname)
        throws NotFoundException, CannotCompileException
    {
        CtClass cc = pool.get(classname);
        cc.setModifiers(Modifier.PUBLIC);
    }
}
//示例
public class Main2 {
  public static void main(String[] args) throws Throwable {
     Translator t = new MyTranslator();
     ClassPool pool = ClassPool.getDefault();
     Loader cl = new Loader();
     cl.addTranslator(pool, t);
     cl.run("MyApp", args);
  }
}
% java Main2 arg1 arg2...

6.2 修改系統Class
由JVM規範可知,system classloader 是比其他classloader 是優先加載的,而system classloader 主要是加載系統Class,所以要修改系統Class,如果默認參數運行程序是不可能修改的。如果需要修改也有一些辦法,即在運行時加入-Xbootclasspath/p: 參數的意義可以參考其他文件。下面修改String的例子如下:
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get("java.lang.String");
CtField f = new CtField(CtClass.intType, "hiddenValue", cc);
f.setModifiers(Modifier.PUBLIC);
cc.addField(f);
cc.writeFile(".");
% java -Xbootclasspath/p:. MyApp arg1 arg2...

6.3 動態重載Class
如果JVM運行時開啓JPDA(Java Platform Debugger Architecture),則Class是運行被動態重新載入的。具體方式可以參考java.lang.Instrument。javassist也提供了一個運行期重載Class的方法,具體可以看API 中的javassist.tools.HotSwapper。

7、Introspection和定製
javassist封裝了很多很方便的方法以供使用,大部分使用只需要用這些API即可,如果不能滿足,Javassist也提供了一個低層的API(具體參考javassist.bytecode 包)來修改原始的Class。

7.1 插入source 文本在方法體前或者後
CtMethod 和CtConstructor 提供了 insertBefore()、insertAfter()和 addCatch()方法,它們可以插入一個souce文本到存在的方法的相應的位置。javassist 包含了一個簡單的編譯器解析這souce文本成二進制插入到相應的方法體裏。
     javassist 還支持插入一個代碼段到指定的行數,前提是該行數需要在class 文件裏含有。
     插入的source 可以關聯fields 和methods,也可以關聯方法的參數。但是關聯方法參數的時,需要在程序編譯時加上 -g 選項(該選項可以把本地變量的聲明保存在class 文件中,默認是不加這個參數的。)。因爲默認一般不加這個參數,所以Javassist也提供了一些特殊的變量來代表方法參數:$1,$2,$args...要注意的是,插入的source文本中不能引用方法本地變量的聲明,但是可以允許聲明一個新的方法本地變量,除非在程序編譯時加入-g選項。
方法的特殊變量說明:
$0, $1, $2, ... this and actual parameters
$args An array of parameters. The type of $args is Object[].
$$ All actual parameters.For example, m($$) is equivalent to m($1,$2,...)
$cflow(...) cflow variable
$r The result type. It is used in a cast expression.
$w The wrapper type. It is used in a cast expression.
$_ The resulting value
$sig An array of java.lang.Class objects representing the formal parameter types
$type A java.lang.Class object representing the formal result type.
$class A java.lang.Class object representing the class currently edited.

7.1.1 $0, $1, $2, ...
$0代碼的是this,$1代表方法參數的第一個參數、$2代表方法參數的第二個參數,以此類推,$N代表是方法參數的第N個。例如:
//實際方法
void move(int dx, int dy) 
//javassist
CtMethod m = cc.getDeclaredMethod("move");
//打印dx,和dy
m.insertBefore("{ System.out.println($1); System.out.println($2); }");
注意:如果javassist改變了$1的值,那實際參數值也會改變。

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

7.1.3 $$
$$是所有方法參數的簡寫,主要用在方法調用上。例如:
//原方法
move(String a,String b)
move($$) 相當於move($1,$2)
如果新增一個方法,方法含有move的所有參數,則可以這些寫:
exMove($$, context) 相當於 exMove($1, $2, context)

7.1.4 $cflow
 $cflow意思爲控制流(control flow),是一個只讀的變量,值爲一個方法調用的深度。例如:
int fact(int n) {
    if (n <= 1)
        return n;
    else
        return n * fact(n - 1);
}

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

7.1.5 $r
指的是方法返回值的類型,主要用在類型的轉型上。例如:
Object result = ... ;
$_ = ($r)result;
如果返回值爲基本類型的包裝類型,則該值會自動轉成基本類型,如返回值爲Integer,則$r爲int。如果返回值爲void,則該值爲null。

7.1.6 $w
$w代表一個包裝類型。主要用在轉型上。比如:Integer i = ($w)5; 如果該類型不是基本類型,則會忽略。

7.1.7 $_
$_代表的是方法的返回值。

7.1.8 $sig
$sig指的是方法參數的類型(Class)數組,數組的順序爲參數的順序。

7.1.9 $class
$class 指的是this的類型(Class)。也就是$0的類型。

7.1.10 addCatch()
addCatch() 指的是在方法中加入try catch 塊,需要主要的是,必須在插入的代碼中,加入return 值。$e代表 異常值。比如:
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;
}

8、修改方法體
CtMethod 和CtConstructor 提供了 setBody() 的方法,可以替換方法或者構造函數裏的所有內容。
支持的變量有:
$0, $1, $2, ... this and actual parameters
$args An array of parameters. The type of $args is Object[].
$$ All actual parameters.For example, m($$) is equivalent to m($1,$2,...)
$cflow(...) cflow variable
$r The result type. It is used in a cast expression.
$w The wrapper type. It is used in a cast expression.
$sig An array of java.lang.Class objects representing the formal parameter types
$type A java.lang.Class object representing the formal result type.
$class A java.lang.Class object representing the class currently edited.

注意 $_變量不支持。

8.1 替換方法中存在的source
javassist 允許修改方法裏的其中一個表達式。 javassist.expr.ExprEditor 這個class 可以替換該表達式。例如:
CtMethod cm = ... ;
cm.instrument(
    new ExprEditor() {
        public void edit(MethodCall m)
                      throws CannotCompileException
        {
            if (m.getClassName().equals("Point")
                          && m.getMethodName().equals("move"))
                m.replace("{ $1 = 0; $_ = $proceed($$); }");
        }
    });

注意: that the substituted code is not an expression but a statement or a block. It cannot be or contain a try-catch statement.

方法instrument() 可以用來搜索方法體裏的內容。比如調用一個方法,field訪問,對象創建等。如果你想在某個表達式前後插入方法,則修改的souce如下:
{ before-statements;
  $_ = $proceed($$);
  after-statements; }

8.2 javassist.expr.MethodCall
MethodCall代表的是一個方法的調用。用replace()方法可以對調用的方法進行替換。

$0 The target object of the method call.
This is not equivalent to this, which represents the caller-side this object.
$0 is null if the method is static.
$1, $2, ...
The parameters of the method call.
$_ The resulting value of the method call.
$r The result type of the method call.
$class A java.lang.Class object representing the class declaring the method.
$sig An array of java.lang.Class objects representing the formal parameter types
$type A java.lang.Class object representing the formal result type.
$proceed The name of the method originally called in the expression.

注意:$w, $args 和 $$也是允許的。$0不是this,是隻調用方法的Object。$proceed指的是一個特殊的語法,而不是一個String。

8.3 javassist.expr.ConstructorCall
ConstructorCall 指的是一個構造函數,比如:this()、super()的調用。ConstructorCall.replace()是用來用替換一個塊當調用構造方法的時候。
$0 The target object of the constructor call. This is equivalent to this.
$1, $2, ... The parameters of the constructor call.
$class A java.lang.Class object representing the class declaring the constructor.
$sig An array of java.lang.Class objects representing the formal parameter types.
$proceed The name of the constructor originally called in the expression.

$w, $args 和 $$  也是允許的。

8.4 javassist.expr.FieldAccess
FieldAccess代表的是Field的訪問類。
$0 The object containing the field accessed by the expression. This is not equivalent to this.
this represents the object that the method including the expression is invoked on.
$0 is null if the field is static.
$1
The value that would be stored in the field if the expression is write access.
Otherwise, $1 is not available.
$_ The resulting value of the field access if the expression is read access.
Otherwise, the value stored in $_ is discarded.
$r The type of the field if the expression is read access.
Otherwise, $r is void.
$class A java.lang.Class object representing the class declaring the field.
$type
A java.lang.Class object representing the field type.
$proceed The name of a virtual method executing the original field access. .

$w, $args 和 $$  也是允許的。

8.5 javassist.expr.NewExpr
NewExpr代表的是一個Object 的操作(但不包括數組的創建)。
$0 null
$1, $2, ...
The parameters to the constructor.
$_ The resulting value of the object creation.
A newly created object must be stored in this variable.
$r The type of the created object.
$sig An array of java.lang.Class objects representing the formal parameter types
$type A java.lang.Class object representing the class of the created object.
$proceed The name of a virtual method executing the original object creation. .

$w, $args 和 $$  也是允許的。

8.6 javassist.expr.NewArray
NewArray 代表的是數組的創建。
$0 null
$1, $2, ...
The size of each dimension.
$_ The resulting value of the object creation. 
A newly created array must be stored in this variable.
$r The type of the created object.
$type A java.lang.Class object representing the class of the created array .
$proceed The name of a virtual method executing the original array creation. .

$w, $args 和 $$  也是允許的。
例如:
String[][] s = new String[3][4];
 $1 和 $2 的值爲 3 和 4, $3 得不到的.
String[][] s = new String[3][];
 $1 的值是 3 ,但 $2 得不到的.

8.7 javassist.expr.Instanceof
Instanceof 代表的是Instanceof 表達式。
$0 null
$1
The value on the left hand side of the original instanceof operator.
$_ The resulting value of the expression. The type of $_ is boolean.
$r The type on the right hand side of the instanceof operator.
$type A java.lang.Class object representing the type on the right hand side of the instanceof operator.
$proceed The name of a virtual method executing the original instanceof expression.
It takes one parameter (the type is java.lang.Object) and returns true
if the parameter value is an instance of the type on the right hand side of
the original instanceof operator. Otherwise, it returns false.
$w, $args 和 $$  也是允許的。

8.8 javassist.expr.Cast
Cast 代表的是一個轉型表達式。
$0 null
$1
The value the type of which is explicitly cast.
$_ The resulting value of the expression. The type of $_ is the same as the type
after the explicit casting, that is, the type surrounded by ( ).
$r the type after the explicit casting, or the type surrounded by ( ).
$type A java.lang.Class object representing the same type as $r.
$proceed The name of a virtual method executing the original type casting.
It takes one parameter of the type java.lang.Object and returns it after
the explicit type casting specified by the original expression.
$w, $args 和 $$  也是允許的。

8.9 javassist.expr.Handler
Handler 代表的是一個try catch 聲明。
$1 The exception object caught by the catch clause.
$r
the type of the exception caught by the catch clause. It is used in a cast expression.
$w The wrapper type. It is used in a cast expression.
$type A java.lang.Class object representing
the type of the exception caught by the catch clause.

9 新增一個方法或者field
Javassist 允許開發者創建一個新的方法或者構造方法。新增一個方法,例如:
CtClass point = ClassPool.getDefault().get("Point");
CtMethod m = CtNewMethod.make(
                 "public int xmove(int dx) { x += dx; }",
                 point);
point.addMethod(m);

在方法中調用其他方法,例如:
CtClass point = ClassPool.getDefault().get("Point");
CtMethod m = CtNewMethod.make(
                 "public int ymove(int dy) { $proceed(0, dy); }",
                 point, "this", "move");
其效果如下:
public int ymove(int dy) { this.move(0, dy); }
下面是javassist提供另一種新增一個方法(未看明白):
Javassist provides another way to add a new method. You can first create an abstract method and later give it a method body:

CtClass cc = ... ;
CtMethod m = new CtMethod(CtClass.intType, "move",
                          new CtClass[] { CtClass.intType }, cc);
cc.addMethod(m);
m.setBody("{ x += $1; }");
cc.setModifiers(cc.getModifiers() & ~Modifier.ABSTRACT);
Since Javassist makes a class abstract if an abstract method is added to the class, you have to explicitly change the class back to a non-abstract one after calling setBody().

9.1 遞歸方法
CtClass cc = ... ;
CtMethod m = CtNewMethod.make("public abstract int m(int i);", cc);
CtMethod n = CtNewMethod.make("public abstract int n(int i);", cc);
cc.addMethod(m);
cc.addMethod(n);
m.setBody("{ return ($1 <= 0) ? 1 : (n($1 - 1) * $1); }");
n.setBody("{ return m($1); }");
cc.setModifiers(cc.getModifiers() & ~Modifier.ABSTRACT);

9.2 新增field
如下:
CtClass point = ClassPool.getDefault().get("Point");
CtField f = new CtField(CtClass.intType, "z", point);
point.addField(f);
//point.addField(f, "0");    // initial value is 0.
或者:
CtClass point = ClassPool.getDefault().get("Point");
CtField f = CtField.make("public int z = 0;", point);
point.addField(f);

9.3 移除方法或者field
調用removeField()或者removeMethod()。

10 註解
獲取註解信息:
//註解
public @interface Author {
    String name();
    int year();
}
//javassist代碼
CtClass cc = ClassPool.getDefault().get("Point");
Object[] all = cc.getAnnotations();
Author a = (Author)all[0];
String name = a.name();
int year = a.year();
System.out.println("name: " + name + ", year: " + year);

11  javassist.runtime 

12 import
引用包:
ClassPool pool = ClassPool.getDefault();
pool.importPackage("java.awt");
CtClass cc = pool.makeClass("Test");
CtField f = CtField.make("public Point p;", cc);
cc.addField(f);

13 限制
(1)不支持java5.0的新增語法。不支持註解修改,但可以通過底層的javassist類來解決,具體參考:javassist.bytecode.annotation
(2)不支持數組的初始化,如String[]{"1","2"},除非只有數組的容量爲1
(3)不支持內部類和匿名類
(4)不支持continue和btreak 表達式。
(5)對於繼承關係,有些不支持。例如
class A {} 
class B extends A {} 
class C extends B {} 

class X { 
    void foo(A a) { .. } 
    void foo(B b) { .. } 
}
如果調用  x.foo(new C()),可能會調用foo(A) 。

(6)推薦開發者用#分隔一個class name和static method或者 static field。例如:
javassist.CtClass.intType.getName()推薦用javassist.CtClass#intType.getName()


13 底層API


14 debug
可以設置一個文件夾,javassist生成的class會保存在該文件夾下面。例如:CtClass.debugDump = "./dump"; 默認debugDump=null.


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