系列文章目录
第一章 读写字节码
第二章 类池
第三章 类加载器
第四章 内省和定制
前言
在上一章我们介绍了Javassist类加载器相关的知识,本章我们会介绍Javassist中的反思和定制。
内省和定制
CtClass提供了内省方法。Javassist的内省能力与Java反射API兼容。CtClass提供getName方法、getSuperclass方法和getMethods等方法。CtClasse还提供修改类定义的方法。它允许添加新的字段、构造函数和方法。插桩方法体也是可能的。
方法由CtMethod对象表示。CtMethod提供了几种修改方法定义的方法。请注意,如果方法是从父类继承的,那么表示继承方法的CtMethod对象也表示在父类中声明的该方法。CtMethod对象对应于每个方法声明。
例如,如果类Point声明了方法move,而Point的子类ColorPoint不重写move方法,则Point中声明的和ColorPoint中继承的两个move方法由相同的CtMethod对象表示。如果此CtMethod对象表示的方法定义被修改,则修改将同时反映在两个方法上。如果只想修改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类中,则应向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 package.
Javassist修改的类文件时仅当使用以$开头的一些特殊标识符时,才需要使用javassist 运行时包来支持。这些特殊标识符如下所述。在没有这些特殊标识符的情况下修改的类文件不需要javassist.runtime 包或其他任何Javassist运行时包。有关更多详细信息,请参阅javassist的API文档。javassist.runtime package。
4.1 在方法体的开头/结尾插入源文本
CtMethod和CtCructor提供insertBefore、insertAfter和addCatch方法。它们用于将代码片段插入到现有方法的体中。用户可以用Java编写的源文本具体说明这些代码片段。Javassist有一个用于处理源文本的简单的Java编译器。它接收Java编写的源文本,并将其编译成Java字节码,将其内联到方法体中。
也可以在行号指定的位置插入代码片段(如果行号表包含在类文件中)。CtMethod和CtCructor中的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, ... this和实际的参数
$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
变量 a r g s 表示所有参数的数组。该变量的类型是 O b j e c t 类的数组。如果参数类型是一个原始类型,例如 i n t 。那么参数值将转换为一个包装对象,例如 j a v a . l a n g . I n t e g e r ,以存储在 args表示所有参数的数组。该变量的类型是Object类的数组。如果参数类型是一个原始类型,例如int。那么参数值将转换为一个包装对象,例如java.lang.Integer,以存储在 args表示所有参数的数组。该变量的类型是Object类的数组。如果参数类型是一个原始类型,例如int。那么参数值将转换为一个包装对象,例如java.lang.Integer,以存储在args中。因此,$args[0]相当于 1 ,除非第一个参数的类型是基本数据类型。注意, 1,除非第一个参数的类型是基本数据类型。注意, 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);
}
要使用 c f l o w ,首先声明 cflow,首先声明 cflow,首先声明cflow用于监视对fact方法的调用:
CtMethod cm = ...;
cm.useCflow("fact");
useCflow方法的参数是声明的$cflow变量的标识符。任何有效的Java名称都可以用作标识符。因为标识符也可以包括.(点),例如,“my.Test.fact”也是一个有效的标识符。
然后, c f l o w ( f a c t ) 表示对 c m 指定的方法递归调用的深度。首次调用方法时, cflow(fact)表示对cm指定的方法递归调用的深度。首次调用方法时, cflow(fact)表示对cm指定的方法递归调用的深度。首次调用方法时,cflow(fact)的值为0(零)。而在方法内递归调用方法时为$cflow(fact)的值为1。例如
cm.insertBefore("if ($cflow(fact) == 0)"
+ " System.out.println(\"fact \" + $1);");
翻译方法fact,使其显示参数。由于检查了$cflow(fact)的值,因此如果在fact方法内递归调用,方法fact不会在控制台打印参数。
c f l o w 的值是当前线程当前最顶层堆栈帧下与指定方法 c m 关联的栈帧数。 cflow的值是当前线程当前最顶层堆栈帧下与指定方法cm关联的栈帧数。 cflow的值是当前线程当前最顶层堆栈帧下与指定方法cm关联的栈帧数。cflow也可以在与方法cm中不同的方法中访问。
$r
$r表示方法的结果类型(返回类型)。它必须用作转换表达式中的转换类型。例如,这是一个典型的用法:
Object result = ... ;
$_ = ($r)result;
如果结果类型是基本数据类型,则( r ) 遵循特殊语义。首先,如果转换表达式的操作类型是基本数据类型,则 ( r)遵循特殊语义。首先,如果转换表达式的操作类型是基本数据类型,则( r)遵循特殊语义。首先,如果转换表达式的操作类型是基本数据类型,则(r)用作结果类型的普通转换运算符。另一方面,如果操作类型是包装类型,( r ) 将从包装类型转换为基本数据类型。例如,如果结果类型为 i n t ,则 ( r)将从包装类型转换为基本数据类型。例如,如果结果类型为int,则( r)将从包装类型转换为基本数据类型。例如,如果结果类型为int,则(r)从java.lang.Integer转换为int类型。
如果结果类型为void,则( r ) 不转换类型;它什么也不做。但是,如果操作是对 v o i d 方法的调用,则 ( r)不转换类型;它什么也不做。但是,如果操作是对void方法的调用,则( r)不转换类型;它什么也不做。但是,如果操作是对void方法的调用,则(r)将导致结果为null。例如,如果结果类型为void,foo是void方法,则
$_ = ($r)foo();
是有效的语句。
强制转换运算符($r)在return语句中也很有用。即使结果类型为void,以下return语句也有效:
return ($r)result;
这里,结果是一些局部变量。由于指定了($r),因此结果值将被丢弃。此return语句等价于没结果值的return语句:
return;
$w
w 表示包装类型。它必须用作转换表达式中的类型转换。 ( w表示包装类型。它必须用作转换表达式中的类型转换。( w表示包装类型。它必须用作转换表达式中的类型转换。(w)从原始类型转换为相应的包装类型。以下代码是一个示例:
Integer i = ($w)5;
所选包装类型取决于($w)之后的表达式的类型。如果表达式的类型是double,那么包装类型是java.lang.Double。
如果( w ) 后面的表达式的类型不是基本数据类型,则 ( w)后面的表达式的类型不是基本数据类型,则( w)后面的表达式的类型不是基本数据类型,则(w)不执行任何操作。
$_
CtMethod和CtCconstructor中的insertAfter方法在方法末尾插入编译后的代码。在insertAfter方法的语句中,不仅可以使用上面显示的变量,例如$0、 1 … … ,还可以使用 1……,还可以使用 1……,还可以使用_。
变量 表 示方法的结果值。该变量的类型是方法结果的类型(返回类型)。如果结果类型为 v o i d ,则 _表示方法的结果值。该变量的类型是方法结果的类型(返回类型)。如果结果类型为void,则 表示方法的结果值。该变量的类型是方法结果的类型(返回类型)。如果结果类型为void,则_的类型为Object,$_的值为null。
尽管insertAfter方法插入的编译代码是在方法正常返回之前执行,但也可以在方法抛出异常时执行。要在异常时执行它,insertAfter方法的第二个参数asFinally必须为true。
如果抛出异常,insertAfter方法插入的编译代码将作为finally子句执行。在编译的代码中, 的 值为 0 或 n u l l 。编译代码的执行终止后,最初抛出的异常将重新抛出给调用者。注意, _的值为0或null。编译代码的执行终止后,最初抛出的异常将重新抛出给调用者。注意, 的值为0或null。编译代码的执行终止后,最初抛出的异常将重新抛出给调用者。注意,_的值永远不会被抛出给调用者;它被丢弃了。
$sig
$sig的值是java.lang.Class对象的数组,这些对象以声明的顺序表示形式参数类型。
$type
$type的值是一个java.lang.Class对象,表示结果值的类型。如果是构造函数,则此变量是指Void.class。
$class
$class的值是一个java.lang.Class对象,表示正在被编辑方法的类。它表示$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 更改方法体
CtMethod和CtCructor提供setBody方法来替换整个方法体。它们将给定的源文本编译成Java字节码,并将其替换为原始方法体。如果给定的源文本为null,除非方法返回类型为void,否则被替换的方法体仅包含一个返回零或null的return语句。
在setBody方法的源文本中,以$开头的标识符具有特殊含义。
$0, $1, $2, ... this和实际的参数
$args 参数数组,$args的类型是Object[]
$$ 所有实际的参数
$cflow(...) cflow 变量 (返回对特定方法的递归调用的深度)
$r 结果类型。它用于强制转换表达式中
$w 包装类型。它用于强制转换表达式中
$sig 表示形式参数类型的java.lang.Class对象数组
$type 表示正常结果类型的java.lang.Class对象
$class 表示当前编辑的类的java.lang.Class对象
请注意,$_不可用。
将源文本替换为现有表达式
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方法:
{ before-statements;
$_ = $proceed($$);
after-statements; }
无论表达式是方法调用、字段访问、对象创建还是其他。第二种语句可以是:
$_ = $proceed();
如果表达式是读访问,可以用以下语句
$proceed($$);
如果表达式是写访问。
如果instrument搜索的方法是使用-g选项编译的(类文件包含一个局部变量属性),则传递给replace方法的源文本中也可以使用目标表达式中可用的局部变量。
javassist.expr.MethodCall
MethodCall对象表示方法调用。MethodCall中的replace方法将语句或块替换为方法调用。它接收表示替换语句或块的源文本,其中以$开头的标识符在传递给insertBefore方法的源文本中具有特殊含义。
$0 方法调用的目标对象。这不等同于This,它表示此对象的调用方。如果方法是静态的,则$0为空。
$1,$2, …方法调用的参数。
$_ 方法调用的结果值。
$r 方法调用的结果类型。
$class 表示声明方法的类的java.lang.class对象。
$sig 表示正式参数类型的java.lang.Class对象数组。
$type 表示正式结果类型的java.lang.Class对象。
$proceed 最初在表达式中调用的方法的名称。
这里,方法调用是指MethodCall对象表示的方法调用。
其他标识符如$w、$args和$$也可用。
除非方法调用的结果类型为void,否则必须为源文本中的 赋 值,并且 _赋值,并且 赋值,并且_的类型为结果类型。如果结果类型为void,则 的 类型为 O b j e c t ,并忽略给分配 _的类型为Object,并忽略给分配 的类型为Object,并忽略给分配$_值。
$proceed不是字符串值,而是特殊语法。在它之后必须跟一个由括号()包围的参数列表。
javassist.expr.ConstructorCall
ConstructorCall对象表示构造函数调用,例如this方法和包含在构造函数主体中的super方法。ConstructorCall中的replace方法将语句或块替换为构造函数调用。它接收表示替换语句或块的源文本,其中以$开头的标识符在传递给insertBefore方法的源文本中具有特殊含义。
$0 构造函数调用的目标对象。这与此相当。
$1,$2, …构造函数调用的参数。
$class 表示声明构造函数的类的java.lang.class对象。
$sig 表示形式参数类型的java.lang.Class对象数组。
$proceed 最初在表达式中调用的构造函数的名称。
这里,构造函数调用是指ConstructorCall对象表示的调用。
其他标识符如$w、$ args和$$也可用。
由于任何构造函数都必须调用super类的构造函数或同类的另一个构造函数,因此替换语句必须包含构造函数调用,通常是对$proceed方法的调用。
$proceed不是字符串值,而是特殊语法。在它之后必须跟一个由括号()包围的参数列表。
javassist.expr.FieldAccess
FieldAccess对象表示字段访问。如果找到字段访问,ExprEditor中的edit方法将接收此对象。FieldAccess中的replace方法接收表示字段访问的替换语句或块的源文本。
在源文本中,以$开头的标识符具有特殊含义:
$0 包含表达式访问的字段的对象。这并不等同于this。this表示调用包含表达式的方法的对象。如果字段是静态的,则$0为空。
$1 如果表达式是写访问,则代表存储在字段中的值。否则,$1不可用。
$_ 如果表达式是读取访问,则代表字段访问的结果值。否则,$_中存储的值将被丢弃。
$r 如果表达式是读取访问,则代表字段的类型。否则,$r无效。
$class 表示声明字段的类的java.lang.class对象。
$type 表示字段类型的java.lang.Class对象。
$proceed 执行原始字段访问的虚拟方法的名称。
其他标识符如$w、$ args和$$也可用。
如果表达式是读访问,则必须在源文本中为 赋 值。 _赋值。 赋值。_的类型是字段的类型。
javassist.expr.NewExpr
NewExpr对象表示使用新运算符创建对象(不包括数组创建)。如果找到对象创建,ExprEditor中的edit方法将接收此对象。NewExpr中的replace方法接收表示对象创建的替换语句或块的源文本。
在源文本中,以$开头的标识符具有特殊含义:
$0 null。
$1,$2, …构造函数的参数。
$_ 对象创建的结果值。新创建的对象必须存储在此变量中。
$r 创建对象的类型。
$sig 表示形式参数类型的java.lang.Class对象数组。
$type 表示创建对象的类的java.lang.Class对象。
$proceed 执行原始对象创建的虚拟方法的名称。
其他标识符如$w、$ args和$$也可用。
javassist.expr.NewArray
NewArray对象表示使用新运算符创建的数组。如果找到数组创建,则ExprEditor中的edit方法将接收此对象。NewArray中的replace方法接收表示数组创建的替换语句或块的源文本。
在源文本中,以$开头的标识符具有特殊含义:
$0 null。
$1,$2, …每个维度的大小。
$_ 数组创建的结果值。新创建的数组必须存储在此变量中。
$r 创建的数组的类型。
$type 表示创建数组的类的java.lang.Class对象。
$proceed 执行原始数组创建的虚拟方法的名称。
其他标识符如$w、$args和$$也可用。
例如如果数组创建是以下表达式,
String[][] s = new String[3][4];
则$1和$2的值分别为3和4,$3不可用。
如果数组创建是以下表达式,
String[][] s = new String[3][];
则$1的值为3,但$2不可用。
javassist.expr.Instanceof
Instanceof对象表示instanceof表达式。如果找到instanceof表达式,则ExprEditor中的edit方法将接收此对象。Instanceof中的replace方法接收表示表达式的替换语句或块的源文本。
在源文本中,以$开头的标识符具有特殊含义:
$0 null。
$1 原始instanceof运算符左侧的值。
$_ 表达式的结果值。$_的类型是布尔型。
$r instanceof运算符右侧的类型。
$type 表示instanceof运算符右侧类型的java.lang.Class对象。
$proceed 执行原始instanceof表达式的虚拟方法的名称。它接受一个参数(类型为java.lang.Object)并且如果参数值是instanceof操作符右侧类型的实例的话返回true。否则,返回false。
其他标识符如$w、$args和$$也可用。
javassist.expr.Cast
Cast对象表示用于显式类型转换的表达式。如果发现显式类型转换,则ExprEditor中的edit方法将接收此对象。Cast中的replace方法接收表示表达式的替换语句或块的源文本。
在源文本中,以$开头的标识符具有特殊含义:
$0 null。
$1 显式转换类型的值。
$_ 表达式的结果值。$_的类型显式转换之后的类型相同,即由()包围的类型。
$r 显式转换后的类型,或由()包围的类型。
$type 表示与$r相同类型的java.lang.Class对象。
$proceed 执行原始类型转换的虚拟方法的名称。它接受java.lang.Object类型的一个参数,并在原始表达式指定的显式类型转换之后返回它。
其他标识符如$w、 $args和$$也可用。
javassist.expr.Handler
Handler对象表示try-catch语句的catch子句。如果发现catch,则ExprEditor中的edit方法将接收此对象。Handler中的insertBefore方法编译接收到的源文本,并将其插入catch子句的开头。
在源文本中,以$开头的标识符具有以下含义:
$1 catch子句捕获的异常对象。
$r catch子句捕获的异常的类型。它用于强制转换表达式中。
$w 包装器类型。它用于强制转换表达式中。
$type java.lang.Class对象表示catch子句捕获的异常的类型。
如果新的异常对象被分配给$1,它将作为捕获的异常传递给原始的catch子句。
4.3 添加新方法或字段
添加方法
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);
将公有方法xmove添加到类Point。在本例中,x是Point类中的int类型的字段。
传递给make方法的源文本可以包含除了$_之外的以$开头标识符,就像setBody方法中那样。如果目标对象和目标方法名也指定给了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方法后,必须显式地将类改回非抽象类。
相互递归方法
如果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方法会自动将类更改为抽象类。
添加字段
Javassist还允许用户创建新字段。
CtClass point = ClassPool.getDefault().get("Point");
CtField f = new CtField(CtClass.intType, "z", point);
point.addField(f);
该程序向Point类添加一个名为z的字段。
如果必须指定添加字段的初始值,则必须将上面展示的程序修改为:
CtClass point = ClassPool.getDefault().get("Point");
CtField f = new CtField(CtClass.intType, "z", point);
point.addField(f, "0"); // initial value is 0.```
现在,addField方法接收第二个参数,这是表示计算初始值的表达式的源文本。如果表达式的结果类型与字段的类型匹配,则该源文本可以是任何Java表达式。请注意,表达式不以分号(;)结尾。
此外,上述代码可以重写为以下简单代码:
```java
CtClass point = ClassPool.getDefault().get("Point");
CtField f = CtField.make("public int z = 0;", point);
point.addField(f);
删除成员
要删除字段或方法,请在CtClass中调用removeField方法或removeMethod方法。CtClass中的removeConstructor方法可以删除CtCconstructor。
4.4 注解
CtClass、CtMethod、CtField和CtCructor提供了一个方便的方法getAnnotations方法来读取注解。它返回注解类型对象。
例如,假设以下注解:
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类的注解仅为@Author,因此数组all的长度为1,all[0]是Author对象。可以通过对Author对象调用name方法和year方法来获得注解的成员值。
要使用getAnnotations方法,当前类路径中必须包含Author等注解类型。它们还必须可以从ClassPool对象中访问。如果找不到注解类型的类文件,Javassist将无法获取该注解类型成员的默认值。
4.5 运行时支持类
在大多数情况下,Javassist修改的类不需要Javassist运行。然而,Javassist编译器生成的某些字节码需要运行时支持类,这些类位于javassist.runtime包中(有关详细信息,请阅读该包的API参考)。注意javassist.runtime包是Javassist修改的类可能需要运行的唯一包。其他Javassist类从未在修改类的运行时使用。
4.6 导入
源代码中的所有类名必须是完全限定名(它们必须包括包名)。然而,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 限制
在当前实现中,Javassist中包含的Java编译器在编译器可以接受的语言方面有几个限制。这些限制如下:
J2SE 5.0引入的新语法(包括枚举和泛型)不被支持。Javassist的低级API支持注解。请参阅javassist.bytecode.annotation包(以及CtClass和CtBehavior中的getAnnotations方法)。泛型也只得到部分支持。有关详细信息,请参阅后一节。
数组值初始化,由括号{和}括起来的逗号分隔的表达式列表不可用,除非数组是一维数组。
不支持内部类或匿名类。注意,这只是编译器的限制。它无法编译包含匿名类声明的源代码。Javassist可以读取和修改内部/匿名类的类文件。
不支持带标签的continue和break语句。
编译器未正确实现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())。
建议用户使用#作为类名和静态方法或字段名之间的分隔符。例如在常规Java中,
javassist.CtClass.intType.getName()
对javassist.CtClass中静态字段intType指示的对象调用getName方法。在javassist中,用户可以编写上面显示的表达式,但建议他们编写:
```java
javassist.CtClass#intType.getName()
以便编译器可以快速解析表达式。
总结
本篇文章介绍了Javassist的常见标识符、更改方法体以及新增或删除方法和字段等。