本文译自Getting Started with Javassist,如果谬误之处,还请指出。
-
bytecode读写
-
ClassPool
-
Class loader
-
自有和定制
-
Bytecode操控接口
-
Generics
-
Varargs
-
J2ME
-
装箱和拆箱
-
调试
1. bytecode读写
Javassist是用来处理java字节码的类库, java字节码一般存放在后缀名称为class的二进制文件中。每个二进制文件都包含一个java类或者是java接口。
Javasist.CtClass是对类文件的抽象,处于编译中的此对象可以用来处理类文件。下面的代码用来展示一下其简单用法:
1: ClassPool pool = ClassPool.getDefault();
2: CtClass cc = pool.get("test.Rectangle");
3: cc.setSuperclass(pool.get("test.Point"));
4: cc.writeFile();
这段程序首先获取ClassPool的实例,它主要用来修改字节码的,里面存储着基于二进制文件构建的CtClass对象,它能够按需创建出CtClass对象并提供给后续处理流程使用。当需要进行类修改操作的时候,用户需要通过ClassPool实例的.get()方法,获取CtClass对象。从上面代码中我们可以看出,ClassPool的getDefault()方法将会查找系统默认的路径来搜索test.Rectable对象,然后将获取到的CtClass对象赋值给cc变量。
从易于扩展使用的角度来说,ClassPool是由装载了很多CtClass对象的HashTable组成。其中,类名为key,CtClass对象为Value,这样就可以通过搜索HashTable的Key来找到相关的CtClass对象了。如果对象没有被找到,那么get()方法就会创建出一个默认的CtClass对象,然后放入到HashTable中,同时将当前创建的对象返回。
从ClassPool中获取的CtClass对象,是可以被修改的。从上面的 代码中,我们可以看到,原先的父类,由test.Rectangle被改成了test.Point。这种更改可以通过调用CtClass().writeFile()将其持久化到文件中。同时,Javassist还提供了toBytecode()方法来直接获取修改的字节码:
1: byte[] b = cc.toBytecode();
你可以通过如下代码直接加载CtClass:
1: Class clazz = cc.toClass();
toClass()方法被调用,将会使得当前线程中的context class loader加载此CtClass类,然后生成 java.lang.Class对象。更多的细节 ,请参见this section below.
新建类
新建一个类,可以使用ClassPool.makeClass()方法来实现:
1: ClassPool pool = ClassPool.getDefault();
2: CtClass cc = pool.makeClass("Point");
上面的代码展示的是创建无成员方法的Point类,如果需要附带方法的话,我们可以用CtNewMethod附带的工厂方法创建,然后利用CtClass.addMethod()将其追加就可以了 。
makeClass()不能用于创建新的接口。但是makeInterface()可以。接口的方法可以用CtNewmethod.abstractMethod()方法来创建,需要注意的是,在这里,一个接口方法其实是一个abstract方法。
冻结类
如果CtClass对象被writeFile(),toClass()或者toBytecode()转换成了类对象,Javassist将会冻结此CtClass对象。任何对此对象的后续更改都是不允许的。之所以这样做,主要是因为此类已经被JVM加载,由于JVM本身不支持类的重复加载操作,所以不允许更改。
一个冻结的CtClass对象,可以通过如下的代码进行解冻,如果想更改类的话,代码如下:
1: CtClasss cc = ...;
2: :
3: cc.writeFile();
4: cc.defrost();
5: cc.setSuperclass(...); // OK since the class is not frozen.
调用了defrost()方法之后,CtClass对象就可以随意修改了。
如果ClassPool.doPruning被设置为true,那么Javassist将会把已冻结的CtClass对象中的数据结构进行精简,此举主要是为了防止过多的内存消耗。而精简掉的部分,都是一些不必要的属性(attriute_info结构)。因此,当一个CtClass对象被精简之后,方法是无法被访问和调用的,但是方法名称,签名,注解可以被访问。被精简过的CtClass对象可以被再次解冻。需要注意的是,ClassPool.doPruning的默认值为false。
为了防止CtClass类被无端的精简,需要优先调用stopPruning()方法来进行阻止:
1: CtClasss cc = ...;
2: cc.stopPruning(true);
3: :
4: cc.writeFile(); //转换为类文件,cc不会被精简.
这样,CtClass对象就不会被精简了。当writeFile()方法调用之后,我们就可以进行解冻,然后为所欲为了。
需要注意的是:在调试的时候, debugWriteFile()方法可以很方便的防止CtClass对象精简和冻住。
类搜索路径
ClassPool.getDefault()方法的搜索路径和JVM的搜索路径是一致的。如果程序运行在JBoss或者Tomcat服务器上,那么ClassPool对象也许不能够找到用户类,原因是应用服务器用的是多个class loader,其中包括系统的class loader来加载对象。正因如此,ClassPool需要 附加特定的类路径才行。 假设如下的pool实例代表ClassPool对象:
1: pool.insertClassPath(new ClassClassPath(this.getClass()));
上面的代码段注册了this所指向的类路径下面的类对象。你可以用其他的类对象来代替this.getClass()。这样就可以加载其他不同的类对象了。
你也可以注册一个目录名字来作为类搜索路径。比如下面代码中,使用/usr/local/javalib目录作为搜索路径:
1: ClassPool pool = ClassPool.getDefault();
2: pool.insertClassPath("/usr/local/javalib");
也可以使用url来作为搜索路径:
1: ClassPool pool = ClassPool.getDefault();
2: ClassPath cp = new URLClassPath("www.javassist.org", 80, "/java/", "org.javassist.");
3: pool.insertClassPath(cp);
上面这段代码将会添加“http://www.javassist.org:80/java/”到类搜索路径。这个URL主要用来搜索org.javassist包下面的类。比如加载org.javassist.test.Main类,此类将会从如下路径获取:
1: http://www.javassist.org:80/java/org/javassist/test/Main.class
此外,你甚至可以直接使用一串字节码,然后创建出CtClass对象。示例如下:
1: ClassPool cp = ClassPool.getDefault();
2: byte[] b = a byte array;
3: String name = class name;
4: cp.insertClassPath(new ByteArrayClassPath(name, b));
5: CtClass cc = cp.get(name);
从上面代码可以看出,ClassPool加载了ByteArrayClasPath构建的对象,然后利用get()方法并通过类名,将对象赋值给了CtClass对象。
如果你不知道类的全名,你也可以用makeClass()来实现:
1: ClassPool cp = ClassPool.getDefault();
2: InputStream ins = an input stream for reading a class file;
3: CtClass cc = cp.makeClass(ins);
makeClass()方法利用给定的输入流构建出CtClass对象。你可以用饿汉方式直接创建出ClassPool对象,这样当搜索路径中有大点的jar文件需要加载的时候,可以提升一些性能,之所以 这样做,原因是ClassPool对象按需加载类文件,所以它可能会重复搜索整个jar包中的每个类文件,正因为如此,makeClass()可以用于优化查找的性能。被makeClass()方法加载过的CtClass对象将会留存于ClassPool对象中,不会再进行读取。
用户可以扩展类搜索路径。可以通过定义一个新的类,扩展自ClassPath接口,然后返回一个insertClassPath即可。这种做法可以允许其他资源被包含到搜索路径中。
2. ClassPool
一个ClassPool里面包含了诸多的CtClass对象。每当一个CtClass对象被创建的时候,都会在ClassPool中做记录。之所以这样做,是因为编译器后续的源码编译操作可能会通过此类关联的CtClass来获取。
比如,一个代表了Point类的CtClass对象,新加一个getter()方法。之后,程序将会尝试编译包含了getter()方法的Point类,然后将编译好的getter()方法体,添加到另外一个Line类上面。如果CtClass对象代表的Point类不存在的话,那么编译器就不会成功的编译getter()方法。需要注意的是原来的类定义中并不包含getter()方法 。因此,要想正确的编译此方法,ClassPool对象必须包含程序运行时候的所有的CtClass对象。
避免内存溢出
CtClass对象非常多的时候,ClassPool将会消耗内存巨大。为了避免个问题,你可以移除掉一些不需要的CtClass对象。你可以通过调用CtClass.detach()方法来实现,那样的话此CtClass对象将会从ClassPool移除。代码如下:
1: CtClass cc = ... ;
2: cc.writeFile();
3: cc.detach();
此CtClass对象被移除后,不能再调用其任何方法。但是你可以调用ClassPool.get()方法来创建一个新的CtClass实例。
另一个方法就是用新的ClassPool对象来替代旧的ClassPool对象。如果旧的ClassPool对象被垃圾回收了,那么其内部的CtClass对象也都会被垃圾回收掉。下面的代码可以用来创建一个新的ClassPool对象:
1: ClassPool cp = new ClassPool(true);
2: //如果需要的话,利用appendClassPath()来添加额外的搜索路径
上面的代码和ClassPool.getDefault()来创建ClassPool,效果是一样的。需要注意的是,ClasssPool.getDefault()是一个单例工厂方法,它能够创建出一个唯一的ClassPool对象并进行重复利用。new ClassPool(true)是一个很快捷的构造方法,它能够创建一个ClassPool对象然后追加系统搜索路径到其中。和如下的代码创建行为表现一致:
1: ClassPool cp = new ClassPool();
2: cp.appendSystemPath(); // or append another path by appendClassPath()
级联ClassPools
如果应用运行在JBOSS/Tomcat上, 那么创建多个ClassPool对象将会很有必要。因为每个类加载其都将会持有一个ClassPool的实例。应用此时最好不用getDefault()方法来创建ClassPool对象,而是使用构造来创建。
多个ClassPool对象像java.lang.ClassLoader一样做级联,代码如下:
1: ClassPool parent = ClassPool.getDefault();
2: ClassPool child = new ClassPool(parent);
3: child.insertClassPath("./classes");
如果child.get()被调用,子ClassPool将会首先从父ClassPool进行查找。当父ClassPool查找不到后,然后将会尝试从./classes目录进行查找。
如果child.childFirstLookup = true, 子ClassPool将会首先查找自己的目录,然后查找父ClassPool,代码如下:
1: ClassPool parent = ClassPool.getDefault();
2: ClassPool child = new ClassPool(parent);
3: child.appendSystemPath(); //和默认的搜索地址一致.
4: child.childFirstLookup = true; //修改子类搜索行为.
为新类重命名
可以从已有类创建出新的类,代码如下:
1: ClassPool pool = ClassPool.getDefault();
2: CtClass cc = pool.get("Point");
3: cc.setName("Pair");
此代码首先从Point类创建了CtClass对象,然后调用setName()重命名为Pair。之后,所有对CtClass对象的引用,将会由Point变成Pair。
需要注意的是setName()方法改变ClassPool对象中的标记。从可扩展性来看,ClassPool对象是HashTable的合集,setName()方法只是改变了key和Ctclass对象的关联。
因此,对于get("Point")方法之后的所有调用,将不会返回CtClasss对象。ClassPool对象再次读取Point.class的时候,将会创建一个新的CtClass,这是因为和Point关联的CtClass对象已经不存在了,请看如下代码:
1: ClassPool pool = ClassPool.getDefault();
2: CtClass cc = pool.get("Point");
3: CtClass cc1 = pool.get("Point"); //cc1和cc是一致的.
4: cc.setName("Pair");
5: CtClass cc2 = pool.get("Pair"); //cc2和cc是一致的.
6: CtClass cc3 = pool.get("Point"); //cc3和cc是不一致的.
cc1和cc2将会指向cc,但是cc3却不会。需要注意的是,在cc.setName("Pair")执行后,cc和cc1指向的CtClass对象都变成了指向Pair类。
ClassPool对象用来维护类之间和CtClass对象之间一对一的映射关系。Javassist不允许两个不同的CtClass对象指向同一个类,除非两个独立的ClassPool存在的情况下。这是为实现程序转换而保证其一致性的最鲜明的特点。
我们知道,可以利用ClassPool.getDefault()方法创建ClassPool的实例,代码片段如下(之前已经展示过):
1: ClassPool cp = new ClassPool(true);
如果你有两个ClassPool对象,那么你可以从这两个对象中分别取出具有相同类文件,但是隶属于不同的CtClass对象生成的,此时可以通过修改这俩CtClass对象来生成不同的类。
从冻结类中创建新类
当CtClass对象通过writeFile()方法或者toBytecode()转变成类文件的时候,Javassist将不允许对这个CtClass对象有任何修改。因此,当代表Point类的CtClass对象被转换成了类文件,你不能够先拷贝Point类,然后修改名称为Pair类,因为Point类中的setName()方法是无法被执行的,错误使用示例如下:
1: ClassPool pool = ClassPool.getDefault();
2: CtClass cc = pool.get("Point");
3: cc.writeFile();
4: cc.setName("Pair"); // wrong since writeFile() has been called.
为了能够避免这种限制,你应该使用getAndRename()方法,正确示例如下:
1: ClassPool pool = ClassPool.getDefault();
2: CtClass cc = pool.get("Point");
3: cc.writeFile();
4: CtClass cc2 = pool.getAndRename("Point", "Pair");
如果getAndRename()方法被调用,那么ClassPool首先会基于Point.class来创建一个新的CtClass对象。之后,在CtClass对象被放到HashTable前,它将CtClass对象名称从Point修改为Pair。因此,getAndRename()方法可以在writeFile()方法或者toBytecode()方法执行后去修改CtClass对象。
3. 类加载器
如果预先知道需要修改什么类,最简单的修改方式如下:
1. 调用ClassPool.get()方法获取CtClass对象
2. 修改此对象
3. 调用CtClass对象的writeFile()方法或者toBytecode()方法来生成类文件。
如果检测类是否修改行为发生在程序加载的时候,那么对于用户说来,Javassist最好提供这种与之匹配的类加载检测行为。事实上,javassist可以做到在类加载的时候来修改二进制数据。使用Javassist的用户可以定义自己的类加载器,当然也可以采用Javassist自身提供的。
3.1 CtClass中的toClass方法
CtClass提供的toClass()方法,可以很方便的加载当前线程中通过CtClass对象创建的类。但是为了使用此方法,调用方必须拥有足够的权限才行,否则将会报SecurityException错误。
下面的代码段展示了如何使用toClass()方法:
1: public class Hello {
2: public void say() {
3: System.out.println("Hello");
4: }
5: }
6:
7: public class Test {
8: public static void main(String[] args) throws Exception {
9: ClassPool cp = ClassPool.getDefault();
10: CtClass cc = cp.get("Hello");
11: CtMethod m = cc.getDeclaredMethod("say");
12: m.insertBefore("{ System.out.println(\"Hello.say():\"); }");
13: Class c = cc.toClass();
14: Hello h = (Hello)c.newInstance();
15: h.say();
16: }
17: }
Test.main()方法中, say()方法被插入了println()方法,之后这个被修改的Hello类实例被创建,say()方法被调用。
需要注意的是,上面代码中,Hello类是放在toClass()之后被调用的,如果不这么做的话,JVM将会先加载Hello类,而不是在toClass()方法加载Hello类之后再调用Hello类,这样做会导致加载失败(会抛出LinkageError错误)。比如,如果Test.main()方法中的代码如下:
1: public static void main(String[] args) throws Exception {
2: Hello orig = new Hello();
3: ClassPool cp = ClassPool.getDefault();
4: CtClass cc = cp.get("Hello"); CtMethod m = cc.getDeclaredMethod("say");
5: m.insertBefore("{ System.out.println(\"Hello.say():\"); }");
6: Class c = cc.toClass();
7: Hello h = (Hello)c.newInstance();
8: h.say();}
main方法中,第一行的Hello类会被加载,之后调用toClass()将会报错,因为一个类加载器无法在同一时刻加载两个不同的Hello类版本。
如果程序跑在JBoss/Tomcat上,利用toClass()方法可能会有些问题。在这种情况下,你将会遇到ClassCastException错误,为了避免这种错误,你必须为toClass()方法提供非常明确的类加载器。比如,在如下代码中,bean代表你的业务bean对象的时候:
1: CtClass cc = ...;
2: Class c = cc.toClass(bean.getClass().getClassLoader());
则就不会出现上述问题。你应当为toClass()方法提供已经加载过程序的类加载器才行。
toClass()的使用会带来诸多方便,但是如果你需要更多更复杂的功能,你应当实现自己的类加载器。
3.2 java中的类加载
在java中,多个类加载器可以共存,不同的类加载器会创建自己的应用区域。不同的类加载器可以加载具有相同类名称但是内容不尽相同的类文件。这种特性可以让我们在一个JVM上并行运行多个应用。
需要注意的是JVM不支持动态的重新加载一个已加载的类。一旦类加载器加载了一个类,那么这个类或者基于其修改的类,在JVM运行时,都不能再被加载。因此,你不能够修改已经被JVM加载的类。但是,JPDA(Java Platform Debugger Architecture)支持这种做法。具体请见 Section 3.6.
如果一个类被两个不同的类加载器加载,那么JVM会将此类分成两个不同的类,但是这两个类具有相同的类名和定义。我们一般把这两个类当做是不同的类,所以一个类不能够被转换成另一个类,一旦这么做,那么这种强转操作将会抛出错误ClassCastException。
比如,下面的例子会抛错:
1: MyClassLoader myLoader = new MyClassLoader();
2: Class clazz = myLoader.loadClass("Box");
3: Object obj = clazz.newInstance();
4: Box b = (Box)obj; //会抛出ClassCastException错误.
Box类被两个类加载器所加载,试想一下,假设CL类加载器加载的类包含此代码段,由于此代码段指向MyClassLoader,Class,Object,Box,所以CL加载器也会将这些东西加载进来(除非它是其它类加载器的代理)。因此变量b就是CL中的Box类。从另一方面说来,myLoader也加载了Box类,obj对象是Box类的实例,因此,代码的最后一行将一直抛出ClassCastException错误,因为obj和b是Box类的不同实例副本。
多个类加载器会形成树状结构,除了底层引导的类加载器外,每一个类加载器都有能够正常的加载子加载器的父加载器。由于加载类的请求可以被类加载器所代理,所以一个类可能会被你所不希望看到的类加载器所加载。因此,类C可能会被你所不希望看到的类加载器所加载,也可能会被你所希望的加载器所加载。为了区分这种现象,我们称前一种加载器为类C的虚拟引导器,后一种加载器为类C的真实加载器。
此外,如果类加载器CL(此类加载器为类C的虚拟引导器)让其父加载器PL来加载类C,那么相当于CL没有加载任何类C相关的东西。此时,CL就不能称作虚拟引导器。相反,其父类加载器PL将会变成虚拟引导器。所有指向类C定义的类,都会被类C的真实加载器所加载。
为了理解这种行为,让我们看看如下的例子:
1: public class Point { // 被PL加载
2: private int x, y;
3: public int getX() { return x; }
4: :
5: }
6:
7: public class Box { // 初始化器为L但是实际加载器为PL
8: private Point upperLeft, size;
9: public int getBaseX() { return upperLeft.x; }
10: :
11: }
12:
13: public class Window { // 被L加载器所加载
14: private Box box;
15: public int getBaseX() { return box.getBaseX(); }
16: }
假如Window类被L加载器所加载,那么Window的虚拟加载器和实际加载器都是L。由于Window类中引用了Box类,JVM将会加载Box类,这里,假设L将此加载任务代理给了其父加载器PL,那么Box的类加载器将会变成L,但是其实际加载器将会是PL。因此,在此种情况下,Point类的虚拟加载器将不是L,而是PL,因为它和Box的实际加载器是一样的。因此L加载器将永远不会加载Point类。
接下来,让我们看一个少量更改过的例子:
1: public class Point {
2: private int x, y;
3: public int getX() { return x; }
4: :
5: }
6:
7: public class Box { // the initiator is L but the real loader is PL
8: private Point upperLeft, size;
9: public Point getSize() { return size; }
10: :
11: }
12:
13: public class Window { // loaded by a class loader L
14: private Box box;
15: public boolean widthIs(int w) {
16: Point p = box.getSize();
17: return w == p.getX();
18: }
19: }
现在看来,Window类指向了Point,因此类加载器L要想加载Point的话,它必须代理PL。必须杜绝的情况是,两个类加载器加载同一个类的情况。其中一个类加载器必须能够代理另一个才行。
当Point类加载后,L没有代理PL,那么widthIs()将会抛出ClassCastExceptioin。由于Box类的实际加载器是PL,所以指向Box类的Point类将也会被PL所加载。因此,getSize()方法的最终结果将是被PL加载的Point对象的实例。反之,widthIs()方法中的p变量的类型将是被L所加载的Point类。对于这种情况,JVM会将其视为不同的类型,从而因为类型不匹配而抛出错误。
这种情况,虽然不方便,但是却很有必要,来看一下如下代码段:
1: Point p = box.getSize();
没有抛出错误,Window将会破坏Point对象的包装。举个例子吧,被PL加载的Point类中,x字段是私有的。但是,如果L利用如下的定义加载了Point类的话,那么Window类是可以直接访问x字段的:
1: public class Point {
2: public int x, y; // not private
3: public int getX() { return x; }
4: :
5: }
想要了解java中更多的类加载器信息,以下信息也许有帮助:
Sheng Liang and Gilad Bracha, "Dynamic Class Loading in the Java Virtual Machine",
ACM OOPSLA'98, pp.36-44, 1998.
3.3 使用javassist.Loader
Javassist提供了javassist.Loader这个类加载器。它使用javassist.ClassPool对象来读取类文件。
举个例子,使用javassist.Loader来加载Javassist修改过的类:
1: import javassist.*;
2: import test.Rectangle;
3:
4: public class Main {
5: public static void main(String[] args) throws Throwable {
6: ClassPool pool = ClassPool.getDefault();
7: Loader cl = new Loader(pool);
8:
9: CtClass ct = pool.get("test.Rectangle");
10: ct.setSuperclass(pool.get("test.Point"));
11:
12: Class c = cl.loadClass("test.Rectangle");
13: Object rect = c.newInstance();
14: :
15: }
16: }
上面的程序就修改了test.Rectangle类,先是test.Point类被设置成了test.Rectangle类的父类,之后程序会加载这个修改的类并创建test.Rectangle类的实例出来。
如果一个类被加载后,用户想要修改成自己想要的东西进来,那么用户可以通过添加事件监听器到javassist.Loader上。每当类加载器加载了类进来,那么事件监听器将会发出通知。此监听器必须实现如下的接口:
1: public interface Translator {
2: public void start(ClassPool pool)
3: throws NotFoundException, CannotCompileException;
4: public void onLoad(ClassPool pool, String classname)
5: throws NotFoundException, CannotCompileException;
6: }
当利用javassist.Loader.addTranslator()将事件监听器添加到javassist.Loader对象上的时候,上面的start()方法将会被触发。而onLoad()方法的触发先于javassist.Loader加载一个类,因此onLoad()方法可以改变已加载的类的定义。
举个例子,下面的事件监听器将会在类被加载器加载之前,修改其类型为public:
1: public class MyTranslator implements Translator {
2: void start(ClassPool pool)
3: throws NotFoundException, CannotCompileException {}
4: void onLoad(ClassPool pool, String classname)
5: throws NotFoundException, CannotCompileException
6: {
7: CtClass cc = pool.get(classname);
8: cc.setModifiers(Modifier.PUBLIC);
9: }
10: }
需要注意的是,onLoad()方法不需要调用toBytecode方法或者writeFile方法,因为javassistLoader会调用这些方法来获取类文件。
为了能够运行MyApp类中的MyTranslator对象,写了一个主方法如下:
1: import javassist.*;
2:
3: public class Main2 {
4: public static void main(String[] args) throws Throwable {
5: Translator t = new MyTranslator();
6: ClassPool pool = ClassPool.getDefault();
7: Loader cl = new Loader();
8: cl.addTranslator(pool, t);
9: cl.run("MyApp", args);
10: }
11: }
想要运行它,可以按照如下命令来:
1: % java Main2 arg1 arg2...
MyApp类和其他的一些类,会被MyTranslator所翻译。
需要注意的是,类似MyApp这种应用类,是不能够访问Main2,MyTranslator,ClassPool这些类的,因为这些类是被不同加载器所加载的。应用类是被javassist.Loader所加载,而Main2这些是被java的默认类加载器所加载的。
javassist.Loader搜寻需要加载的类的时候,和java.lang.ClassLoader.ClassLoader是截然不同的。后者先使用父类加载器进行加载,如果父类加载器找不到类,则尝试用当前加载器进行加载。而javassist.Load在如下情况下,则尝试直接加载:
ClassPool对象上,无法找到get方法
或者
父类使用delegateLoadingOf()方法进行加载
Javassist可以按照搜索的顺序来加载已修改的类,但是,如果它无法找到已修改的类,那么将会由父类加载器进行加载操作。一旦当一个类被父加载器所加载,那么指向此类的其他类,也将被此父加载器所加载,因为,这些被加载类是不会被修改的。如果你的程序无法加载一个已修改的类,你需要确认所有的类是否是被javassist.Loader所加载。
3.4 打造一个类加载器
用javassist打造一个简单的类加载器,代码如下:
1: import javassist.*;
2:
3: public class SampleLoader extends ClassLoader {
4: /* Call MyApp.main().
5: */
6: public static void main(String[] args) throws Throwable {
7: SampleLoader s = new SampleLoader();
8: Class c = s.loadClass("MyApp");
9: c.getDeclaredMethod("main", new Class[] { String[].class })
10: .invoke(null, new Object[] { args });
11: }
12:
13: private ClassPool pool;
14:
15: public SampleLoader() throws NotFoundException {
16: pool = new ClassPool();
17: pool.insertClassPath("./class"); // MyApp.class must be there.
18: }
19:
20: /* Finds a specified class.
21: * The bytecode for that class can be modified.
22: */
23: protected Class findClass(String name) throws ClassNotFoundException {
24: try {
25: CtClass cc = pool.get(name);
26: // modify the CtClass object here
27: byte[] b = cc.toBytecode();
28: return defineClass(name, b, 0, b.length);
29: } catch (NotFoundException e) {
30: throw new ClassNotFoundException();
31: } catch (IOException e) {
32: throw new ClassNotFoundException();
33: } catch (CannotCompileException e) {
34: throw new ClassNotFoundException();
35: }
36: }
37: }
MyApp类是一个应用程序。为了执行这个应用,我们首先需要将类文件放到./class文件夹下,需要确保当前文件夹不在类搜索目录下,否则将会被SampleLoader的父类加载器,也就是系统默认的类加载器所加载。./class目录名称在insertClassPath方法中必须要有所体现,当然此目录名称是可以随意改变的。接下来我们运行如下命令:
1: % java SampleLoader
此时,类加载器将会加载MyApp类(./class/MyApp.class)并调用MyApp.main方法。
这是使用基于Javassist类加载器最简单的方式。然而,如果你想写一个更加复杂的类加载器,你需要对Java的类加载器机制有足够的了解。比如,上面的代码中,MyApp类的命名空间和SampleLoader类的命名空间是不同的,是因为这两个类是被不同的类加载器锁加载的。因此,MyApp类无法直接访问SampleLoader类。
3.5 修改系统类
系统类,比如java.lang.String,会优先被系统的类加载器所加载。因此,上面展示的SampleLoader或者javassist.Loader在进行类加载的时候,是无法修改系统类的。
如果需要进行修改的话,系统类必须被静态的修改。比如,下面的代码将会给java.lang.String添加一个hiddenValue的字段:
1: ClassPool pool = ClassPool.getDefault();
2: CtClass cc = pool.get("java.lang.String");
3: CtField f = new CtField(CtClass.intType, "hiddenValue", cc);
4: f.setModifiers(Modifier.PUBLIC);
5: cc.addField(f);
6: cc.writeFile(".");
此段代码会产生"./java/lang/String.class"文件。
为了能够让更改的String类在MyApp中运行,可以按照如下的方式来进行:
1: % java -Xbootclasspath/p:. MyApp arg1 arg2...
假设MyApp的代码如下:
1: public class MyApp {
2: public static void main(String[] args) throws Exception {
3: System.out.println(String.class.getField("hiddenValue").getName());
4: }
5: }
此更改的String类成功的被加载,然后打印出了hiddenValue。
需要注意的是:用如上的方式来修改rt.jar中的系统类并进行部署,会违反Java 2 Runtime Environment binary code license.
3.6 运行状态下重新加载类
如果JVM中的JPDA(Java Platform Debugger Architecture)是可用状态,那么一个类是可以被动态加载的。JVM加载类后,此类的之前版本将会被卸载,而新版本将会被加载。所以,从这里看出,在运行时状态,类是可以被动态更改的。然而,新的类必须能够和旧的类兼容,是因为JVM不允许直接更改类的整体框架,他们必须有相同的方法和字段。
Javassist提供了简单易用的方式来重新加载运行时的类。想要获取更多内容,请翻阅javassist.tools.HotSwapper的API文档。
4. 定制化
CtClass提供了很多方法来用进行定制化。Javassist可以和Java的反射API进行联合定制。CtClass提供了getName方法,getSuperclass方法,getMethods方法等等。CtClass同时也提供了方法来修改类定义,允许添加新的字段,构造,方法等。即便对于检测方法体这种事情来说,也是可行的。
方法都是被CtMethod对象所代表,它提供了多个方法用于改变方法的定义,需要注意的是,如果方法继承自父类,那么在父类中的同样方法将也会被CtMethod所代表。CtMethod对象可以正确的代表任何方法声明。
比如,Point类有一个move方法,其子类ColorPoint不会重写move方法, 那么在这里,两个move方法,将会被CtMethod对象正确的识别。如果CtMethod对象的方法定义被修改,那么此修改将会反映到两个方法上。如果你想只修改ColorPoint类中的move方法,你需要首先创建ColorPoint的副本,那么其CtMethod对象将也会被复制,CtMethod对象可以使用CtNewMethod.copy方法来实现。
Javassist不支持移除方法或者字段,但是支持修改名字。所以如果一个方法不再需要的话,可以在CtMethod中对其进行重命名并利用setName方法和setModifiers方法将其设置为私有方法。
Javassist不支持为已有的方法添加额外的参数。但是可以通过为一个新的方法创建额外的参数。比如,如果你想添加一个额外的int参数newZ到Point类的方法中:
1: void move(int newX, int newY) { x = newX; y = newY; }
你应当在Point类中添加如下方法
1: void move(int newX, int newY, int newZ) {
2: // do what you want with newZ.
3: move(newX, newY);
4: }
Javassist同时也提供底层的API来直接修改原生的类文件。比如,CtClass类中的getClassFile方法可以返回一个ClassFile对象来代表一个原生的类文件。而CtMethod中的getMethodInfo方法则返回MethodInfo对象来代表一个类中的method_info结构。底层的API单词大多数来自于JVM,所以用于用起来不会感觉到陌生。更多的内容,可以参看 javassist.bytecode
package.
Javassist修改类文件的时候,一般不需要javassist.runtime包,除非一些特别的以$符号开头的。这些特殊符号会在后面进行讲解。更多的内容,可以参考javassist.runtime包中的API文档。
4.1 方法体前/后穿插代码段
CtMethod和CtConstructor提供了insertBefore,insertAfter,addCatch三个方法,它们用于在已存在的方法中插入代码段。使用者可以插入java代码段是因为Javassist内置了一个简易的java编译器来处理这些源码。此编译器会将java源码编译成字节码,然后插入到方法体中。
同时,在指定行号的位置插入代码段也是允许的(只有当行号在当前类中存在)。CtMethod和CtConstructor中的insertAt方法带有源码输入和行号的定义,它能够将编译后的代码段插入到指定了行号的位置。
insertBefore,insertAfter,addCatch和insertAt方法均接受一个String类型的代表源码块的入参。此代码段可以是简单的控制类语句if和while,也可以是以分号结尾的表达式,都需要用左右大括号{}进行包装。因此,下面的示例源码都是符合要求的代码段:
1: System.out.println("Hello");
2: { System.out.println("Hello"); }
3: if (i < 0) { i = -i; }
代码段可以指向字段和方法,也可以为编译器添加-g选项来让其指向插入的方法中的参数。否则,只能利用$0,$1,$2...这种如下的变量来进行访问。虽然不允许访问方法中的本地变量,但是在方法体重定义一个新的本地变量是允许的。例外的是,编译器开启了-g选项的话,insertAt方法是允许代码段访问本地变量的。
insertBefore,insertAfter,addCatch和insertAt入参中的String对象,也就是用户输入的代码段,会被Javassist中的编译器编译,由于此编译器支持语言扩展,不同的$符号有不同的含义:
$0
, $1
, $2
, ... this 和实参
$args
参数列表. $args的类型是
Object[]
.
$$
所有实参.例如, m($$)
等价于 m($1,$2,
...)
$cflow(
...)
cflow变量
$r
结果类型. 用于表达式转换.
$w
包装类型. 用于表达式转换.
$_
结果值
$sig
java.lang.Class列表,代表正式入参类型
$type
java.lang.Class对象,代表正式入参值
.
$class
java.lang.Class对象,代表传入的代码段
.
$0, $1, $2, ...
传给目标方法的参数$1,$2...将会替换掉原始的参数名称。$1代表第一个参数,$2代表第二个参数,以此类推。这些参数的类型和原始的参数类型是一致的。$0等价于this关键字,如果方法为static,那么$0将不可用。
这些变量的使用方法如下,以Point类为例:
1: class Point {
2: int x, y;
3: void move(int dx, int dy) { x += dx; y += dy; }
4: }
调用move方法,打印dx和dy的值,执行如下的程序
1: ClassPool pool = ClassPool.getDefault();
2: CtClass cc = pool.get("Point");
3: CtMethod m = cc.getDeclaredMethod("move");
4: m.insertBefore("{ System.out.println($1); System.out.println($2); }");
5: cc.writeFile();
需要注意的是,insertBefore方法中的代码段是被大括号{}包围的,此方法只接受一个被大括号包围的代码段入参。
更改之后的Point类如下:
1: class Point {
2: int x, y;
3: void move(int dx, int dy) {
4: { System.out.println(dx); System.out.println(dy); }
5: x += dx; y += dy;
6: }
7: }
$1和$2被dx和dy替换掉。
从这里可以看出,$1,$2,$3...是可以被更新的。如果一个新的值被赋予了这几个变量中的任意一个,那么这个变量对应的参数值也会被更新。下面来说说其他的参数。
$args
$args变量代表所有参数列表。其类型为Object数组类型。如果一个参数类型基础数据类型,比如int,那么将会被转换为java.lang.Integer并放到$args中。因此,$args[0]一般情况下等价于$1,除非第一个参数的类型为基础数据类型。需要注意的是,$args[0]和$0是不等价的,因为$0代表this关键字。
如果object列表被赋值给$args,那么列表中的每个元素将会被分配给对应的参数。如果一个参数的类型为基础数据类型,那么对应的正确的数据类型为包装过的类型。此转换会发生在参数被分配之前。
$$
$$是被逗号分隔的所有参数列表的缩写。比如,如果move方法中的参数数量有三个,那么
move($$)
等价于:
move($1,$2,$3)
如果move()无入参,那么move($$)等价于move().
$$也可以被用于其他的场景,如果你写了如下的表达式:
exMove($$,context)
那么此表达式等价于:
exMove($1,$2,$3,context)
需要注意的是,$$虽说是方法调用的通用符号,但是一般和$proceed联合使用,后面会讲到。
$cflow
代表着“流程控制”。这个只读变量会返回方法的递归调用深度。
假设如下的方法代表CtMethod中的对象cm:
1: int fact(int n) {
2: if (n <= 1)
3: return n;
4: else
5: return n * fact(n - 1);
6: }
为了使用$cflow,首先需要引用$cflow,用于监听fact方法的调用
1: CtMethod cm = ...;
2: cm.useCflow("fact");
useCflow()方法就是用来声明$cflow变量。任何可用的java命名都可以用来进行识别。此名称也可以包含.(点号),比如"my.Test.face"也是可以的。
然后,$cflow(fact)代表着方法cm递归调用的深度。当方法第一次被调用的时候,$cflow(fact)的值为0,再调用一次,此值将会变为1.比如:
1: cm.insertBefore("if ($cflow(fact) == 0)"
2: + " System.out.println(\"fact \" + $1);");
代码段将fact方法进行编译以便于能够看到对应的参数。由于$cflow(fact)被选中,那么对fact方法的递归调用将不会显示参数。
$cflow的值是当前线程中,从cm方法中,最上层栈帧到当前栈帧的值。$cflow同时和cm方法在同一个方法内部的访问权限也是不一样的。
$r
代表着结果类型,必须在转换表达式中用作类型转换。比如,如下用法
1: Object result = ... ;
2: $_ = ($r)result;
如果结果类型为基础数据类型,那么($r)需要遵循如下的规则:
首先,如果操作数类型是基础数据类型,($r)将会被当做普通的转义符。相反的,如果操作数类型是包装类型,那么($r)将会把此包装类型转换为结果类型,比如如果结果类型是int,那么($r)会将java.lang.Integer转换为intl;如果结果类型是void,那么($r)将不会进行类型转换;如果当前操作调用了void方法,那么($r)将会返回null。举个例子,如果foo方法是void方法,那么:
1: $_ = ($r)foo();
是一个有效的申明。
转换符号($r)同时也用于return申明中,即便返回类型是void,如下的return申明也是有效的:
1: return ($r)result;
这里,result是一个本地变量,由于($r)这里做了转换,那么返回结果是无效的。此时的return申明和没有任何返回的return申明是等价的:
1: return;
$w
代表包装类型。必须在转义表达式中用于类型转换。($w)将基础类型转换为对应的包装类型,如下代码示例
1: Integer i = ($w)5;
结果类型依据($w)后面的表达式来确定,如果表达式是double类型,那么包装类型则为java.lang.Double。如果($w)后面的表达式不是基础类型,那么($w)将不进行任何转换。
$_
CtMethod和CtConstructor中的insertAfter方法将编译过的代码插入到方法的尾部。之前给过的一些例子有关insertAfter的例子中,不仅包括$0.$1这种例子的讲解,而且包括$_的这种例子。说道$_变量,它用来代表方法的结果值。其变量类型是方法返回的结果类型。如果返回的结果类型是void,那么$_的类型是Object类型,但是其值为null。
尽管利用insertAfter插入的编译过的代码,是在方法返回之前被执行的,但是这种代码也可以在在方法抛出的exception中执行。为了能够让其在方法抛出的exception中执行,insertAfter方法中的第二个参数asFinally必须为true。
当exception被抛出的时候,利用insertAfter方法插入的代码段将会和作为finally代码块来执行。此时在编译过的代码中,$_的值为0或者null。当此代码段执行完毕后,exception会被重新抛给调用端。需要注意的是,$_是永远不会被抛给调用端的,它会直接被抛弃掉。
$sig
$type的值是java.lang.Class对象,代表着返回值的正确的类型。如果它指向的是构造器,那么其值为Void.class。
$class
$class的值是java.lang.Class对象,代表着当前编辑的方法,此时和$0是等价的。
addCatch()
此方法用于将代码段插入到方法体中进行执行,在执行过程中一旦方法体抛出exception,可以控制给发送给客户端的返回。下面的源码展示了利用特殊的变量$e来指向exception
1: CtMethod m = ...;
2: CtClass etype = ClassPool.getDefault().get("java.io.IOException");
3: m.addCatch("{ System.out.println($e); throw $e; }", etype);
此方法体m被翻译出来后,展示如下:
1: try {
2: the original method body
3: }
4: catch (java.io.IOException e) {
5: System.out.println(e);
6: throw e;
7: }
需要注意的是,插入的代码段必须以throw或者return命令结尾。
4.2 修改方法体
CtMethod和CtContructor提供setBody方法来取代整个方法体。此方法能够将传入的代码段编译为Java字节码,然后用此字节码将其原有的方法体给替换掉。如果给定代码段为空,那么被替换的方法体将只有return 0声明,如果结果类型为void,那么则只有 return null声明。
外部传入给setBody方法的代码段,会包含如下的以$开头的识别码,这些识别码有不同的含义:
$0
, $1
, $2
, ... this
和实参
$args
参数列表.$args类型为
Object数组
.
$$
所有参数.
$cflow(
...)
cflow变量
$r
结果类型. 用于表达式转换.
$w
包装类型. 用于表达式转换.
$sig
java.lang.Class对象数组,代表正式的参数类型
.
$type
java.lang.Class
对象,代表正式的结果类型.
$class
java.lang.Class对象,代表当前操作的方法
(等价于$0的类型).
需要注意的是,此时$_是不可用的。
利用源文本替换现有表达式
Javassist允许修改方法体中的表达式。可以利用javassist.expr.ExprEditor类来进行替换操作。用户可以通过定义ExprEditor的子类来修改表达式。为了运行ExprEditor对象,用户必须调用CtMethod或者CtClass中的instrument方法来进行,示例如下
1: CtMethod cm = ... ;
2: cm.instrument(
3: new ExprEditor() {
4: public void edit(MethodCall m)
5: throws CannotCompileException
6: {
7: if (m.getClassName().equals("Point")
8: && m.getMethodName().equals("move"))
9: m.replace("{ $1 = 0; $_ = $proceed($$); }");
10: }
11: });
上面例子可以看出,通过搜索cm方法体中,通过替换掉Point类中的move方法为如下代码后,
1: { $1 = 0; $_ = $proceed($$); }
move方法中的第一个参数将永远为0,需要注意的替换的代码不仅仅是表达式,也可以是声明或者代码块,但是不能是try-catch声明。
instrument方法可以用来搜索方法体,如果找到了待替换的表达式,比如说方法体,字段,创建的类等,之后它会调用ExprEditor对象中的edit方法来进行修改。传递给edit方法的参数是找寻到的表达式对象,然后edit方法就可以通过此表达式对象来进行替换操作。
通过调用传递给edit方法的表达式对象中的replace方法,可以用来替换成给定的的表达式声明或者代码段。如果给定的代码段是空的,那么也就是说,将会执行replace("{}")方法,那么之前的代码段将会在方法体中被移除。如果你仅仅是想在表达式之前或者之后插入代码段操作,那么你需要将下面的代码段传递给replace方法:
1: { before-statements;
2: $_ = $proceed($$);
3: after-statements; }
此代码段可以是方法调用,字段访问,对象创建等等。
再来看看第二行声明:
1: $_ = $proceed();
上面表达式代表着读访问操作,也可以用如下声明来代表写访问操作:
1: $proceed($$);
目标表达式中的本地变量是可以通过replace方法传递到被instrument方法查找到的代码段中的,如果编译的时候开启了-g选项的话。
javassist.expr.MethodCall
MethodCall对象代表了一个方法调用,它里面的replace方法可以对方法调用进行替换,它通过接收准备传递给insertBefore方法中的以$开头的识别符号来进行替换操作:
$0
The target object of the method call.
This is not equivalent to this
, which represents the caller-side this
object.$0
is null
if the method is static.
$1
, $2
, ...
The parameters of the method call.
$_
The resulting value of the method call.
$r
The result type of the method call.
$class
A java.lang.Class
object representing the class declaring the method.
$sig
An array of java.lang.Class
objects representing the formal parameter types.
$type
A java.lang.Class
object representing the formal result type.
$proceed
The name of the method originally called in the expression.
这里,方法调用是指MethodCall对象。$w,$args和$$在这里都是可用的,除非方法调用的结果类型为void,此时,$_必须被赋值且$_的类型就是返回类型。如果调用的结果类型为Object,那么$_的类型就是Object类型且赋予$_的值可以被忽略。
$proceed不是字符串,而是特殊的语法,它后面必须利用小括号()来包上参数列表。
javassist.expr.ConstructorCall
代表构造器调用,比如this()调用和构造体中的super调用。其中的replace方法可以用来替换代码段。它通过接收insertBefore方法中传入的含有以$开头的代码段来进行替换操作:
$0
The target object of the constructor call. This is equivalent to this
.
$1
, $2
, ...
The parameters of the constructor call.
$class
A java.lang.Class
object representing the class declaring the constructor.
$sig
An array of java.lang.Class
objects representing the formal parameter types.
$proceed
The name of the constructor originally called in the expression.
这里,构造器调用代表着ContructorCall对象,其他的符号,比如$w,$args和$$也是可用的。
由于构造器调用,要么是父类调用,要么是类中的其他构造器调用,所以被替换的方法体必须包含构造器调用操作,一般情况下都是调用$proceed().
$proceed不是字符串,而是特殊的语法,它后面必须利用小括号()来包上参数列表。
javassist.expr.FieldAccess
此对象代表着字段访问。ExprEditor中的edit方法中如果有字段访问被找到,那么就会接收到这个对象。FieldAccess中的replace方法接收待替换的字段。
在代码段中,以$开头的识别码有如下特殊的含义:
$0
The object containing the field accessed by the expression. This is not equivalent to this
.this
represents the object that the method including the expression is invoked on.$0
is null
if the field is static.
$1
The value that would be stored in the field if the expression is write access.
Otherwise, $1
is not available.
$_
The resulting value of the field access if the expression is read access.
Otherwise, the value stored in $_
is discarded.
$r
The type of the field if the expression is read access.
Otherwise, $r
is void
.
$class
A java.lang.Class
object representing the class declaring the field.
$type
A java.lang.Class
object representing the field type.
$proceed
The name of a virtual method executing the original field access. .
其他的识别符,例如$w,$args和$$都是可用的。如果表达式是可访问的,代码段中,$_必须被赋值,且$_的类型就是此字段的类型。
javassist.expr.NewExpr
NewExpr对象代表利用new操作符来进行对象创建。其edit方法接收对象创建行为,其replace方法则可以接收传入的代码段,将现有的对象创建的表达式进行替换。
在代码段中,以$开头的识别码有如下含义:
$0
null
.
$1
, $2
, ...
The parameters to the constructor.
$_
The resulting value of the object creation.
A newly created object must be stored in this variable.
$r
The type of the created object.
$sig
An array of java.lang.Class
objects representing the formal parameter types.
$type
A java.lang.Class
object representing the class of the created object.
$proceed
The name of a virtual method executing the original object creation. .
其他的识别码,比如$w,$args和$$也都是可用的。
javassist.expr.NewArray
此对象表示利用new操作符进行的数组创建操作。其edit方法接收数组创建操作的行为,其replace方法则可以接收传入的代码段,将现有的数组创建的表达式进行替换。
在代码段中,以$开头的识别码有如下含义:
$0
null
.
$1
, $2
, ...
The size of each dimension.
$_
The resulting value of the array creation.
A newly created array must be stored in this variable.
$r
The type of the created array.
$type
A java.lang.Class
object representing the class of the created array.
$proceed
The name of a virtual method executing the original array creation. .
其他的识别码,比如$w,$args和$$也是可用的。
比如,如果数组创建的表达式如下:
String[][] s = new String[3][4];
那么,$1和$2的值将分别为3和4,而$3则是不可用的。
但是,如果数组创建的表达式如下:
String[][] s = new String[3][];
那么,$1的值为3,而$2是不可用的。
javassist.expr.Instanceof
此对象代表instanceof表达式。其edit方法接收instanceof表达式行为,其replace方法则可以接收传入的代码段,将现有的表达式进行替换。
在代码段中,以$开头的识别码有如下含义:
$0
null
.
$1
The value on the left hand side of the original instanceof
operator.
$_
The resulting value of the expression. The type of $_
is boolean
.
$r
The type on the right hand side of the instanceof
operator.
$type
A java.lang.Class
object representing the type on the right hand side of the instanceof
operator.
$proceed
The name of a virtual method executing the original instanceof
expression.
It takes one parameter (the type is java.lang.Object
) and returns true
if the parameter value is an instance of the type on the right hand side of
the original instanceof
operator. Otherwise, it returns false.
其他的识别码,比如$w,$args和$$也是可用的。
javassist.expr.Cast
此对象代表显式类型转换。其edit方法接收显式类型转换的行为,其replace方法则可以接收传入的代码段,将现有的代码段进行替换。
在代码段中,以$开头的识别码有如下的含义:
$0
null
.
$1
The value the type of which is explicitly cast.
$_
The resulting value of the expression. The type of $_
is the same as the type
after the explicit casting, that is, the type surrounded by ( )
.
$r
the type after the explicit casting, or the type surrounded by ( )
.
$type
A java.lang.Class
object representing the same type as $r
.
$proceed
The name of a virtual method executing the original type casting.
It takes one parameter of the type java.lang.Object
and returns it after
the explicit type casting specified by the original expression.
其他的识别码,比如$w,$args和$$也是可用的。
javassist.expr.Handler
此对象代表try-catch申明中的catch子句。其edit方法接收catch表达式行为,其insertBefore方法将接收的代码段进行编译,然后将其插入到catch子句的开始部分。
在代码段中,以$开头的识别码有如下的含义:
$1
The exception object caught by the catch
clause.
$r
the type of the exception caught by the catch
clause. It is used in a cast expression.
$w
The wrapper type. It is used in a cast expression.
$type
A java.lang.Class
object representing
the type of the exception caught by the catch
clause.
如果一个新的exception对象被赋值给$1,那么它将会将此exception传递给原有的catch子句并被捕捉。
4.3 添加新方法或字段
添加一个方法
Javassist一开始就允许用户创建新的方法和构造,CtNewMethod和CtNewConstructor提供了多种静态工厂方法来创建CtMethod或者CtConstructor对象。特别说明一下,其make方法可以从给定的代码段中创建CtMethod或者CtContructor对象。
比如,如下程序:
1: CtClass point = ClassPool.getDefault().get("Point");
2: CtMethod m = CtNewMethod.make(
3: "public int xmove(int dx) { x += dx; }",
4: point);
5: point.addMethod(m);
添加了一个公共方法xmove到Point类中,此例子中,x是Point类中的int字段。
make方法中的代码段可以包含以$开头的识别码,但是setBydy方法中的$_除外。如果目标对象和目标方法的名字也传递给了make方法,那么此方法也可以包含$proceed。比如:
1: CtClass point = ClassPool.getDefault().get("Point");
2: CtMethod m = CtNewMethod.make(
3: "public int ymove(int dy) { $proceed(0, dy); }",
4: point, "this", "move");
上面代码创建如下ymove方法定义:
1: public int ymove(int dy) { this.move(0, dy); }
需要注意的是,$proceed已经被this.move替换掉了。
Javassist也提供另一种方式来添加新方法,你可以首先创建一个abstract方法,然后赋予它方法体:
1: CtClass cc = ... ;
2: CtMethod m = new CtMethod(CtClass.intType, "move",
3: new CtClass[] { CtClass.intType }, cc);
4: cc.addMethod(m);
5: m.setBody("{ x += $1; }");
6: cc.setModifiers(cc.getModifiers() & ~Modifier.ABSTRACT);
如果一个abstract方法被添加到了类中,此时Javassist会将此类也变为abstract,为了解决这个问题,你不得不利用setBody方法将此类变回非abstract状态。
相互递归调用方法
当一个方法调用另一个为添加到操作类中的方法时,Javassist是无法编译此方法的(Javassist可以编译自己调用自己的递归方法)。为了添加相互递归调用的方法到类中,你需要如下的窍门来进行。假设你想添加m和n方法到cc中:
1: CtClass cc = ... ;
2: CtMethod m = CtNewMethod.make("public abstract int m(int i);", cc);
3: CtMethod n = CtNewMethod.make("public abstract int n(int i);", cc);
4: cc.addMethod(m);
5: cc.addMethod(n);
6: m.setBody("{ return ($1 <= 0) ? 1 : (n($1 - 1) * $1); }");
7: n.setBody("{ return m($1); }");
8: cc.setModifiers(cc.getModifiers() & ~Modifier.ABSTRACT);
首先,你需要创建两个abstract方法并把他们添加到类中。
然后,为方法设置方法体,方法体内部可以实现相互调用。
最后,将类变为非abstract的,因为addMethod添加abstract方法的时候,会自动将类变为abstract的。
添加字段
Javassist允许用户创建一个新的字段:
1: CtClass point = ClassPool.getDefault().get("Point");
2: CtField f = new CtField(CtClass.intType, "z", point);
3: point.addField(f);
上面的diam会添加z字段到Point类中。
如果添加的字段需要设定初始值的话,代码需要被改为如下方式来进行:
1: CtClass point = ClassPool.getDefault().get("Point");
2: CtField f = new CtField(CtClass.intType, "z", point);
3: point.addField(f, "0"); // initial value is 0.
现在,addField方法接收了第二个用于计算初始值的参数。此参数可以为任何符合要求的java表达式。需要注意的是,此表达式不能够以分号结束(;)。
此外,上面的代码可以被重写为如下更简单的方式:
1: CtClass point = ClassPool.getDefault().get("Point");
2: CtField f = CtField.make("public int z = 0;", point);
3: point.addField(f);
成员移除
为了移除字段或者方法,可以调用CtClass类中的removeField或者removeMethod来进行。而移除CtConstructor,可以通过调用removeConstructor方法来进行。
4.4 Annotations
CtClass,CtMethod,CtField和CtConstructor提供了getAnnotations这个快捷的方法来进行注解的读取操作,它会返回注解类型对象。
比如,如下注解方式:
1: public @interface Author {
2: String name();
3: int year();
4: }
可以按照如下方式来使用:
1: @Author(name="Chiba", year=2005)
2: public class Point {
3: int x, y;
4: }
此时,这些注解的值就可以用getAnnotations方法来获取,此方法将会返回包含了注解类型的对象列表。
1: CtClass cc = ClassPool.getDefault().get("Point");
2: Object[] all = cc.getAnnotations();
3: Author a = (Author)all[0];
4: String name = a.name();
5: int year = a.year();
6: System.out.println("name: " + name + ", year: " + year);
上面代码打印结果如下:
name: Chiba, year: 2005
由于Point类的注解只有@Author,所以all列表的长度只有一个,且all[0]就是Author对象。名字和年龄这俩注解字段值可以通过调用Author对象中的name方法和year来获取。
为了使用getAnnotations方法,类似Author这种注解类型必须被包含在当前的类路径中,同时必须能够被ClassPool对象所访问,如果类的注解类型无法被找到,Javassist就无法获取此注解类型的默认注解值。
4.5 运行时类支持
在大部分情况下,在Javassist中修改类并不需要Javassist运行时的支持。但是,有些基于Javassist编译器生成的字节码,则需要javassist.runtime这种运行时支持类包的支持(更多细节请访问此包的API)。需要注意的是,javassist.runtime包是Javassist中进行类修改的时候,唯一可能需要调用的包。
4.6导入
所有的源码中的类名,必须是完整的(必须包含完整的包名),但是java.lang包例外,比如,Javassist编译器可以将java.lang包下的Object转换为java.lang.Object。
为了让编译器能够找到类名锁对应的包,可以通过调用ClassPool的importPackage方法来进行,示例如下:
1: ClassPool pool = ClassPool.getDefault();
2: pool.importPackage("java.awt");
3: CtClass cc = pool.makeClass("Test");
4: CtField f = CtField.make("public Point p;", cc);
5: cc.addField(f);
第二行代表引入java.awt包,那么第三行就不会抛出错误,因为编译器可以将Point类识别为java.awt.Point。
需要注意的是,importPckage方法不会影响到ClassPool中的get方法操作,只会影响到编译器的包导入操作。get方法中的参数在任何情况下,必须是完整的,包含包路径的。
4.7限制
在当前扩展中,Javassist中的Java编译器有语言层面的几大限制,具体如下:
不支持J2SE 5.0中的新语法(包括enums和generics)。Javassist底层API才会支持注解,具体内容可以查看javassist.bytecode.annotation包(CtClass和CtBehavior中的getAnnotations方法)。泛型被部分支持,可以查看后面的章节来了解更详细的内容。
数组初始化,也就是被双括号包围的以逗号分隔的表达式,不支持同时初始化多个。
不支持内部类或者匿名类。需要注意的是,这仅仅是因为编译器不支持,所以无法编译匿名表达式。但是Javassist本身是可以读取和修改内部类或者匿名类的。
continue和break关键字不支持。
编译器不能够正确的识别java的方法派发模型,如果使用了这种方式,将会造成编译器解析的混乱。比如:
1: class A {}
2: class B extends A {}
3: class C extends B {}
4:
5: class X {
6: void foo(A a) { .. }
7: void foo(B b) { .. }
8: }
如果编译的表达式是x.foo(new C()),其中x变量指向了X类实例,此时编译器尽管可以正确的编译foo((B)new C()),但是它依旧会将会调用foo(A)。
推荐用户使用#号分隔符来分隔类名和静态方法或者字段名。比如在java中,正常情况下我们会这么调用:
javassist.CtClass.intType.getName()
我们会访问javassist.Ctclass中的静态字段intType,然后调用其getName方法。而在Javassist中,我们可以按照如下的表达式来书写:
javassist.CtClass#intType.getName()
这样编译器就能够快速的解析此表达式了。
5. 字节码API
为了直接修改类文件,Javassist也提供了底层的API,想使用这些API的话,你需要有良好的Java字节码知识储备和类文件格式的认知,这样,你使用这些API修改类文件的时候,才可以随心所欲而不逾矩。
如果你只是想生成一个简单的类文件,那么javassist.bytecode.ClassFileWriter类可以做到。它虽然体积小,但是是比javassist.bytecode.ClassFile更为快速的存在。
5.1 获取ClassFile对象
一个javassist.bytecode.ClassFile对象就代表着一个类文件,为了获取这个对象,CtClass中的getClassFile方法可以做到。如果不想这么做的话,你也可以直接在类文件中构造一个javassist.bytecode.ClassFile,代码如下:
1: BufferedInputStream fin
2: = new BufferedInputStream(new FileInputStream("Point.class"));
3: ClassFile cf = new ClassFile(new DataInputStream(fin));
这个代码片段展示了从Point.class类中创建出一个ClassFile对象出来。
既然可以从类文件中创建出ClassFile,那么也能将ClassFile回写到类文件中。ClassFile中的write方法就可以将类文件内容回写到给定的DataOutputStream中。让我们全程展示一下这种做法:
1: ClassFile cf = new ClassFile(false, "test.Foo", null);
2: cf.setInterfaces(new String[] { "java.lang.Cloneable" });
3:
4: FieldInfo f = new FieldInfo(cf.getConstPool(), "width", "I");
5: f.setAccessFlags(AccessFlag.PUBLIC);
6: cf.addField(f);
7:
8: cf.write(new DataOutputStream(new FileOutputStream("Foo.class")));
上面的代码生成了Foo.class这个类文件,它包含了对如下类的扩展:
1: package test;
2: class Foo implements Cloneable {
3: public int width;
4: }
5.2 添加和删除成员
ClassFile提供了addField方法和addMethod方法来添加字段或者方法(需要注意的是,在字节码层面上说来,构造器也被视为方法),同时也提供了addAttribute方法来为类文件添加属性。
需要注意的是FiledInfo,MethodInfo和AttributeInfo对象包含了对ConstPool(const pool table)对象的指向。此ConstPool对象被添加到ClassFile对象中后,在ClassFile对象和FiledInfo对象(或者是MethodInfo对象等)中必须是共享的。换句话说,FiledInfo对象(或者MethodInfo对象等)在不同的ClassFile中是不能共享的。
为了从ClassFile对象中移除字段或者方法,你必须首先通过类的getFields方法获取所有的字段以及getMethods方法获取所有的方法来生成java.util.List对象,然后将此对象返回。之后就可以通过List对象上的remove方法来移除字段或者方法了,属性的移除方式也不例外,只需要通过FiledInfo或者MethodInfo中的getAttributes方法来获取到属性列表后,然后将相关属性从中移除即可。
5.3 遍历方法体
为了校验方法体中的每个字节码指令,CodeIterator则非常有用。想要获取这个对象的话,需要如下步骤:
1: ClassFile cf = ... ;
2: MethodInfo minfo = cf.getMethod("move"); // we assume move is not overloaded.
3: CodeAttribute ca = minfo.getCodeAttribute();
4: CodeIterator i = ca.iterator();
CodeIterator对象允许你从前到后挨个访问字节码指令。如下的方法是CodeIterator中的一部分:
void begin()
移到第一个指令处.void move(int index)
移到指定索引处boolean hasNext()
如果存在指令的话,返回trueint next()
返回下一个指令的索引
需要注意的是,此方法并不会返回下一个指令的操作码int byteAt(int index)
返回指定索引处的无符号8bit位长值.int u16bitAt(int index)
返回指定索引处的无符号16bit位长值.int write(byte[] code, int index)
在指定索引处写入字节数组.void insert(int index, byte[] code)
在指定索引处写入字节数组,其他字节码的offset等将会自适应更改。
下面的代码段展示了方法体中的所有指令:
1: CodeIterator ci = ... ;
2: while (ci.hasNext()) {
3: int index = ci.next();
4: int op = ci.byteAt(index);
5: System.out.println(Mnemonic.OPCODE[op]);
6: }
5.4 字节码序列的生成
Bytecode对象代表了字节码序列,它是一组在持续不断进行增长的字节码的简称,来看看下面简单的代码片段:
1: ConstPool cp = ...; // constant pool table
2: Bytecode b = new Bytecode(cp, 1, 0);
3: b.addIconst(3);
4: b.addReturn(CtClass.intType);
5: CodeAttribute ca = b.toCodeAttribute();
代码将会产生如下的序列:
iconst_3 ireturn
你也可以利用Bytecode中的get方法来获取一个字节码数组序列,之后可以将此数组插入到另一个代码段中。
虽然Bytecode提供了一系列的方法添加特殊的指令到序列中,它同时也提供了addOpcode方法来添加8bit操作码,提供了addIndex方法来添加索引。8bit操作码的值是在Opcode接口中被定义的。
addOpcode方法和其他添加特殊指令的方法可以自动的维持堆栈的深度,除非操作流程出现了分歧,在这里,我们可以使用Bytecode的getMaxStack方法来获取堆栈最大深度。同时,堆栈深度和Bytecode对象内创建的CodeAtrribute对象也有关系,为了重新计算方法体中的最大堆栈深度,可以使用CodeAttribute中的computeMaxStack来进行。
Bytecode可以用来构建一个方法,示例如下:
1: ClassFile cf = ...
2: Bytecode code = new Bytecode(cf.getConstPool());
3: code.addAload(0);
4: code.addInvokespecial("java/lang/Object", MethodInfo.nameInit, "()V");
5: code.addReturn(null);
6: code.setMaxLocals(1);
7:
8: MethodInfo minfo = new MethodInfo(cf.getConstPool(), MethodInfo.nameInit, "()V");
9: minfo.setCodeAttribute(code.toCodeAttribute());
10: cf.addMethod(minfo);
上面的代码流程是创建了默认的构造函数后,然后将其添加到cf指向的类中。具体说来就是,Bytecode对象首先被转换成了CodeAttribute对象,接着被添加到minfo所指向的方法中。此方法最终被添加到cf类文件中。
5.5 注解 (Meta tags)
注解在运行时态,作为一个可见或者不可见的属性被保存在类文件中。它们可以从ClassFile,MethodInfo或者FieldInfo对象中通过getAttribute(AnnotationsAttribute.invisibleTag)方法来获取。更多的谢洁,可以看看javadoc中关于javassist.bytecode.AnnotationsAttribute类和javassist.bytecode.annotation包的描述。
Javassist也能够让你利用一些应用层的API来访问注解。只需要利用CtClass或者CtBehavior中的的getAnnotations方法接口。
6.泛型
Javassist底层的API可以完全支持Java5中的泛型。另一方面,其更高级别的API,诸如CtClass是无法直接支持泛型的。对于字节码转换来说,这也不是什么大问题。
Java的泛型,采用的是擦除技术。当编译完毕后,所有的类型参数都将会被擦掉。比如,假设你的源码定义了一个参数类型Vector<String>:
1: Vector<String> v = new Vector<String>();
2: :
3: String s = v.get(0);
编译后的字节码等价于如下代码:
1: Vector v = new Vector();
2: :
3: String s = (String)v.get(0);
所以,当你写了一套字节码转换器后,你可以移除掉所有的类型参数。由于嵌入在Javassist的编译器不支持泛型,所以利用其编译的时候,你不得不在调用端做显式的类型转换。比如,CtMethod.make方法。但是如果源码是利用常规的Java编译器,比如javac,来编译的话,是无需进行类型转换的。
如果你有一个类,示例如下:
1: public class Wrapper<T> {
2: T value;
3: public Wrapper(T t) { value = t; }
4: }
想添加Getter<T>接口到Wrapper<T>类中:
1: public interface Getter<T> {
2: T get();
3: }
那么实际上,你需要添加的接口是Getter(类型参数<T>已经被抹除),需要添加到Wrapper中的方法如下:
1: public Object get() { return value; }
需要注意的是,非类型参数是必须的。由于get方法返回了Object类型,那么调用端如果用Javassist编译的话,就需要进行显式类型转换。比如,如下例子,类型参数T是String类型,那么(String)就必须被按照如下方式插入:
1: Wrapper w = ...
2: String s = (String)w.get();
当使用常规的Java编译器编译的时候,类型转换是不需要的,因为编译器会自动进行类型转换。
如果你想在运行时态,通过反射来访问类型参数,那么你不得不在类文件中添加泛型符号。更多详细信息,请参阅API文档CtClass中的setGenericSignature方法。
7.可变参数
目前,Javassist无法直接支持可变参数。为了让方法可以支持它,你需要显式设置方法修改器,其实很简单,假设你想生成如下的方法:
1: public int length(int... args) { return args.length; }
下面的Javassist代码将会生成如上的方法:
1: CtClass cc = /* target class */;
2: CtMethod m = CtMethod.make("public int length(int[] args) { return args.length; }", cc);
3: m.setModifiers(m.getModifiers() | Modifier.VARARGS);
4: cc.addMethod(m);
参数类型int...变成了int[]数组,Modifier.VARARGS被添加到了方法修改器中。
为了能够在Javassist编译器中调用此方法,你需要这样来:
1: length(new int[] { 1, 2, 3 });
而不是这样来:
1: length(1, 2, 3);
8. J2ME
如果你想在J2ME执行环境中修改类文件,你需要进行预校验操作,此操作会产生栈Map对象,此对象和JDK1.6中的J2SE栈map表有些相似。当且仅当javassist.bytecode.MethodInfo.doPreverify为true的时候,Javassist会维护J2ME中的栈map。
你也可以为修改的方法手动生成一个栈map,比如,一个给定的CtMethod对象中的m,你可以调用如下方法来生成一个栈map:
1: m.getMethodInfo().rebuildStackMapForME(cpool);
这里,cpool是ClassPool对象,此对象可以利用CtClass对象中的getClassPool来获取,它负责利用给定的类路径来找寻类文件。为了获取所有的CtMethods对象,可以通过调用CtClass对象的getDeclaredMethods来进行。
9.装箱/拆箱
在Java中,装箱和拆箱操作是语法糖。对于字节码说来,是不存在装箱和拆箱的。所以Javassist的编译器不支持装箱拆箱操作。比如,如下的描述,在java中是可行的:
1: Integer i = 3;
可以看出,此装箱操作是隐式的。但是在Javassist中,你必须显式的将值类型从int转为Integer:
1: Integer i = new Integer(3);
10.调试
将CtClass.debugDump设置为目录名称之后,所有被Javassist生成或修改的类文件将会被保存到此目录中。如果不想这么做,可以将CtClass.debugDump设置为null,需要注意的是,它的默认值就是null。
示例代码:
1: CtClass.debugDump = "./dump";
此时,所有的被修改的类文件将会被保存到./dump目录中。