[toc]
ClassPool对象是多个CtClass对象的容器。一旦CtClass对象被创建,它就会永远被记录再ClassPool对象中。这是因为编译器之后在编译源码的时候可能需要访问CtClass对象。
例如,假定有一个新方法getter() 被增添到了表示Point类的CtClass对象。稍后,程序会试图编译代码,它包含了对Point方法的getter() 调用,并会使用编译后代码作为一个方法的方法体,它将会被增添到另一个类Line中。如果表示Point类的CtClass对象丢了的话,编译器就不能编译调用getter() 的方法了(注意:原始类定义中不包含getter() )。因此,为了正确编译这样一个方法调用,ClassPool在程序过程中必须示种包含所有的CtClass对象。
ClassPool classPool = ClassPool.getDefault();
CtClass point = classPool.makeClass("Point");
point.addMethod(getterMethod); // Point增添了getter方法
CtClass line = ...; // Line方法
// line 调用point的getter方法
某种特定的ClassPool可能造成巨大的内存消耗,导致OOM,比如CtClass对象变得非常的(这个发生的很少,因为Javassist已经尝试用不同的方法减少内存消耗了,比如冻结类)。为了避免该问题,你可以从ClassPool中移除不需要的CtClass对象。只需要调用CtClass的detach() 方法就行了:
CtClass cc = ... ;
cc.writeFile();
cc.detach(); // 该CtClass已经不需要了,从ClassPool中移除
在调用detach() 之后,这个CtClass对象就不能再调用任何方法了。但是你可以依然可以调用classPool.get() 方法来创建一个相同的类。如果你调用get() ,ClassPool会再次读取class文件,然后创建一个新的CtClass对象并返回。
另一种方式是new一个新的ClassPool,旧的就不要了。这样旧的ClassPool就会被垃圾回收,它的CtClass也会被跟着垃圾回收。可以使用以下代码完成:
ClassPool cp = new ClassPool(true); // true代表使用默认路径
// 如果需要的话,可以用appendClassPath()添加一个额外的搜索路径。
上面这个new ClassPool和ClassPool.getDefault() 的效果是一样。注意,ClassPool.getDefault() 是一个单例的工厂方法,它只是为了方便用户创建提供的方法。这两种创建方式是一样的,源码也基本是一样的,只不过**ClassPool.getDefault()**是单例的。
注意,new ClassPool(true) 是一个很方便的构造函数,它构造了一个ClassPool对象,然后给他增添了系统搜索路径。它构造方法的调用就等同于下面的这段代码:
ClassPool cp = new ClassPool();
cp.appendSystemPath(); // 你也可以通过appendClassPath()增添其他路径
如果一个程序运行在Web应用服务器上,你可能需要创建多个ClassPool实例。为每一个类加载器(ClassLoader)创建一个ClassPool(也就是容器)。这时程序在创建ClassPool对象的时候就不能再用getDefault() 了,而是要用ClassPool的构造函数。
多个ClassPool对象可以像java.lang.ClassLoader那样进行级联。例如:
ClassPool parent = ClassPool.getDefault();
ClassPool child = new ClassPool(parent);
child.insertClassPath("./classes");
如果调用了child.get() ,child的ClassPool首先会代理parent的ClassPool,如果parent的ClassPool中没有找到要找的类,才会试图到child中的**./classes**目录下找。
如果child.childFirstLookup设置为了true,child的ClassPool就会首先到自己路径下面找,之后才会到parent的路径下面找。
ClassPool parent = ClassPool.getDefault();
ClassPool child = new ClassPool(parent);
child.appendSystemPath(); // 这默认使用相同的类路径
child.childFirstLookup = true; // 改变child的行为。
一个“新类”可以从一个已经存在的类copy出来。可以使用以下代码:
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get("Point");
cc.setName("Pair");
这段代码首先获取了Point的CtClass对象。然后调用setName() 方法给对象一个新的名字Pair。在这个调用之后,CtClass表示的类中的所有Point都会替换为Pair。类定义的其他部分不会变。
既然setName() 改变了ClassPool对象中的记录。从实现的角度看,ClassPool是一个hash表,setName() 改变了关联这个CtClass对象的key值。这个key值从原名称Point变为了新名称Pair。
因此,如果之后调用get("Point") ,就不会再返回上面的cc引用的对象了。ClassPool对象会再次读取class文件,然后构造一个新的CtClass对象。这是因为Point这个CtClass在ClassPool中已经不存在了。请看下面代码:
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get("Point");
CtClass cc1 = pool.get("Point"); // 此时,cc1和cc是完全一样的。
cc.setName("Pair");
CtClass cc2 = pool.get("Pair"); // cc2和cc是完全一样的
CtClass cc3 = pool.get("Point"); // cc3和cc是不一样的,因为cc3是重新读取的class文件
cc1和cc2引用的是相同的实例,和cc指向的是同一地址。但是,cc3却不是。注意,在执行cc.setName("Pair") 之后,cc和cc1引用的是同一地址,所以它们的CtClass都是代表Pair类。
ClassPool对象用于维护CtClass对象和类之间的一一映射关系。Javassist不允许两个不同的CtClass对象代表相同的类,除非你用两个ClassPool。这个是程序转换一致性的重要特性。
要创建ClassPool的副本,可以使用下面的代码片段(这个上面已经提到过了):
ClassPool cp = new ClassPool(true);
如果你又两个ClassPool对象,那么你就可以从这两个对象中获取到相同class文件但是不同的CtClass对象。你可以对那两个CtClass进行不同方式的修改,然后生成两个版本的Class。
一旦CtClass对象转化为Class文件后,比如writeFile() 或是 toBytecode() 之后,Javassist会拒绝CtClass对象进一步的修改。因此,在CtClass对象转为文件之后,你将不能再通过setNme() 的方式将该类拷贝成一个新的类了。比如,下面的这段错误代码:
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get("Point");
cc.writeFile();
cc.setName("Pair"); // 错, 因为cc已经调用了writeFile()
为了解除这个限制,你应该调用ClassPool 的 getAndRename() 方法。 例如:
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get("Point");
cc.writeFile();
CtClass cc2 = pool.getAndRename("Point", "Pair");
如果调用了getAndRename,ClassPool首先为了创建代表Pair的CtClass而去读取Point.class。然而,它在记录CtClass到hash表之前,会把CtClass由Point重命名为Pair。因此getAndRename() 可以在writeFile() 或 toBytecode() 之后执行。