Skip to content

Latest commit

 

History

History
105 lines (65 loc) · 7.42 KB

Javassist-第二章-ClassPool详解.md

File metadata and controls

105 lines (65 loc) · 7.42 KB

[toc]

1. ClassPool简介

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方法

2. 避免内存溢出

某种特定的ClassPool可能造成巨大的内存消耗,导致OOM,比如CtClass对象变得非常的(这个发生的很少,因为Javassist已经尝试用不同的方法减少内存消耗了,比如冻结类)。为了避免该问题,你可以从ClassPool中移除不需要的CtClass对象。只需要调用CtClassdetach() 方法就行了:

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 ClassPoolClassPool.getDefault() 的效果是一样。注意,ClassPool.getDefault() 是一个单例的工厂方法,它只是为了方便用户创建提供的方法。这两种创建方式是一样的,源码也基本是一样的,只不过**ClassPool.getDefault()**是单例的。

注意,new ClassPool(true) 是一个很方便的构造函数,它构造了一个ClassPool对象,然后给他增添了系统搜索路径。它构造方法的调用就等同于下面的这段代码:

ClassPool cp = new ClassPool();
cp.appendSystemPath();  // 你也可以通过appendClassPath()增添其他路径

3. 级联ClassPool

如果一个程序运行在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的行为。

4. 更改类名的方式定义新类

一个“新类”可以从一个已经存在的类copy出来。可以使用以下代码:

ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get("Point");
cc.setName("Pair");

这段代码首先获取了PointCtClass对象。然后调用setName() 方法给对象一个新的名字Pair。在这个调用之后,CtClass表示的类中的所有Point都会替换为Pair。类定义的其他部分不会变。

既然setName() 改变了ClassPool对象中的记录。从实现的角度看,ClassPool是一个hash表,setName() 改变了关联这个CtClass对象的key值。这个key值从原名称Point变为了新名称Pair

因此,如果之后调用get("Point") ,就不会再返回上面的cc引用的对象了。ClassPool对象会再次读取class文件,然后构造一个新的CtClass对象。这是因为Point这个CtClassClassPool中已经不存在了。请看下面代码:

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文件

cc1cc2引用的是相同的实例,和cc指向的是同一地址。但是,cc3却不是。注意,在执行cc.setName("Pair") 之后,cccc1引用的是同一地址,所以它们的CtClass都是代表Pair类。

ClassPool对象用于维护CtClass对象和类之间的一一映射关系。Javassist不允许两个不同的CtClass对象代表相同的类,除非你用两个ClassPool。这个是程序转换一致性的重要特性。

要创建ClassPool的副本,可以使用下面的代码片段(这个上面已经提到过了):

ClassPool cp = new ClassPool(true);

如果你又两个ClassPool对象,那么你就可以从这两个对象中获取到相同class文件但是不同的CtClass对象。你可以对那两个CtClass进行不同方式的修改,然后生成两个版本的Class。

5. 重命名冻结类的方式定义新类

一旦CtClass对象转化为Class文件后,比如writeFile() 或是 toBytecode() 之后,Javassist会拒绝CtClass对象进一步的修改。因此,在CtClass对象转为文件之后,你将不能再通过setNme() 的方式将该类拷贝成一个新的类了。比如,下面的这段错误代码:

ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get("Point");
cc.writeFile();
cc.setName("Pair");    // 错, 因为cc已经调用了writeFile()

为了解除这个限制,你应该调用ClassPoolgetAndRename() 方法。 例如:

ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get("Point");
cc.writeFile();
CtClass cc2 = pool.getAndRename("Point", "Pair"); 

如果调用了getAndRenameClassPool首先为了创建代表PairCtClass而去读取Point.class。然而,它在记录CtClass到hash表之前,会把CtClassPoint重命名为Pair。因此getAndRename() 可以在writeFile()toBytecode() 之后执行。