Javassist: Java Bytecode Engineering Made Simple

 翻译:ithuriel xiao

Summaryjava


Javassist是一个执行字节码操做的强而有力的驱动代码库。它容许开发者自由的在一个已经编译好的类中添加新的方法,或者是修改已有的方法。可是,和其余的相似库不一样的是,Javassist并不要求开发者对字节码方面具备多么深刻的了解,一样的,它也容许开发者忽略被修改的类自己的细节和结构。数据库


字节码驱动一般被用来执行对于已经编译好的类的修改,或者由程序自动建立执行类等等等等相关方面的操做。这就要求字节码引擎具有不管是在运行时或是编译时都能修改程序的能力。当下有些技术即是使用字节码来强化已经存在的Java类的,也有的则是使用它来使用或者产生一些由系统在运行时动态建立的类。举例而言,JDO1.0规范就使用了字节码技术对数据库中的表进行处理和预编译,并进而包装成Java类。特别是在面向对象驱动的系统开发中,至关多的框架体系使用字节码以使咱们更好的得到程序的范型性和动态性。而某些EJB容器,好比JBOSS项目,则经过在运行中动态的建立和加载EJB,从而戏剧性的缩短了部署EJB的周期。这项技术是如此的引人入胜,以致于在JDK中也有了标准的java.lang.reflect.Proxy类来执行相关的操做。框架


可是,尽管如此,编写字节码对于框架程序开发者们而言,倒是一个至关不受欢迎的繁重任务。学习和使用字节码在某种程度上就如同使用汇编语言。这使得于大多数开发者而言,尽管在程序上能够得到至关多的好处,可攀登它所须要的难度则足以冷却这份热情。不只如此,在程序中使用字节码操做也大大的下降了程序的可读性和可维护性。函数


这是一块很好的奶油面包,可是咱们却只能隔着橱窗流口水…难道咱们只能如此了吗?工具


所幸的是,咱们还有Javassist。Javassist是一个能够执行字节码操做的函数库,但是尽管如此,它倒是简单而便与理解的。他容许开发者对本身的程序自由的执行字节码层的操做,固然了,你并不须要对字节码有多深的了解,或者,你根本就不须要了解。学习


API Parallel to the Reflection APIthis


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以得到相关的信息。

 

 

呼呼,赶了两小时的说~~~英语太糟糕了,不过这个东西真不错,特别是结合反射一块儿用,超级灵啊,可能有不少的错误,个人正式工做经验还不到一年,请你们指教咯。