在前几个篇章介绍Javassist如何修改方法体,本篇介绍javassist内省与定制的剩余部分。
1. 添加新的方法或属性
添加方法
Javassist允许用户创建从零开始创建一个新的方法和构造函数。CtNewMethod和CtNewConstructor提供了多种工厂方法,它们都是用来创建CtMethod或者CtConstrutor对象的静态方法。特别的,make()方法可以根据所给的源代码文本创建一个CtMethod或者CtConstructor对象。
例如下面这个程序:
CtClass point = ClassPool.getDefault().get("Point");
CtMethod m = CtNewMethod.make(
"public int xmove(int dx) { x += dx; }",
point);
point.addMethod(m);
添加一个pulbic方法xmove()到Point类中。在这个例子中,x是Poin类中一个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()方法,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()接收第二个参数,它是用来计算初始值的源代码文本。这段源代码文本可以是任何一个表达式,只要返回类型和属性的类型匹配。注意,一个表达式不需要以(;)结尾。
此外,上面的代码可以重写成下面这样简化的版本:
CtClass point = ClassPool.getDefault().get("Point");
CtField f = CtField.make("public int z = 0;", point);
point.addField(f);
移除成员
移除属性或方法,可以调用CtClass的removeField()或者removeMethod()。CtConstructor可以调用CtClass的removeConstructor()移除。
2. 注解
CtClass,CtMethod,CtField和CtConstructor提供了方便的方法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无法获取注解类型的默认值。
3. 运行时支持类
在大多数场景中,Javassist修改的类不需要Javassist去运行。然而一些由Javassist编译器生成的字节码需要运行时支持类,它被封装在javassist.runtime包中(更详细的说明,请阅读该包的相关API手册)。注意,javassist.runtime包仅仅是Javassist修改的类运行所需要的包。其它的Javassist类不需要使用。
4. 导入
所有的源代码中的类名必须是全类名(必须包含包名)。然而,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()的参数必须总是全类名。
5. 限制
在当前实现中,Javassist包含的编译器在编译器可接收的语言方便有着很多限制。这些限制如下:
-
J2SE 5.0介绍的新语法(包括枚举和范型)还不被支持。注解只被Javassist的低级API支持。可以查看javassist.bytecode.annotation包。范型只支持部分。
-
数组初始化,由{}包含的表达式中的逗号风格符不支持,除非数组是一维的。
-
内部类或者匿名类不被支持。注意,这只是编译器的限制。它无法编译包含匿名类的声明的源代码。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中,用户可以像如下并推荐使用如下写法:
javassist.CtClass#intType.getName()
这样编译器可以快速解析表达式。