Java字节码Javassist之内省和定制(四)


  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:
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包。
  由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或以分号(;)结尾的表达式。block是一组用大括号{}括起来的语句。因此,下面的每一行都是一个有效语句或块的例子:

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

  语句和块可以引用字段和方法。它们还可以将参数引用到它们所插入的方法(如果该方法是使用-g选项编译的)(以便在类文件中包含局部变量属性)。否则,它们必须通过特殊变量$0,$1,KaTeX parse error: Expected 'EOF', got '&' at position 84: …语句和块访问可用的局部变量。 &̲emsp;&emsp;传递给i…开头的几个标识符具有特殊含义:

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

修改方法体

  CtMethod和CtConstructor提供了setBody()来替换整个方法体。它们将给定的源文本编译为Java字节码,并用它代替原始方法体。如果给定的源文本为空,则替换的正文只包括一个return语句,该语句返回零或空,除非结果类型为空。
  在提供给setBody()的源文本中,以$开头的标识符具有特殊含义:

  • $0, $1, $2, … 实际参数
  • a r g s 一个参数数组。 args 一个参数数组。 args一个参数数组。args类型为Object[]。
  • 所有实际参数例如, m ( 所有实际参数 例如,m( 所有实际参数例如,m()等价于m($1,$2,…)
  • $cflow(…) cflow变量
  • $r 结果类型。它用于强制转换表达式。
  • $w 包装器类型。它用于强制转换表达式。
  • $sig 表示形式形参类型的java.lang.Class对象数组。
  • $type 表示形式结果类型的class对象。
  • c l a s s c l a s s 对象,表示当前编辑的类。 = = 注意 class class对象,表示当前编辑的类。 ==注意 classclass对象,表示当前编辑的类。==注意_是不可用的。==

新增方法或字段

增加方法

  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()的源文本可以包括以 开头的标识符,除了 s e t B o d y ( ) 中的 开头的标识符,除了setBody()中的 开头的标识符,除了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);

这个程序将一个名为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);

移除字段或方法

  要删除一个字段或方法,请调用CtClass中的removeField()或removeMethod()。CtConstructor可以通过CtClass中的removeConstructor()删除。

注解

  CtClass, CtMethod, CtField和CtConstructor提供了一个方便的方法getannotation()来读取注释。它返回一个注释类型的对象。
例如,假设有以下注释:

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就无法获得该注释类型成员的默认值。

运行时支持类

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

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

限制

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

  • 不支持J2SE 5.0引入的新语法(包括枚举和泛型)Javassist的低级API支持注释。请参阅javassist. byteccode .annotation包(以及CtClass和CtBehavior中的getAnnotations())。泛型也只得到部分支持。有关更多细节,请参阅后一节。
  • 数组初始化器是由括号{和}括起来的表达式的逗号分隔列表,除非数组维数为1,否则不可用。
  • 不支持内部类或匿名类。注意,这只是编译器的限制。它不能编译包含匿名类声明的源代码。Javassist可以读取和修改内部/匿名类的类文件。
  • 不支持标记的continue和break语句。
  • 编译器没有正确地实现Java方法分派算法。如果类中定义的方法具有相同的名称但采用不同的形参列表,则编译器可能会混淆。
  • 建议用户使用#作为类名和静态方法或字段名之间的分隔符。例如,在常规Java中
javassist.CtClass.intType.getName()

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

javassist.CtClass#intType.getName()

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


[资料来源]  http://www.javassist.org/tutorial/tutorial.html

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值