avassist是一个执行字节码操作的强而有力的驱动代码库。它允许开发者自由的在一个已经编译好的类中添加新的方法,或者是修改已有的方法。但是,和其他的类似库不同的是,Javassist并不要求开发者对字节码方面具有多么深入的了解,同样的,它也允许开发者忽略被修改的类本身的细节和结构。
字节码驱动通常被用来执行对于已经编译好的类的修改,或者由程序自动创建执行类等等等等相关方面的操作。这就要求字节码引擎具备无论是在运行时或是编译时都能修改程序的能力。当下有些技术便是使用字节码来强化已经存在的Java类的,也有的则是使用它来使用或者产生一些由系统在运行时动态创建的类。举例而言,JDO1.0规范就使用了字节码技术对数据库中的表进行处理和预编译,并进而包装成Java类。特别是在面向对象驱动的系统开发中,相当多的框架体系使用字节码以使我们更好的获得程序的范型性和动态性。而某些EJB容器,比如JBOSS项目,则通过在运行中动态的创建和加载EJB,从而戏剧性的缩短了部署EJB的周期。这项技术是如此的引人入胜,以至于在JDK中也有了标准的java.lang.reflect.Proxy类来执行相关的操作。
但是,尽管如此,编写字节码对于框架程序开发者们而言,却是一个相当不受欢迎的繁重任务。学习和使用字节码在某种程度上就如同使用汇编语言。这使得于大多数开发者而言,尽管在程序上可以获得相当多的好处,可攀登它所需要的难度则足以冷却这份热情。不仅如此,在程序中使用字节码操作也大大的降低了程序的可读性和可维护性。
这是一块很好的奶油面包,但是我们却只能隔着橱窗流口水…难道我们只能如此了吗?
所幸的是,我们还有Javassist。Javassist是一个可以执行字节码操作的函数库,可是尽管如此,它却是简单而便与理解的。他允许开发者对自己的程序自由的执行字节码层的操作,当然了,你并不需要对字节码有多深的了解,或者,你根本就不需要了解。
API Parallel to the Reflection API
Javassist的最外层的API和JAVA的反射包中的API颇为类似。它使你可以在装入ClassLoder之前,方便的查看类的结构。它主要由CtClass,,CtMethod,,以及CtField几个类组成。用以执行和JDK反射API中java.lang.Class,,java.lang.reflect.Method,, java.lang.reflect.Method .Field相同的操作。这些类可以使你在目标类被加载前,轻松的获得它的结构,函数,以及属性。此外,不仅仅是在功能上,甚至在结构上,这些类的执行函数也和反射的API大体相同。比如getName,getSuperclass,getMethods,,getSignature,等等。如果你对JAVA的反射机制有所了解的话,使用Javassist的这一层将会是轻松而快乐的。
接下来我们将给出一个使用Javassist来读取org.geometry.Point.class的相关信息的例子(当然了,千万不要忘记引入javassist.*包):
1. ClassPool pool = ClassPool.getDefault();
2. CtClass pt = pool.get("org.geometry.Point");
3. System.out.println(pt.getSuperclass().getName());
其中,ClassPool是CtClass 的创建工厂。它在class path中查找CtClass的位置,并为每一个分析请求创建一个CtClass实例。而“getSuperclass().getName()”则展示出org.geometry.Point.class所继承的父类的名字。
但是,和反射的API不尽相同的是,Javassist并不提供构造的能力,换句话说,我们并不能就此得到一个org.geometry.Point.class类的实例。另一方面,在该类没有实例化前,Javassist也不提供对目标类的函数的调用接口和获取属性的值的方法。在分析阶段,它仅仅提供对目标类的类定义修改,而这点,却是反射API所无法做到的。
举例如下:
4. pt.setSuperclass(pool.get("Figure"));
这样做将修改目标类和其父类之间的关系。我们将使org.geometry.Point.clas改继承自Figure类。当然了,就一致性而言,必须确保Figure类和原始的父类之间的兼容性。
而往目标类中新增一个新的方法则更加的简单了。首先我们来看字节码是如何形成的:
5. CtMethod m = CtNewMethod.make("public int xmove(int dx) { x += dx; }", pt);
6. pt.addMethod(m);
CtMethod类的让我们要新增一个方法只需要写一段小小的函数。这可是一个天大的好消息,开发者们再也不用为了实现这么一个小小的操作而写一大段的虚拟机指令序列了。Javassist将使用一个它自带的编译器来帮我们完成这一切。
最后,千万别忘了指示Javassist把已经写好的字节码存入到你的目标类里:
7. pt.writeFile();
writeFile方法可以帮我们把修改好了的定义写到目标类的.class文件里。当然了,我们甚至可以在该目标类加载的时候完成这一切,Javassist可以很好的和ClassLoader协同工作,我们不久就将看到这一点。
Javassist并不是第一套用以完成从代码到字节码的翻译的函数库。Jakarta的BCEL也是一个比较知名的字节码引擎工具。但是,你却无法使用BCEL来完成代码级别的字符码操作。如果你需要在一个已经编译好的类中添加一个新的方法,假如你用的是BCEL的话,你只能定义一段由那么一大串字符码所构成的指令序列。正如上文所说,这并不是我们所希望看到的。因此,就此方面而言,Javassis使用代码的形式来插入新的方法实在是一大福音。
Instrumenting a Method Body
和方法的新增一样,对于一个类的方法的其他操作也是定义在代码层上的。换而言之,尽管这些步骤是必须的,开发者们也同样无须直接对虚拟机的指令序列进行操作和修改,Javassis将自动的完成这些操作。当然了,如果开发者认为自己有必要对这些步骤进行管理和监控,或者希望由自己来管理这些操作的话,Javassist同样提供了更加底层的API来实现,不过我们在这篇文章中将不会就此话题再做深入探讨。恩,尽管从结构而言,它和BCEL的字节码层API差不多。
设计Javassist对目标类的子函数体的操作API的设想立足与ASPect-Oriented Programming(AOP)思想。Javassist允许把具有耦合关系的语句作为一个整体,它允许在一个插入语句中调用或获取其他函数或者及属性值。它将自动的对这些语句进行优先级分解并执行嵌套操作。
如下例所示,清单1首先包含了一个CtMethod,它主要针对Screen类的draw方法。然后,我们定义一个Point类,该类有一个move操作,用来实现该Point的移动。当然了,在移动前,我们希望可以通过draw方法得到该point目前的位置,那么,我们需要对该move方法加增如下的定义:
{ System.out.println("move"); $_ = $proceed($$); }
这样,在执行move之前,我们就可以打印出它的位置了。请注意这里的调用语句,它是如下格式的:
$_ = $proceed($$);
这样我们就将使用原CtMethod类中的process()对该point的位置进行追踪了。
基与如上情况,CtMethod的关于methord的操作其实被划分成了如下步骤,首先,CtMethod的methord将扫描插入语句(代码)本身。一旦发现了子函数,则创建一个ExprEditor实例来分析并执行这个子函数的操作。这个操作将在整个插入语句执行之前完成。而假如这个实例存在某个static的属性,那么methord将率先检测对插入语句进行检测。然后,在执行插入到目标类---如上例的point类---之前,该static属性将自动的替换插入语句(代码)中所有的相关的部分。不过,值得注意的是,以上的替换操作,将在Javassist把插入语句(代码)转变为字节码之后完成。
Special Variables
在替换的语句(代码)中,我们也有可能需要用到一些特殊变量来完成对某个子函数的调用,而这个时候我们就需要使用关键字“$”了。在Javassist中,“$”用来申明此后的某个词为特殊参数,而“$_”则用来申明此后的某个词为函数的回传值。每一个特殊参数在被调用时应该是这个样子的“$1,$2,$3…”但是,特别的,目标类本身在被调用时,则被表示为“$0”。这种使用格式让开发者在填写使用子函数的参数时轻松了许多。比如如下的例子:
{ System.out.println("move"); $_ = $proceed($1, 0); }
请注意,该子函数的第2个参数为0。
另外一个特殊类型则是$arg,它实际上是一个容纳了函数所有调用参数的Object队列。当Javassist在扫描该$arg时,如果发现某一个参数为JAVA的基本类型,则它将自动的对该参数进行包装,并放入队列。比如,当它发现某一个参数为int类型时,它将使用java.lang.integer 类来包装这个int参数,并存入参数队列。和Java的反射包:java.lang.reflect.Methord类中的invoke方法相比,$args明显要省事的多。
Javassist也同样允许开发者在某个函数的头,或者某个函数的尾上插入某段语句(代码)。比如,它有一个insertBefore方法用以在某函数的调用前执行某个操作,它的使用大致是这个样子的:
1. ClassPool pool = ClassPool.getDefault();
2. CtClass cc = pool.get("Screen");
3. CtMethod cm = cc.getDeclaredMethod("draw", new CtClass[0]);
4. cm.insertBefore("{ System.out.println($1); System.out.println($2); }");
5. cc.writeFile();
以上例子允许我们在draw函数调用之前执行打印操作---把传递给draw的两个参数打印出来。
同样的,我们也可以使用关键字$对某一个函数进行修改或者是包装,下面就
1. CtClass cc = sloader.get("Point");
2. CtMethod m1 = cc.getDeclaredMethod("move");
3. CtMethod m2 = CtNewMethod.copy(m1, cc, null);
4. m1.setName(m1.getName() + "_orig");
5. m2.setBody("{ System.out.println("call"); return $proceed($$); }", "this", m1.getName());
6. cc.addMethod(m2);
7. cc.writeFile();
以上代码的前四行不难理解,Javassist首先对Point中的move方法做了个拷贝,并创建了一个新的函数。然后,它把存在与Point类中的原move方法更名为“_orig”。接下来,让我们关注一下程序第五行中的几个参数:第一个参数指示该函数的在执行的最初部分需要先打印一段信息,然后执行子函数proceed()并返回结果,这个和move方法差不多,很好理解。第二个参数则只是申明该子函数所在的类的位置。这里为this即为Point类本身。第三个参数,也就是“m1.getName()”则定义了这个新函数的名字。
Javassist也同样具有其他的操作和类来帮助你实现诸如修改某一个属性的值,改变函数的回值,并在某个函数的执行后补上其他操作的功能。您可以浏览www.javassist.org以获得相关的信息。
示例:
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
public class Test {
public static void main(String[] args) throws Exception {
ClassPool pool = ClassPool.getDefault();
//设置目标类的路径,确保能够找到需要修改的类,这里我指向firestorm.jar
//解包后的路径
pool.insertClassPath("d:/work/firestorm/firestorm") ;
//获得要修改的类
CtClass cc = pool.get("com.codefutures.if.if");
//设置方法需要的参数
CtClass[] param = new CtClass[3] ;
param[0] = pool.get("java.security.PublicKey") ;
param[1] = pool.get("byte[]") ;
param[2] = pool.get("byte[]") ;
//得到方法
CtMethod m = cc.getDeclaredMethod("a", param);
//插入新的代码
m.insertBefore("{return true ;}") ;
//保存到文件里
cc.writeFile() ;
}
}