Skip to content

自省和自定义

CtClass提供了内省的方法。Javassist的内省功能与Java反射API兼容。CtClass提供了getName()、getSuperclass()、getMethods()等等。CtClass还提供了修改类定义的方法。它允许添加新的字段、构造函数和方法。插装方法体也是可能的。

方法由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:

java
void move(int newX, int newY) { x = newX; y = newY; }

在Point类中,那么你应该向Point类添加以下方法:

java
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。字节码方案。

只有在使用一些以$开头的特殊标识符时,由Javassist修改的类文件才需要Javassist .runtime包来支持运行时。下面将描述这些特殊标识符。在没有这些特殊标识符的情况下修改的类文件在运行时不需要Javassist .runtime包或任何其他Javassist包。有关详细信息,请参阅javassist.runtime包的API文档。

在方法体的开头/结尾插入源文本

CtMethod和CtConstructor提供了insertBefore()、insertAfter()和addCatch()方法。它们用于将代码片段插入到现有方法的主体中。用户可以用Java编写的源文本指定这些代码片段。Javassist包括一个简单的Java编译器,用于处理源文本。它接收用Java编写的源文本并将其编译为Java字节码,该字节码将内联到方法体中。

在由行号指定的位置插入代码片段也是可能的(如果行号表包含在类文件中)。CtMethod和CtConstructor中的insertAt()接受源文本和原始类定义源文件中的行号。它编译源文本并在行号处插入编译后的代码。

insertBefore()、insertAfter()、addCatch()和insertAt()方法接收一个表示语句或块的String对象。语句是一个单一的控制结构,如if和while或以分号(;)结尾的表达式。块是一组用大括号{}括起来的语句。因此,下面的每一行都是一个有效语句或代码块的例子:

java
System.out.println("Hello");
{ System.out.println("Hello"); }
if (i < 0) { i = -i; }

语句和块可以引用字段和方法。如果方法是用-g选项编译的(在类文件中包含一个局部变量属性),它们还可以将参数引用到插入方法的方法。否则,它们必须通过特殊变量$0,$1,$2,…来访问方法参数。下面描述。不允许访问方法中声明的局部变量,但允许在块中声明新的局部变量。但是,insertAt()允许语句和块访问局部变量,如果这些变量可用的话

传递给insertBefore()、insertAfter()、addCatch()和insertAt()方法的String对象由Javassist中包含的编译器编译。由于编译器支持语言扩展,以$开头的几个标识符具有特殊含义:

$0, $1, $2,…this和实际参数
$args   参数数组。$args的类型是Object[]。
$$      所有实际参数。例如,m($$)等价于m($1,$2,…)
$cflow(…)   cflow变量
$r      结果类型。它用于强制转换表达式。
$w      包装器类型。它用于强制转换表达式。
$_      结果值,代表形式参数类型的java.lang.Class对象数组。
$type   表示正式结果类型的java.lang.Class对象。
$class  一个java.lang.Class对象,表示当前编辑的类。

$0, $1, $2, ...

传递给目标方法的参数可以通过$1,$2,…而不是原来的参数名称。$1表示第一个参数,$2表示第二个参数,依此类推。这些变量的类型与参数类型相同。$0等于this。如果方法是静态的,则$0不可用。

这些变量的使用方法如下。假设有一个类Point:

java
class Point {
    int x, y;
    void move(int dx, int dy) { x += dx; y += dy; }
}

要在调用move()方法时打印dx和dy的值,请执行以下程序:

java
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类定义如下:

java
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,则

bash
move($$)
// 两者对等
move($1, $2, $3)

如果move()不带任何参数,那么move($$)等价于move()。$$可以与另一种方法一起使用。如果你写一个表达式:

exMove($$, context)

那么这个表达式等价于:

exMove($1, $2, $3, context)

请注意,$$允许根据参数数量对方法调用进行通用标记。它通常与后面显示的$proceed一起使用。

$cflow

$cflow表示“控制流”。这个只读变量返回对特定方法的递归调用的深度。

假设下面所示的方法由一个CtMethod对象cm表示:

java
int fact(int n) {
    if (n <= 1)
        return n;
    else
        return n * fact(n - 1);
}

要使用$cflow,首先声明$cflow用于监视对方法fact()的调用:

java
CtMethod cm = ...;
cm.useCflow("fact");

useCflow()的参数是声明的$cflow变量的标识符。任何有效的Java名称都可以用作标识符。因为标识符还可以包括。(点),例如,“my.Test.”“事实”是一个有效的标识符。

然后,$cflow(fact)表示对cm指定的方法的递归调用的深度。当第一次调用该方法时,$cflow(fact)的值为0(零),而当在该方法内递归调用该方法时,它的值为1。例如,

java
cm.insertBefore("if ($cflow(fact) == 0)"
              + "    System.out.println(\"fact \" + $1);");

转换方法fact(),使其显示参数。由于检查了$cflow(fact)的值,因此如果在fact()中递归调用fact()方法,则fact()方法不会显示该参数。

$cflow的值是在当前线程的当前最顶层堆栈帧下与指定方法cm相关联的堆栈帧数。$cflow也可以在不同于指定方法cm的方法中访问。

$r

$r表示方法的结果类型(返回类型)。它必须用作强制转换表达式中的强制转换类型。例如,这是一个典型的用法:

java
Object result = ... ;
$_ = ($r)result;

如果结果类型是原始类型,则($r)遵循特殊语义。首先,如果强制转换表达式的操作数类型是基本类型,则($r)作为对结果类型的普通强制转换操作符。另一方面,如果操作数类型是包装器类型,($r)将从包装器类型转换为结果类型。例如,如果结果类型是int,那么($r)将从java.lang.Integer转换为int。

如果结果类型为void,则($r)不转换类型;它什么也不做。但是,如果操作数是对void方法的调用,则($r)的结果为null。例如,如果结果类型为void并且foo()是一个void方法,那么

$_ = ($r)foo();

强制转换操作符($r)在返回语句中也很有用。即使结果类型为void,以下返回语句也是有效的:

java
return ($r)result;

这里,result是某个局部变量。由于指定了($r),结果值将被丢弃。这条return语句被认为等同于没有结果值的return语句:

java
return;

$w

$w表示包装器类型。它必须用作强制转换表达式中的强制转换类型。($w)将原始类型转换为相应的包装类型。如下代码为示例:

java
Integer i = ($w)5;

选择的包装器类型取决于后面的表达式($w)的类型。如果表达式的类型是double,那么包装器类型就是java.lang.Double。

如果($w)后面的表达式类型不是基本类型,则($w)不执行任何操作。

$_

CtMethod和CtConstructor中的insertAfter()在方法的末尾插入编译后的代码。在给insertAfter()的语句中,不仅上面显示的变量如$0,$1,…但是也可以使用$_。

变量$_表示该方法的结果值。该变量的类型是方法的结果类型(返回类型)的类型。如果结果类型为void,则$_的类型为Object, $_的值为null。

虽然insertAfter()插入的编译代码是在控件通常从方法返回之前执行的,但是在从方法抛出异常时也可以执行。要在抛出异常时执行它,insertAfter()的第二个参数asFinally必须为真。

如果抛出异常,insertAfter()插入的编译代码将作为finally子句执行。编译后的代码中$_的值为0或null。编译后的代码执行结束后,最初抛出的异常被重新抛出给调用者。注意$_的值永远不会被抛出给调用者;它被抛弃了。

$sig

$sig的值是java.lang.Class对象的数组,这些对象按照声明顺序表示形式参数类型。

$type

$type的值是一个java.lang.Class对象,表示结果值的形式类型。如果这是一个构造函数,则该变量引用Void.class。

$class

$class的值是一个java.lang.Class对象,表示在其中声明编辑方法的类。这表示$0的类型。

addCatch()

addCatch()将代码片段插入方法体中,以便在方法体抛出异常并将控制返回给调用者时执行该代码片段。在表示插入代码片段的源文本中,异常值使用特殊变量$e来引用。

java
CtMethod m = ...;
CtClass etype = ClassPool.getDefault().get("java.io.IOException");
m.addCatch("{ System.out.println($e); throw $e; }", etype);

将m表示的方法体转换成这样的形式:

java
try {
    the original method body
}
catch (java.io.IOException e) {
    System.out.println(e);
    throw e;
}

注意,插入的代码片段必须以throw或return语句结束。

修改方法体

CtMethod和CtConstructor提供了setBody()来替换整个方法体。它们将给定的源文本编译成Java字节码,并用它代替原始方法体。如果给定的源文本为空,则替换的正文只包含一个返回语句,该语句返回零或空,除非结果类型为空。

在给定给setBody()的源文本中,以$开头的标识符具有特殊含义

$0, $1, $2,   …这个和实际参数
$args           参数数组。$args的类型是Object[]。
$$              所有实际参数。
$cflow(…)       cflow变量
$r              结果类型。它用于强制转换表达式。
$w              包装器类型。它用于强制转换表达式。
$sig            代表形式参数类型的java.lang.Class对象数组。
$type	        表示正式结果类型的java.lang.Class对象。
$class	        一个java.lang.Class对象,表示声明该方法的类当前已编辑(类型为$0)。

将源文本替换为现有表达式

Javassist只允许修改方法体中包含的表达式。expreditor是一个用于替换方法体中的表达式的类。用户可以定义exppreditor的子类来指定如何修改表达式。

要运行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表示的方法体,并用block替换Point类中对move()的所有调用:

{ $1 = 0; $_ = $proceed($$); }

因此move()的第一个参数总是0。请注意,替换的代码不是表达式,而是语句或块。它不能是或包含try-catch语句。

方法instrument()搜索方法体。如果它找到一个表达式,比如方法调用、字段访问和对象创建,那么它就在给定的ExprEditor对象上调用edit()。edit()的参数是一个表示找到的表达式的对象。edit()方法可以通过该对象检查和替换表达式。

在参数上调用replace()以edit()替换给定的语句或块。如果给定的块是空块,也就是说,如果执行replace("{}"),则从方法体中删除表达式。如果你想在表达式之前/之后插入一个语句(或块),应该传递一个像下面这样的块来替换():

bash
{ before-statements;
  $_ = $proceed($$);
  after-statements; }

无论表达式是方法调用、字段访问、对象创建还是其他。第二种说法可以是:

bash
$_ = $proceed();

如果表达式为读访问,则为

bash
$proceed($$);

如果表达式是写访问。如果instrument()搜索的方法是用-g选项编译的(类文件包含一个局部变量属性),那么在传递给replace()的源文本中,目标表达式中可用的局部变量也可用。

javassist.expr.MethodCall

MethodCall对象表示一个方法调用。MethodCall中的replace()方法用语句或块代替方法调用。它接收表示被替换语句或块的源文本,其中以$开头的标识符与传递给insertBefore()的源文本一样具有特殊含义。

$0      方法调用的目标对象。This不等同于This,后者表示调用方This对象。如果方法是静态的,则$0为null。1美元,2美元,……方法调用的参数。
$_      方法调用的结果值。
$r      方法调用的结果类型。
$class  一个java.lang.Class对象,表示声明该方法的类。
$sig    代表形式参数类型的java.lang.Class对象数组。
$type   表示正式结果类型的java.lang.Class对象。
$proceed    表达式中最初调用的方法的名称

这里的方法调用是指由MethodCall对象表示的方法调用。其他标识符,如$w、$args和$$也可用。

除非方法调用的结果类型为void,否则必须在源文本中将值赋给$_,并且$_的类型为结果类型。如果结果类型为void,则$_的类型为Object,并且分配给$_的值将被忽略。

$proceed不是一个字符串值,而是一个特殊语法。它必须后跟一个由圆括号()包围的参数列表。

javassist.expr.ConstructorCall

ConstructorCall对象表示构造函数调用,例如this()和包含在构造函数体中的super。ConstructorCall中的replace()方法用语句或块代替构造函数调用。它接收表示被替换语句或块的源文本,其中以$开头的标识符与传递给insertBefore()的源文本一样具有特殊含义。

$0          构造函数调用的目标对象。这个和this是等价的。
$1, $2,     ……构造函数调用的参数。
$class      一个java.lang.Class对象,表示声明构造函数的类。
$sig        代表形式参数类型的java.lang.Class对象数组。
$proceed    表达式中最初调用的构造函数的名称

这里的构造函数调用是指由ConstructorCall对象表示的构造函数调用。其他标识符,如$w、$args和$$也可用。

由于任何构造函数都必须调用父类的构造函数或同一类的另一个构造函数,因此替换语句必须包含构造函数调用,通常是对$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对象。
执行原始字段访问的虚拟方法的名称

其他标识符,如$w、$args和$$也可用。

如果表达式是读访问,则必须给源文本中的$_赋值。$_的类型是字段的类型。

javassist.expr.NewExpr

NewExpr对象表示使用new操作符创建对象(不包括创建数组)。如果找到对象创建,ExprEditor中的edit()方法将接收此对象。NewExpr中的replace()方法接收表示用于对象创建的替换语句或块的源文本。

在源文本中,以$开头的标识符具有特殊含义:

$0          null。
$1,$2,    ……构造函数的参数。
$_          对象创建的结果值。新创建的对象必须存储在这个变量中。
$r          创建对象的类型。
$sig        代表形式参数类型的java.lang.Class对象数组。
$type       一个java.lang.Class对象,表示所创建对象的类。
$proceed    执行原始对象创建的虚拟方法的名称。

其他标识符,如$w、$args和$$也可用。

例如,如果数组创建为以下表达式:

java
String[][] s = new String[3][4];

那么$1和$2的值分别是3和4。$3是不可用的。

如果创建的数组是以下表达式:

java
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   一个java.lang.Class对象,表示instanceof操作符右侧的类型。
$proceed    执行原始instanceof表达式的虚拟方法的名称。它接受一个参数(类型为java.lang.Object)并返回true的右侧类型的实例原始的instanceof操作符。否则,它return false

其他标识符,如$w、$args和$$也可用。

javassist.expr.Cast

Cast对象表示显式类型转换的表达式。如果发现显式类型转换,ExprEditor中的方法edit()将接收此对象。Cast中的replace()方法接收表示被替换语句或表达式块的源文本。

在源文本中,以$开头的标识符具有特殊含义:

$0      null。
$1      类型被显式强制转换的值。
$_      表达式的结果值。$_的类型与类型相同在显式强制转换之后,即被()包围的类型。
$r      是显式强制转换后的类型,或者被()包围的类型。
$type   一个java.lang.Class对象,表示与$r相同的类型。
$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子句。

添加新方法或字段

Adding a method

Javassist允许用户从头创建新方法和构造函数。CtNewMethod和CtNewConstructor提供了几个工厂方法,它们是用于创建CtMethod或CtConstructor对象的静态方法。特别是,make()从给定的源文本创建一个CtMethod或CtConstructor对象。

java
CtClass point = ClassPool.getDefault().get("Point");
CtMethod m = CtNewMethod.make(
                 "public int xmove(int dx) { x += dx; }",
                 point);
point.addMethod(m);

向类Point添加一个公共方法xmove()。在本例中,x是Point类中的int字段。

传递给make()的源文本可以包括以$开头的标识符,除了setBody()中的$_。如果目标对象和目标方法名也指定给make(),它还可以包含$proceed。例如,

java
CtClass point = ClassPool.getDefault().get("Point");
CtMethod m = CtNewMethod.make(
                 "public int ymove(int dy) { $proceed(0, dy); }",
                 point, "this", "move");

注意,$proceed已被this.move所取代。

Javassist提供了另一种添加新方法的方法。你可以先创建一个抽象方法,然后给它一个方法体:

java
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可以编译递归调用自身的方法。)要向类中添加相互递归方法,需要使用如下所示的技巧。假设你想给一个由cc表示的类添加方法m()和n():

java
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还允许用户创建新字段。

java
CtClass point = ClassPool.getDefault().get("Point");
CtField f = new CtField(CtClass.intType, "z", point);
point.addField(f);

如果必须指定添加字段的初始值,则必须将上面的程序修改为:

java
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()。CtConstructor可以通过CtClass中的removeConstructor()移除。

Annotations

CtClass, CtMethod, CtField和CtConstructor提供了一个方便的方法getAnnotations()来读取注释。它返回一个注释类型的对象。

java
public @interface Author {
    String name();
    int year();
}

该注释的用法如下:

java
@Author(name="Chiba", year=2005)
public class Point {
    int x, y;
}

然后,可以通过getAnnotations()获得注释的值。它返回一个包含注释类型对象的数组。

java
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);

由于Point的注释只有@Author,因此数组all的长度为1,而all[0]是一个Author对象。注释的成员值可以通过调用Author对象上的name()和year()来获得。

要使用getAnnotations(),注释类型(如Author)必须包含在当前类路径中。它们还必须可以从ClassPool对象访问。如果找不到注释类型的类文件,Javassist就无法获得该注释类型成员的默认值。

Runtime support classes

在大多数情况下,由Javassist修改的类不需要Javassist来运行。但是,Javassist编译器生成的某些字节码需要运行时支持类,这些类在Javassist .runtime包中(有关详细信息,请阅读该包的API参考)。注意,Javassist .runtime包是由Javassist修改的类可能需要运行的唯一包。其他Javassist类永远不会在修改后的类运行时使用。

Import

源代码中的所有类名必须是完全限定的(它们必须包括包名)。然而,java.Lang包是个例外;例如,Javassist编译器可以解析Object以及java.lang.Object。

要告诉编译器在解析类名时搜索其他包,请在ClassPool中调用importPackage()。例如,

java
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()的参数必须始终是一个完全限定名。

Limitations

在当前的实现中,Javassist中包含的Java编译器在编译器可接受的语言方面有几个限制。这些限制是:

  • 不支持J2SE 5.0引入的新语法(包括枚举和泛型)。注释由Javassist的低级API支持。请参阅javassist.bytecode.annotation包(以及CtClass和CtBehavior中的getAnnotations())。泛型也只得到部分支持。有关详细信息,请参见后一节。

  • 数组初始化器是由大括号{和}括起来的逗号分隔的表达式列表,除非数组维度为1,否则不可用。

  • 不支持内部类或匿名类。请注意,这只是编译器的限制。它不能编译包含匿名类声明的源代码。Javassist可以读取和修改内部/匿名类的类文件。

  • 不支持标记的continue和break语句。

  • 编译器没有正确实现Java方法分派算法。如果类中定义的方法具有相同的名称但采用不同的参数列表,编译器可能会混淆。

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中,
java
javassist.CtClass.intType.getName()

在javassist.CtClass中的静态字段intType所指示的对象上调用getName()方法。在Javassist中,用户可以编写如上所示的表达式,但建议这样写:

java
javassist.CtClass#intType.getName()

这样编译器就可以快速解析表达式。

Released under the MIT License.