CtClass提供了自省的方法。Javassist的内省能力与Java反射API兼容。CtClass提供了getName( ),getSuperclass( ),getMethods( )等方法。CtClass还提供了修改类定义的方法。 它允许添加一个新的字段,构造函数和成员方法。 检测方法体也是可以办到的。
方法由CtMethod对象表示。 CtMethod提供了几种修改方法定义的方法。 请注意,如果方法是从父类继承的,则表示相同继承方法的CtMethod对象表示在该父类中声明的方法。 CtMethod对象对应于每个方法声明。
例如,如果类Point声明方法move( )并且Point的子类ColorPoint不覆盖move( ),则在Point中声明并在ColorPoint中继承的两个move( )方法由相同的CtMethod对象表示。 如果修改了此CtMethod对象表示的方法定义,则修改将反映在这Point中的move( )和ColorPoint中的move( )这两个方法上。 如果只想修改ColorPoint中的move( )方法,则首先必须向子类ColorPoint添加表示父类Point中的move( )的CtMethod对象的副本。 CtMethod对象的副本可以通过CtNewMethod.copy( )方法获得。
Javassist不允许删除方法或字段,但允许更改名称。 所以如果一个方法不再是必要的,它应该被重命名,并通过调用在CtMethod中声明的setName( )和setModifiers( )方法来变成一个私有方法。
Javassist不允许向现有的方法添加额外的参数。 然而,可以通过在同一个类中添加一个同名的相同方法来接收额外的参数(重载)。例如,如果你想添加一个额外的int参数newZ到一个方法:
void move(int newX, int newY) { x = newX; y = newY; }
如果想在Point类的move方法中新加入一个参数,我们应该在Point类中新加入一个方法:
void move(int newX, int newY, int newZ) {
// do what you want with newZ.
move(newX, newY);
}
Javassist还提供低级API以直接编辑原始类文件。 例如,CtClass中的getClassFile( )返回表示原始类文件的ClassFile对象。CtMethod中的getMethodInfo( )返回一个MethodInfo对象,表示一个包含在类文件中的method_info结构。 低级API使用Java虚拟机规范中的词汇表。用户必须具有关于类文件和字节码的知识。 有关更多详细信息,用户应该看到javassist.bytecode包。
由Javassist修改的类文件只有在使用了以$开头的特殊标识符时,才需要javassist.runtime包用于运行时支持。 这些特殊的标识符如下所述。 在没有这些特殊标识符的情况下修改的类文件在运行时不需要javassist.runtime包或任何其他Javassist包。 有关更多详细信息,请参阅javassist.runtime包的API文档。
4.1 Inserting source text at the beginning/end of a method body
CtMethod和CtConstructor提供了insertBefore( ),insertAfter( )和addCatch( )方法。它们用于将代码片段插入到现有方法的方法体中。用户可以用Java编写的源文本指定这些代码片段。Javassist包含一个用于处理源文本的简单Java编译器。 它接收用Java编写的源文本并将其编译为Java字节码,该字节码将被内联到方法体中。
通过行号将代码片段插入到指定的位置也是可能的(前提是行号表包含在类文件中)。CtMethod和CtConstructor中的insertAt( )方法从原始类文件定义中获取原文本和行号,它编译源文本并在行号处插入编译的代码。
insertBefore( ),insertAfter( ),addCatch( )和insertAt( )方法接收一个语句或块所表示的String对象。声明是一个单一的控制结构,如if和while或以分号(;)结尾的表达式。块是用大括号{}包围的一组语句。 因此,以下每行都是有效语句或块的示例:
System.out.println("Hello");
{ System.out.println("Hello"); }
if (i < 0) { i = -i; }
语句和块可以引用字段和方法。如果使用-g选项编译该方法(在类文件中包括本地变量属性),则也可以将参数引用到插入的方法中。否则,他们必须通过下面描述的特殊变量$0
,$1
,$2
,… 来访问方法参数。访问方法中声明的局部变量是不允许的,但是允许在块中声明一个新的局部变量。但是,如果这些变量在指定的行号可用并且目标方法是使用-g选项编译的,那么insertAt( )方法允许语句和块访问本地变量。
传递给方法insertBefore( ),insertAfter( ),addCatch( )和insertAt( )的String对象由Javassist中包含的编译器编译。 由于编译器支持语言扩展,因此以$
开头的几个标识符具有特殊含义:
$0,$1,$2, ... 实际参数;
$args 一个参数数组。 $args的类型是Object[];
$$ 所有实际参数。例如,m($$)相当于m($ 1,$ 2,...);
$cflow(...) cflow变量;
$r 结果类型。 它被用在一个表达式中;
$w 包装类型。 它被用在一个表达式中;
$ _结果值;
$sig 表示形式参数类型的java.lang.Class对象数组;
$type 表示正式结果类型的java.lang.Class对象;
$class 表示当前编辑的类的java.lang.Class对象;
$0, $1, $2, ...
传递给目标方法的参数可以用$1
,$2
…来访问,而不是原始的参数名称。$1
表示第一个参数,$2
表示第二个参数,依此类推。这些变量的类型与参数类型相同。 $0
相当于this。 如果该方法是静态的,则$0
不可用。
这些变量的用法如下。 假设一个类Point:
class Point {
int x, y;
void move(int dx, int dy) { x += dx; y += dy; }
}
在调用move( )方法时,要打印dx和dy的值,请执行以下程序:
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get("Point");
CtMethod m = cc.getDeclaredMethod("move");
m.insertBefore("{ System.out.println($1); System.out.println($2); }");
cc.writeFile();
请注意,传递给insertBefore( )方法的源文本被大括号{ }包围。 insertBefore( )方法只接受单个语句或用大括号包围块。
修改后的类Point的定义如下:
class Point {
int x, y;
void move(int dx, int dy) {
{ System.out.println(dx); System.out.println(dy); }
x += dx; y += dy;
}
}
$1
和 $2
分别替换为dx和dy。
$1
,$2
,$3
…是可更新的。 如果将新值分配给其中一个变量,则由该变量表示的参数值也会更新。
$args
变量$args
表示一个所有参数的数组。该变量的类型是一个Object类的数组。如果参数类型是像int这样的基本类型,那么参数值将被转换成一个包装对象,比如java.lang.Integer,然后储在$args
中。因此,$args[0]
相当于$ 1
(除非第一个参数的类型是基本类型)。 请注意,$args[0]
不等于$0
, $0
表示this。
如果将一个Object数组分配给$args,那么该数组的每个元素都会分配给每个参数。 如果参数类型是基本类型,则相应元素的类型必须是包装类型。 在将值分配给参数之前,该值将从包装器类型转换为基元类型。
$$
变量$$
是以逗号分隔的所有参数列表的缩写。 例如,如果方法move( )方法的参数数量是3,那么:
move($$)
等价于:
move($1, $2, $3)
如果move( )方法没有任何参数,则move($$)等价于move( ).
$$
可以与其他方法一起使用。 如果你写一个表达式:
exMove($$, context)
等价于:
exMove($1, $2, $3, context)
请注意,$$
通用符号表示允许方法调用的参数的数量。 它通常与稍后显示的$proceed
一起使用。
$cflow
$cflow
表示“控制流”。 此只读变量将递归调用的深度返回给特定的方法。
假设下面显示的方法由CtMethod对象cm表示:
int fact(int n) {
if (n <= 1)
return n;
else
return n * fact(n - 1);
}
要使用$cflow
,首先声明$cflow
用于监视方法fact( )的调用:
CtMethod cm = ...;
cm.useCflow("fact");
useCflow( )方法的参数是声明$cflow
变量的标识符。任何有效的Java名称都可以用作标识符。并且标识符包含“.”(dot),例如:“my.Test.fact”就是一个有效的标识符。
然后,$cflow(fact)
表示对cm指定的方法的递归调用的深度。当方法被第一次调用时,$cflow(fact)
的值为0(零),而在方法中递归调用方法时,它的值为1。 例如:
cm.insertBefore("if ($cflow(fact) == 0)"
+ " System.out.println(\"fact \" + $1);");
translates the method fact() so that it shows the parameter.
Since the value of $cflow(fact) is checked, the method fact() does not show the parameter if it is recursively called within fact().
The value of $cflow is the number of stack frames associated with the specified method cm under the current topmost stack frame for the current thread.
$cflow 也可以在不同于指定方法cm的方法中访问。
$r
$r
表示方法的结果类型(返回类型)。它必须当做转型表达式的转型类型来使用。典型的例子:
Object result = ... ;
$_ = ($r)result;
如果结果类型是基本类型,那么 ($r
) 遵循特殊的语义。 首先,如果转换表达式的操作数类型是原始类型,则 ($r
) 作为结果类型的普通转换运算符。 另一方面,如果操作数类型是包装类型,则 ($r
) 将从包装类型转换为结果类型。 例如,如果结果类型是int,则 ($r
) 将从java.lang.Integer转换为int。
如果结果类型是void,那么 ($r
) 不会转换类型; 它什么都不做。 但是,如果操作数是对void方法的调用,则 ($r
) 的结果为null。 例如,如果结果类型是void而foo( )是一个void方法,那么:
$_ = ($r)foo();
是一个有效的表达式。
转换运算符 ($r
) 在返回语句中也很有用。 即使结果类型是无效的,下面的返回语句也是有效的:
return ($r)result;
在这里,result是一个局部变量。 由于 ($r
) 被指定,所以产生的值被丢弃。 这个返回语句被认为是没有结果值的返回语句的等价物:
return;
$w
$w
代表一个包装类型。它必须当做转型表达式的转型类型来使用。($w
) 从原始类型转换为相应的包装器类型。 例如:
Integer i = ($w)5;
所选的包装类型取决于 ($w
) 后面的表达式的类型。 如果表达式的类型是double,那么包装类型是java.lang.Double。
如果 ($w
) 后面的表达式的类型不是原始类型,那么 ($w
) 不做任何事情。
$_
CtMethod和CtConstructor中的insertAfter( )方法将编译后的代码插入到方法的末尾。在insertAfter( )插入的语句中,不仅变量$0
,$1
,…是可以用的,还有 $_
是可用的。
变量$_
表示方法的结果值。 该变量的类型是该方法的结果类型(返回类型)。如果结果类型是void,那么$_
的类型是Object,$_
的值是null。
虽然insertAfter( )插入的编译代码在方法返回之前被执行,但是当方法抛出异常时也可以执行该代码。为了在异常抛出时执行这段代码,必须要将insertAfter( )方法的第二个参数设置成true。
如果引发异常,insertAfter( )方法插入的编译代码将作为finally子句执行。$_
的值在编译的代码中为0或null。 编译后的代码执行终止后,最初抛出的异常被重新抛出给调用者。 请注意 $_
的值永远不会被抛给调用者,而是被丢弃。
$sig
$sig
的值是一个按顺序存储声明的形式参数类型的java.lang.Class对象数组。
$type
$type
的值是一个表示结果值类型的java.lang.Class对象。 如果这是一个构造函数,这个变量引用Void.class。
$class
The value of $class
is an java.lang.Class object representing the class in which the edited method is declared. This represents the type of $0
.
addCatch()
addCatch()方法将一个代码片段插入到方法体中,以便在方法体引发异常时执行代码片段,并返回给调用者。在代表插入代码片段的源文本中,异常值用特殊变量$e来引用。例如:
CtMethod m = ...;
CtClass etype = ClassPool.getDefault().get("java.io.IOException");
m.addCatch("{ System.out.println($e); throw $e; }", etype);
将由m表示的方法体转换成如下形式:
try {
// the original method body
}
catch (java.io.IOException e) {
System.out.println(e);
throw e;
}
请注意,插入的代码段必须以throw或return语句结束。
4.2 Altering a method body
CtMethod和CtConstructor提供setBody( )方法来替换方法中的整个方法体。 它将给定的源文本编译成Java字节码,并用其替换原始方法体。 如果给定的源文本为null,则替换的主体只包含一个返回语句,该语句返回零或空值,除非方法结果类型为void。
在赋给setBody( )方法的源文本中,以$开头的标识符具有特殊含义:
$0, $1, $2, ... this 和 实际参数;
$args 一个参数数组,类型为Object[];
$$ 所有实际参数;
$cflow(...) cflow 变量;
$r 结果类型。它被用在转型表达式中;
$w 包装类型。它被用在转型表达式中;
$sig 一个代表形参类型的java.lang.Class 对象数组;
$type 一个代表正式结果类型的java.lang.Class 对象;
$class 表示声明该方法的类的java.lang.Class 对象;当前正在编辑的类,使用 $0 表示;
注意: $r
不可使用。
用源文本替换现有的表达式
Javassist只允许修改方法体中包含的表达式。 javassist.expr.ExprEditor是一个替换方法体中的表达式的类。 用户可以定义ExprEditor的子类来指定如何修改表达式。
要运行ExprEditor对象,用户必须在CtMethod或CtClass中调用instrument( ) 方法。 例如:
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($$); }");
}
});
搜索由cm表示的方法体,并用如下的块代替所有调用的类Point中move( )方法:
{ $1 = 0; $_ = $proceed($$); }
所以move( )方法的第一个参数总是0。请注意,替换后的代码不是表达式,而是语句或块。 它不能 是或包含 一个try-catch语句。
instrument( )方法搜索一个方法体。 如果它发现了诸如方法调用,字段访问和对象创建之类的表达式,那么它会在给定的ExprEditor对象上调用edit。 edit( )方法,参数是一个代表找到的表达式的对象。 edit( )方法可以通过该对象检查和替换表达式。
调用edit( ) 中的replace( ) 方法,replace( ) 方法的参数将给定方法中的语句或块替换为表达式。如果给定的块是空块,也就是说,如果执行replace(“{}”),则表达式将从方法体中移除。如果要在表达式之前/之后插入一个语句(或一个块),应将如下所示的块传递给replace( ):
{ before-statements;
$_ = $proceed($$);
after-statements; }
无论表达式是方法调用,字段访问,对象创建还是其他。 第二个陈述可以是:
$_ = $proceed();
if the expression is read access,or
$proceed($$);
if the expression is write access.
如果instrument( )搜索的方法是使用-g选项(类文件包含本地变量属性)编译的,那么在传递给replace( )的源文本中也可以使用目标表达式中可用的局部变量。
javassist.expr.MethodCall
javassist.expr.ConstructorCall
javassist.expr.FieldAccess
javassist.expr.NewExpr
javassist.expr.NewArray
javassist.expr.Instanceof
javassist.expr.Cast
javassist.expr.Handler
4.3 Adding a new method or field
Adding a method
Javassist允许用户创建一个新的方法和构造函数。 CtNewMethod和CtNewConstructor提供了几个工厂方法,这些方法是用于创建CtMethod或CtConstructor对象的静态方法。 特别的是,make( )方法从给定的源文本中创建一个CtMethod或CtConstructor对象。
例如:
CtClass point = ClassPool.getDefault().get("Point");
CtMethod m = CtNewMethod.make("public int xmove(int dx) { x += dx; }", point);
point.addMethod(m);
将一个public方法xmove( )添加到类Point中。 在这个例子中,x是类Point中的一个int类型的字段。
传递给make( )的源文本可以包含以 开头的标识符,和setBody()一样除了 开 头 的 标 识 符 , 和 s e t B o d y ( ) 一 样 除 了 _ 。如果目标对象和目标方法名也传给make( ),那么它也可以包含$proceed。例如:
CtClass point = ClassPool.getDefault().get("Point");
CtMethod m = CtNewMethod.make("public int ymove(int dy) { $proceed(0, dy); }", point, "this", "move");
这个程序创建一个方法ymove( )定义如下:
public int ymove(int dy) { this.move(0, dy); }
请注意,$proceed
已被替换为this.move。
Javassist提供了另一种添加新方法的方式。 你可以先创建一个抽象方法,然后给它一个方法体:
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); // 注意将类改回非抽象类
由于Javassist在抽象方法被添加到类中时产生了抽象类,所以在调用setBody( )之后,必须显式地将类更改回非抽象类。
Mutual recursive methods
如果Javassist调用另一个尚未添加到类中的方法,Javassist将无法编译该方法。Javassist可以编译一个递归调用自己的方法。要将相互递归方法添加到类中,您需要一个如下所示的技巧。 假设你想把方法 m( ) 和 n( ) 添加到由cc表示的类中:
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);
您必须先制作两个抽象方法并将其添加到类中。 然后,即使方法体包含相互方法调用,也可以将方法体提供给这些方法。最后,您必须将该类更改为非抽象类,因为如果添加了抽象方法,addMethod( )会自动将类更改为抽象类。
Adding a field
Javassist也允许用户创建一个新的字段。
CtClass point = ClassPool.getDefault().get("Point");
CtField f = new CtField(CtClass.intType, "z", point);
point.addField(f);
这段代码将一个名为z的字段添加到类Point中。
如果必须指定添加字段的初始值,则上面的代码必须修改为:
CtClass point = ClassPool.getDefault().get("Point");
CtField f = new CtField(CtClass.intType, "z", point);
point.addField(f, "0"); // initial value is 0.
现在,addField( )方法接收第二个参数,它是表示计算初始值的表达式的源文本。如果表达式的结果类型与字段类型匹配,则此源文本可以是任何Java表达式。 请注意,表达式不以分号(;)结束。
此外,上面的代码可以被重写成下面的简单代码:
CtClass point = ClassPool.getDefault().get("Point");
CtField f = CtField.make("public int z = 0;", point);
point.addField(f);
Removing a member
要删除字段或方法,请在CtClass中调用removeField( )或removeMethod( )。 CtConstructor可以通过CtClass中的removeConstructor( )来移除。
4.4 Annotations
CtClass,CtMethod,CtField和CtConstructor提供了一个方便的方法getAnnotations( )来读取 annotations。 它返回一个annotations类型的对象。
例如,假设有一下注解:
public @interface Author {
String name();
int year();
}
那么上面定义的这个注解可以这样来使用:
@Author(name="Chiba", year=2005)
public class Point {
int x, y;
}
然后,注解的值可以通过getAnnotations( )方法来获得。 它返回一个包含注解类型对象的数组。
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);
这段代码将打印:
name: Chiba, year: 2005
由于Point的annoation只有@Author,数组的长度都是1,所以all[0]是Author对象。 注解的成员值可以通过调用Author对象的name( )和year( )来获得。
要使用getAnnotations( )方法,注解类型(如Author)必须包含在当前类路径中。它们也必须可以从ClassPool对象访问。如果未找到注解类型的类文件,Javassist将无法获得该注解类型成员的默认值。
4.5 Runtime support classes
In most cases, a class modified by Javassist does not require Javassist to run.但是,由Javassist编译器生成的某些类型的字节码需要运行时支持类,它们位于javassist.runtime包中(有关详细信息,请阅读该包的API参考)。Note that the javassist.runtime package is the only package that classes modified by Javassist may need for running. The other Javassist classes are never used at runtime of the modified classes.
4.6 Import
源代码中的所有类名必须是完全限定的(它们必须包括包名)。 但是,java.lang包是个例外, 例如,Javassist编译器可以解析Object以及java.lang.Object。
要告诉编译器在解析类名时搜索其他包,在ClassPool中调用importPackage( ) 方法。 例如:
ClassPool pool = ClassPool.getDefault();
pool.importPackage("java.awt");
CtClass cc = pool.makeClass("Test");
CtField f = CtField.make("public Point p;", cc);
cc.addField(f);
第二行指示编译器导入java.awt包。 因此,第三行不会抛出异常。 编译器可以将Point识别为java.awt.Point。
请注意,importPackage( )方法不会影响ClassPool中的get( )方法。 只有编译器才会考虑导入的包。 因此get( )的参数必须始终是完全限定的名称。
4.7 Limitations
在当前的实现中,Javassist中包含的Java编译器在编译器可以接受的语言方面有一些限制。 这些限制是:
1、J2SE 5.0引入的新语法(包括枚举和泛型)不被支持。 注释由Javassist的低级API支持。 请参阅javassist.bytecode.annotation包(以及CtClass和CtBehavior中的getAnnotations( ))。 泛型也只是部分支持。 有关更多详细信息,请参阅后一节。
2、除非数组维度是1,否则数组初始化程序(一个逗号分隔的用括号括起来的表达式{and}的列表)不可用。
3、内部类或匿名类不受支持。 请注意,这只是编译器的限制。 它不能编译包括匿名类声明的源代码。 Javassist可以读取和修改内部/匿名类的类文件。
4、不支持continue和break语句。
5、编译器不能正确实现Java方法调度算法。 如果类中定义的方法具有相同的名称,但是采用不同的参数列表,则编译器可能会产生混淆。
class A {}
class B extends A {}
class C extends B {}
class X {
void foo(A a) { .. }
void foo(B b) { .. }
}
如果编译的表达式是x.foo(new C( ) ),其中x是X的一个实例,编译器可能会调用foo(A),尽管编译器可以正确编译foo( (B) new C( ) )。
6、建议用户使用#作为类名和静态方法或字段名之间的分隔符。 例如,在普通的Java中:
“`java
javassist.CtClass.intType.getName()
在由javassist.CtClass中的静态字段intType指示的对象上调用方法getName( )。 在Javassist中,用户可以编写上面显示的表达式,但是建议编写:
```java
javassist.CtClass#intType.getName()
以便编译器可以快速解析表达式。