系列文章目录
第一章 读写字节码 第二章 类池
前言
在上一章我们介绍了Javassist读取字节码的一些操作,本章我们会介绍Javassist中的ClassPool。
类池
ClassPool对象是由许多个CtClass对象构成的容器。一旦一个CtClass对象被创建出来,它就会被永远地记录在某个ClassPool中。这是因为编译器在编译引用该CtClass所表示的类的源代码时可能需要访问CtClass对象。如果CtClass对象所代表的Point 类丢失的话,编辑器将不能编译对getter方法的调用。
例如,一个新方法getter被添加到一个CtClass 对象所代表的Point类中。稍后,该程序尝试编译包括对Point中getter方法调用的源代码,,并将编译后的代码用作方法体,该方法体将被添加到另一个类Line中。
避免内存溢出
如果CtClass对象的数量变得非常多(这种情况很少发生,因为Javassist试图以各种方式减少内存消耗),那么类池的规模可能会导致巨大的内存消耗。为了避免这个问题,可以从类池中显式删除不必要的CtClass对象。如果你调用CtClass对象的detach方法,那么这个CtClass对象将从类池中删除。例如:
CtClass cc = ... ;
cc.writeFile();
cc.detach();
在一个CtClass 对象的detach方法被调用后,您将不能再调用该对象的任何方法。然而你可以调用ClassPool 对象的get方法来创建一个原来的CtClass 对象的新实例。如果你调用了ClassPool 对象的get方法,ClassPool 会再次读取一个类文件,然后去创建一个由get方法返回的新的CtClass 对象。
另一个解决方案是用一个新N收,则该类池中包含的CtClass对象也会被垃圾收集器回收。要创建新的ClassPool的实例,请执行以下代码段:
ClassPool cp = new ClassPool(true);
以上代码创建了与ClassPool.getDefault方法返回的默认ClassPool 具有相同的行为。注意ClassPool.getDefault方法是一个提供便捷的单例工厂方法。它以如上所示的方式创建一个ClassPool 对象。尽管它保留了一个ClassPool 实例并重用它,getDefault方法返回的ClassPool 对象并没有特殊用途。getDefault方法是一个简便的方法。
请注意,new ClassPool(true)是一个方便的构造函数,它构造一个ClassPool对象并将系统搜索路径附加到它。调用该构造函数等效于以下代码:
ClassPool cp = new ClassPool();
cp.appendSystemPath();
级联类池
如果程序在 Web 应用程序服务器上运行,则可能需要创建多个Classpool实例;ClassPool应该为每个类加载器(即容器)创建一个实例。程序不应该通过调用getDefault方法创建ClassPool 对象,而应该使用ClassPool的构造器去创建ClassPool 对象。
多个ClassPool 对象可以像java.lang.ClassLoader一样串联在一起。例如:
ClassPool parent = ClassPool.getDefault();
ClassPool child = new ClassPool(parent);
child.insertClassPath("./classes");
如果child的get方法被调用,子类池首先委托给父类池去获取CtClass对象。如果父类池没有发现CtClass对象的类文件,则子类池尝试在./classes目录下去查找CtClass对象的类文件。
如果child 的childFirstLookup 属性为true的话,子类池会尝试在委托父类池之前查找CtClass对象的类文件。例如:
ClassPool parent = ClassPool.getDefault();
ClassPool child = new ClassPool(parent);
child.appendSystemPath();
child.childFirstLookup = true;
更改类名以定义新类
新类可以用现有的类拷贝而来。下面的程序可以做到这点:
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get("Point");
cc.setName("Pair");
这段代码首先获得Point类的CtClass对象。然后调用setName方法为CtClass 对象cc赋予一个新的名字Pair 。在这次的方法调用之后,由该对象表示的类定义中所有出现的类名都从Point 更改为Pair。类定义的其他部分没有改变。
注意 ,CtClass 对象中的setName方法会改变ClassPool 对象中的一条记录。从实现的角度来看,ClassPool 对象是一个由CtClass 对象组成的哈希表。setName方法改变哈希表中CtClass 对象关联的key。key从原始的类名改成了新的类名。
因此,如果get(“Point”)方法稍后在 ClassPool对象上再次调用,则它永远不会返回 CtClass变量cc引用的对象。该ClassPool对象再次读取一个类文件 Point.class,并CtClass 为 class 构造一个新对象Point。这是因为与Point关联的CtClass对象不存在了,请参考下面的代码:
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get("Point");
CtClass cc1 = pool.get("Point");
cc.setName("Pair");
CtClass cc2 = pool.get("Pair");
CtClass cc3 = pool.get("Point");
cc1与cc2引用着相同的CtClass实例,cc3却与cc引用的实例不同。注意在cc.setName(“Pair”)这行语句执行之后,cc和cc1的CtClass 对象指的是Pair类。
ClassPool 对象用于维护类和CtClass对象之间的一对一的映射关系。Javassist 禁止两个不同的CtClass 对象表示相同的类,除非创建了两个独立的ClassPool对象。这是一致性程序转换的一个重要特性。
如果想要创建由ClassPool.getDefault方法返回的默认实例的另一个副本,请执行以下代码片段(此代码已在上面显示):
ClassPool cp = new ClassPool(true);
如果你有两个ClassPool 对象,你就可以从每一个ClassPool中获取表示相同的类文件的不同的CtClass 对象。你可以对这些CtClass 对象进行不同的修改然后生成这个类的不同版本。
重命名冻结类以定义新类
一旦一个CtClass 对象由writeFile方法或toBytecode方法转换成一个类文件,Javassist 会拒绝对那个CtClass 对象更多的修改。因此,在CtClass 对象所代表的Point 类被转换成类文件之后。你将无法定义Pair 类作为Point的副本,因为对Point调用的setName方法被拒绝执行了。以下代码是错误的示范:
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get("Point");
cc.writeFile();
cc.setName("Pair");
为避免此类限制,应当调用ClassPool中的getAndRename方法。例如:
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get("Point");
cc.writeFile();
CtClass cc2 = pool.getAndRename("Point", "Pair");
如果getAndRename方法被调用,ClassPool 首先读取Point类去创建一个表示Point 类的新的CtClass 对象。但是,getAndRename方法会在将该对象记录在哈希表中之前将该对象从Point重命名为Pair。因此getAndRename方法可以在writeFile方法和toBytecode方法在CtClass 对象所表示的Point 类上调用之后执行。
总结
本篇文章介绍了Javassist的类池ClassPool,主要讲解了级联类池的用法以及CtClass对象的数量变得非常多的时候如何避免内存溢出的功能。
说明
|