Javassist之内省与定制(五)

在前几个篇章介绍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()
    

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

你可能感兴趣的:(Javassist之内省与定制(五))