【Javassist官方文档翻译】第四章 内省和定制

系列文章目录

第一章 读写字节码
第二章 类池
第三章 类加载器
第四章 内省和定制


前言

在上一章我们介绍了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。编译代码的执行终止后,最初抛出的异常将重新抛出给调用者。注意, 值为0null。编译代码的执行终止后,最初抛出的异常将重新抛出给调用者。注意,_的值永远不会被抛出给调用者;它被丢弃了。

$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  包含表达式访问的字段的对象。这并不等同于thisthis表示调用包含表达式的方法的对象。如果字段是静态的,则$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的常见标识符、更改方法体以及新增或删除方法和字段等。

说明

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值