1、读和写字节码
Javassist是一个处理Java字节码的库,java字节码是使用二进制格式存储在文件中的话,我们就称之为一个字节码文件,每个字节码文件包含着一个class类或一个interface。
Javassist.CtClass对应着一个对象的字节码文件,CtClass对象就是用来处理字节码文件的,以下就是一个非常简单的例子:
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get("test.Rectangle");
cc.setSuperclass(pool.get("test.Point"));
cc.writeFile();
这段代码首先获取了一个用来控制字节码修改的ClassPool对象,ClassPool对象是一个CtClass对象的集合,这个类可以读取一个字节码文件,并构造出一个CtClass对象,我们使用CtClass便可以修改一个已经定义的类。
上面的例子中,CtClass对象代表着一个从classPool类中拿到的test.Rectangle字节码对象,并定义成cc,ClassPool通过getDefault方法从默认的系统的搜索路径中获取的。
ClassPool 是一个包含CtClass对象的hash表结构,它通过get()方法通过特定的key值从hash表里拿CtClass,如果没有找到的话,它变会构建一个新的CtClass。
CtClass对象是可以修改的,上面的例子中它的父类被修改成"test.Point"类,如果writeFile()方法被执行的话,最终它代表的类的字节码文件将会被修改。
Javassist也提供了一个直接获取修改字节码文件后的字节数组
byte[] b = cc.toBytecode()
同时也可以通过加载toClass()方法拿到对应的字节码对象,toClass()代表请求当前线程的ClassLoader去加载CtClass代表的字节码文件。
- Frozen classes
如果一个CtClass对象被writeFile()、toClass()、toBytecode()转化成一个class文件,Javassist冻住了该CtClass对象的话,后续我们就不能修改这个CtClass对象了,这是为了提醒开发者,JVM并不允许我们修改一个已经被加载的字节码文件。
一个处于冰冻状态的CtClass如果执行defrost()方法后,就接触冰冻状态了,这个CtClass对象又允许我们修改了。
CtClasss cc = ...;
:
cc.writeFile();
cc.defrost();
cc.setSuperclass(...); // OK since the class is not frozen.
当ClassPool.doPruning 设置为true时,当Javassist冰冻这个对象时,Javassist可以修改CtClass代表的类结构,这样是为了减少内存消耗,删除一些类中不必要的属性,例如代码的属性结构-方法体,因此当一个CtClass对象被修剪后,方法的字节码是不可以访问的,但方法名,签名和注解除外。已经被修剪过的CtClass
是不能再次被解冻的。另外ClassPool的doPruning字段默认是false。
为了禁止修剪一个特定的CtClass,建议一定要像下面一样调用StopPruning()
方法:
CtClasss cc = ...;
cc.stopPruning(true);
:
cc.writeFile(); // convert to a class file.
// cc is not pruned.
CtClass对象cc是不运行被修剪的,因此在调用writeFile方法被调用后,它还能够被解冻。
注意,当debug的时候,你可能想暂时暂停修剪并且冰冻和把一个被修改的class文件写进硬盘,此时用debugWriteFile()便可以达到此目的,当停止修剪,便把该CtClass写进硬盘,解冻它,此时又可以修改它。
- 类搜索路径
classPool的getDefault()返回的ClassPool,是通过JVM的默认class加载路径去搜索的。如果一个运行在服务器的应用,例如JBoss和Tomcat,ClassPool对象可能并不能找到用户的字节码,因为这样一个远程服务器使用多个classLoader类加载器,这种情况下,我们必须把classPath注册进ClassPool
pool.insertClassPath(new ClassClassPath(this.getClass()));
你可以注册一个路径作为类搜索路径,例如:
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath("/usr/local/javalib");
当然我们还可以添加一个URL作为类搜索路径:
ClassPool pool = ClassPool.getDefault();
ClassPath cp = new URLClassPath("www.javassist.org", 80, "/java/", "org.javassist.");
pool.insertClassPath(cp);
这个程序添加“http://www.javassist.org:80/java/”作为字节码搜索路径,这个URL被当做搜索属于org.javassist包下的classes,例如为了加载一个org.javassist.test.main,它的字节码文件可以从下面的路径中获取:
http://www.javassist.org:80/java/org/javassist/test/Main.class
此外,你还可以使用ByteArrayClassPath直接拿一个byte数组构造出一个CtClass对象,例如
ClassPool cp = ClassPool.getDefault();
byte[] b = a byte array;
String name = class name;
cp.insertClassPath(new ByteArrayClassPath(name, b));
CtClass cc = cp.get(name);
如果你不知道类的全名称,然后你可以使用ClassPool的makeClass()方法构造出CtClass对象。
ClassPool cp = ClassPool.getDefault();
InputStream ins = an input stream for reading a class file;
CtClass cc = cp.makeClass(ins);
2、ClassPool
避免OOM
ClassPool如果加载了很多内存较大的对象,可能会导致很大的内存消耗,
为了避免这种问题,你可以使用CtClass类的detch()方法,把CtClass对象从ClassPool中移除,例如:
CtClass cc = ... ;
cc.writeFile();
cc.detach();
如果一个CtClass对象已经被detach了,那么就不能调用它的任何方法了,
然而,你可以调用get()方法从ClassPool中new一个新的同样的CtClass对象。当然我们也可以自己new一个ClassPool,如下:
ClassPool cp = new ClassPool(true);
// if needed, append an extra search path by appendClassPath()
3、Class Loader
- 3.1 CtClass的toClass方法
CtClass有一个非常方便的toClass方法,这个方法会请求当前线程的classLoader去加载CtClass对象对应的对象,调用的时候必须要有合适的权限,否则的话会抛出SecurityException异常,下面的例子会展示如何使用CtClass()方法。
public 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.insertBefore("{ System.out.println(\"Hello.say():\"); }");
Class c = cc.toClass();
Hello h = (Hello)c.newInstance();
h.say();
}
}
如果在执行toClass()方法之前,Hello类被加载过,那么程序将抛出LinkageError异常,因为class loader不能同时加载两个不同版本的hello class 类, 例如:
public static void main(String[] args) throws Exception {
Hello orig = new Hello();
ClassPool cp = ClassPool.getDefault();
CtClass cc = cp.get("Hello");
:
}
如果程序运行在服务器端,即存在多个类加载器的情况,toClass()使用默认的ClassLoader加载类可能会出现ClassCastException,为了避免这种异常,我们应该给toClass()方法设置一个合理的ClassLoader去加载类,例如:
CtClass cc = ...;
Class c = cc.toClass(bean.getClass().getClassLoader());
不同的类加载器加载的类,及时类的名称一样,但其实还是两个不同的类。如果JVM加载一个类后,会做一次强制转换,有可能便会抛出强制转换的异常。如下面代码所示:
MyClassLoader myLoader = new MyClassLoader();
Class clazz = myLoader.loadClass("Box");
Object obj = clazz.newInstance();
Box b = (Box)obj; // this always throws ClassCastException.
在我们现实开发中要尽量避免出现多个classLoader加载两个名字一样的类的情况。
- 3.2 使用javassist.Loader
Javassist提供了一个javassist.Loader的类加载器,这个类加载器使用一个ClassPool对象读取一个字节码文件。下面是Javassist.Loader加载一个已经修改的特定类例子:
import javassist.*;
import test.Rectangle;
public class Main {
public static void main(String[] args) throws Throwable {
ClassPool pool = ClassPool.getDefault();
Loader cl = new Loader(pool);
CtClass ct = pool.get("test.Rectangle");
ct.setSuperclass(pool.get("test.Point"));
Class c = cl.loadClass("test.Rectangle");
Object rect = c.newInstance();
:
}
如果一个用户想按照需要修改一个已经加载的类,用户可以给javassist.Loader添加一个监听,当classLoader加载类的时候,事件监听将会进行回调。这个事件监听必须实现下面的接口:
public interface Translator {
public void start(ClassPool pool)
throws NotFoundException, CannotCompileException;
public void onLoad(ClassPool pool, String classname)
throws NotFoundException, CannotCompileException;
}
start()方法将会在这个listener添加到javassist.Loader对象时执行,也就是javassist.Loader的addTranslator()方法被调用时执行;
onLoad()方法会在javassist.Loader加载一个class之前调用。
import javassist.*;
public class Main2 {
public static void main(String[] args) throws Throwable {
Translator t = new MyTranslator();
ClassPool pool = ClassPool.getDefault();
Loader cl = new Loader();
cl.addTranslator(pool, t);
cl.run("com.lianjia.link.mytransformplugin.Test", args);
}
}
Test类的代码如下:
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("com.lianjia.link.mytransformplugin.Hello");
CtMethod m = cc.getDeclaredMethod("say");
m.insertBefore("{ System.out.println(\"Hello.say():\"); }");
Class c = cc.toClass();
Hello h = (Hello)c.newInstance();
h.say();
}
}
上面代码是用javassist.Loader去加载Test这个类,但Test这个类是系统的ClassLoader已经加载过的,所以上述代码会报错。
- 3.3 自定义一个classLoader
import javassist.*;
import java.io.IOException;
public class SampleLoader extends ClassLoader {
/* Call MyApp.main().
*/
public static void main(String[] args) throws Throwable {
SampleLoader s = new SampleLoader();
Class c = s.loadClass("com.lianjia.link.mytransformplugin.Test");
c.getDeclaredMethod("main", new Class[] { String[].class })
.invoke(null, new Object[] { args });
}
private ClassPool pool;
public SampleLoader() throws NotFoundException {
pool = new ClassPool();
pool.insertClassPath("./class"); // MyApp.class must be there.
}
/* Finds a specified class.
* The bytecode for that class can be modified.
*/
protected Class findClass(String name) throws ClassNotFoundException {
try {
CtClass cc = pool.get(name);
// modify the CtClass object here
byte[] b = cc.toBytecode();
return defineClass(name, b, 0, b.length);
} catch (NotFoundException e) {
throw new ClassNotFoundException();
} catch (IOException e) {
throw new ClassNotFoundException();
} catch (CannotCompileException e) {
throw new ClassNotFoundException();
}
}
}
执行结果:
Hello.say():
Hello
Process finished with exit code 0
4、定制化修改类的结构
Javassist和java反射的api很类似,CtClass提供了getName(),getSuperClass(),getMethods(),同事CtClass也提供了修改类的方法,Javassist也允许添加一个新的字段、构造方法和普通方法,构造一个方法体也是可能的。
Methods在Javassist中对应着CtMethod类,它提供了一些修改类方法的功能,注意如果一个方法是集成基类的,要修改这类的话,要通过代表基类的CtMethod来实现。一个CtMethod对象对应着每一个定义的方法。
Javassist不允许移除一个方法或字段,但是它允许改变方法和字段的名称,如果一个方法不再需要之后,它应该通过CtMethod的setName()方法和setModifiers()方法,被重命名和修改成私有的。
Javassist不允许给已经存在的方法添加一个额外的参数,如果非要给现有方法添加参数,你可以定义一个新的方法去实现。
Javassist同时也提供了api直接来修改一个类的字节码文件,例如通过CtClass的getClassFile()方法返回一个代表不成熟字节码文件的ClassFile对象,通过CtMethod的getMethodInfo()方法返回一个代表方法结构的MethodInfo对象,这些api使用到了java虚拟机定义的语法,所以我们必须要对字节码知识有所了解。详情请看javassist.bytecode package.
Javassist修改字节码文件要用到Javassist.runtime包,它支持在程序运行的时候,使用一些包含$的特殊标识符,下面我们会介绍这些特殊运算符的用法,需要了解更多的话,可以了解下javassist.runtime包的文档。