BCEL 介绍:
Byte Code Engineering Library (BCEL),这是Apache Software Foundation 的Jakarta 项目的一部分。
BCEL
是 Java classworking 最广泛使用的一种框架,它可以让您深入 JVM 汇编语言进行类操作的细节。BCEL与Javassist
有不同的处理字节码方法,BCEL在实际的JVM 指令层次上进行操作(BCEL拥有丰富的JVM 指令级支持)而Javassist
所强调的源代码级别的工作。
http://sourceforge.net/projects/bcel
Javassist:使字节码工程变得简单(Zz) :
javassist
是一个执行字节码操作的强而有力的驱动代码库。它允许开发者自由的在一个已经编译好的类中添加新的方法,或者是修改已有的方法。但是,和其他的类似库不同
的是,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以获得相关的信息
使用Javassist对.class文件进行修改(Zz) :
最
近重新再看<Inside
JVM>,对JAVA编译成的字节码结构很感兴趣,希望找个工具能够对.class文件进行的解析和查看。没找到,倒发现javaassist可以
对字节码进行操作和修改。此工具是JBOSS项目的一部分,JBOSS实现AOP的基础。呵呵,开眼界了,原来我们可以直接对字节码文件进行修改,哪怕不
知道源文件(跟反编译完全不同)。一个简单例子:
import javassist.*;
class Hello {
public void say() {
System.out.println("Hello");
}
}
public class Test {
public static void main(String[] args) throws Exception {
ClassPool cp = ClassPool.getDefault();
CtClass cc = cp.get("Hello");
CtMethod m = cc.getDeclaredMethod("say");
m.setBody("{System.out.println(""shit"");}");
m.insertBefore("System.out.println(""fuck"");");
Class c = cc.toClass();
Hello h = (Hello)c.newInstance();
h.say();
}
}
编译运行此文件,输出:
fuck
shit
我们在
CtMethod m = cc.getDeclaredMethod("say");
m.setBody("{System.out.println(""shit"");}");
m.insertBefore("System.out.println(""fuck"");");
修改了say()方法,改成了
System.out.println("fuck");
System.out.println("shit");
这里的ClassPool是CtClass的容器,它读取class文件,并根据要求保存CtClass的结构以便日后使用,默认状态下是从当前的类装载器获得,当然你可以指定:
pool.insertClassPath("/usr/local/javalib");
当然,不仅仅是修改方法,你还可以新建一个class,利用makeClass()方法,如:
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.makeClass("Point");
还可以新增方法,下面是sample里的一个例子,同样的:
package sample;
import javassist.*;
import java.lang.reflect.*;
/*
A very simple sample program
This program overwrites sample/Test.class (the class file of this
class itself) for adding a method g(). If the method g() is not
defined in class Test, then this program adds a copy of
f() to the class Test with name g(). Otherwise, this program does
not modify sample/Test.class at all.
To see the modified class definition, execute:
% javap sample.Test
after running this program.
*/
public class Test {
public int f(int i) {
i++;
return i;
}
public static void main(String[] args) throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get("sample.Test");
Test test=new Test();
Class c=test.getClass();
Method []method=c.getDeclaredMethods();
for(int i=0;i<method.length;i++){
System.out.println(method[i]);
}
try {
cc.getDeclaredMethod("g");
System.out.println("g() is already defined in sample.Test.");
}
catch (NotFoundException e) {
/* getDeclaredMethod() throws an exception if g()
* is not defined in sample.Test.
*/
CtMethod fMethod = cc.getDeclaredMethod("f");
CtMethod gMethod = CtNewMethod.copy(fMethod, "g", cc, null);
cc.addMethod(gMethod);
cc.writeFile(); // update the class file
System.out.println("g() was added.");
}
}
}
第一次运行时,因为Test里并没有g()方法,所以执行
CtMethod fMethod = cc.getDeclaredMethod("f");
CtMethod gMethod = CtNewMethod.copy(fMethod, "g", cc, null); //把f方法复制给g
cc.addMethod(gMethod);
cc.writeFile(); //更新class文件
System.out.println("g() was added.");
打印:g() was added
第2次运行时,因为以上步骤已经在class文件中增加了一个g方法,所以
System.out.println("g() is already defined in sample.Test.");
打印:g() is already defined in sample.Test
Javassist不仅能修改你自己的class文件,而且可以同样修改JDK自带的类库(废话,类库也是人写的^_^)具体请看它的tutorial。
Javassist 官方网站: http://www.csg.is.titech.ac.jp/~chiba/javassist/