β

javassist这货到底干嘛的?

IT男 4106 阅读

最近对一个快速开发的框架很感兴趣,使用这个框架的开发人员采用面向声明的开发模式,异常的简单和方便,而且效率也是原来的好几倍。自己曾经在公司写过些类似的东西,都是通过实体或数据库提供数据结构,模板提供页面或代码结构,生成需要实现的具体页面和代码。但这个框架我搭起来后,在目录文件里找了半天都没看到半点生成的代码和页面,他是如何实现的?

对,主要就是javassist这货的功劳!

平时在添加依赖jar包时,总少不了它,其实Javassist是一个动态字节码生成框架,包括支持两种API,Java语言级别和字节码级别。简单的说就是他可以通过配置,在程序运行时,生成临时的java代码,来实现当前需要的特定功能的框架。下面我们来看看如何使用这个强大的框架。

1、读写字节码

Javassist是一个处理Java字节码的类库。Java字节码存储在名叫class file的二进制文件里。每个class文件包含一个Java类或者接口。

Javassit.CtClass是一个class文件的抽象表示。一个CtClass(compile-time class)对象可以用来处理一个class文件。下面的代码是一个简单的示例:

ClassPool pool = ClassPool.getDefault();

CtClass cc = pool.get(“test.CtClass”);

cc.setSuperclass(pool.get(“test.SuperClass”));

cc.writeFile();

这段代码首先是取得一个ClassPool对象,它主要负责用Javassist来控制字节码的修改。ClassPool对象是CtClass对象的一个容器。它通过需要读取class文件来创建CtClass对象,并记录这个对象用于后续的操作。要修改一个类的属性,首先需要从一个ClassPool里取得表示这个类的CtClass对象的引用。ClassPool的get()方法就是这个作用的。在上面的例子里,表示test.Rectangle的 CtClass是从ClassPool里取到的,并且赋值给变量cc。通过getDefault()返回的ClassPool会按照系统的默认搜索路径进行搜索。

从实现的角度来讲,ClassPool是一个包含CtClass对象的hash table,它用class名词作为key。调用ClassPool的get()方法,就是通过名称来搜索指定的类。如果在hash table中没有找到,get()方法会读取相应的class文件,创建一个新的CtClass对象,把它保存在hash table里,然后作为结果返回。

从ClassPool里取得的CtClass对象是可以修改的。就像上面的例子,test.Rectangel的父类被修改为test.Point。这个修改会在最后调用writeFile()方法的时候体现到原始的class文件上。

在ClassPool加载类的体系中可以指定Child优先,通过如下代码:
child.childFirstLookup = true;

writeFile()方法把一个CtClass对象转换成一个class文件,然后把它写入到本地磁盘。Javassist也提供了直接获取修改后的字节码的方法。要获取字节码,调用toByteCode()方法:

byte[] b = cc.toByteCode()

当然,你也可以直接加载修改后的CtClass:调用toClass()方法请求当前线程上下文的class loader去加载这个CtClass所表示的类。它会返回一个表示这个类的java.lang.Class实例。

2、定义一个新的类

要完全定义一个新的类,需要在ClassPool上调用makeClass()方法。

ClassPool cp = ClassPool.getDefault();

CtClass cc = cp.makeClass(“CtClass”);

这段代码定义了一个没有成员变量的Point类。Point的成员方法可以通过调用CtNewMethod的工厂方法来创建,然后用CtClass的addMethod()方法添加到Point类上去。

makeClass()不能用来创建接口;如果想创建接口ClassPool的makeInterface()方法可以做到。接口里的成员方法可以用CtNewMethod的abstractMethod()来创建。注意接口的方法都是抽象方法。

3. 内醒和定制Introspection and customization

Javassist 不能删除方法或者域成员;不能给方法添加参数,必须通过创建新的方法满足。

Javassist通过内联的方式给方法添加代码块。方法的局部变量名是无法得到的,除非用-g参数编译类。通过$做前缀来访问。

insertBefore(), insertAfter(), andaddCatch()

Javassist提供了各种类来帮助修改不同类型的数据,其中$在不同的类型中意义有所差别或者Not available.

javassist.expr.MethodCall

javassist.expr.ConstructorCall

javassist.expr.FieldAccess

javassist.expr.NewExpr

javassist.expr.NewArray

javassist.expr.Instanceof

javassist.expr.Cast

javassist.expr.Handler//处理异常

添加方法时,如果方法之间有依赖调用,如果找不到定义则会失败。因此需要通过添加抽象方法的方式实现

CtClass cc = … ;

CtMethod m = CtNewMethod.make(“public abstract int m(int i);”, cc);

CtMethod n = CtNewMethod.make(“public abstract int n(int i);”, cc);

cc.addMethod(m);

cc.addMethod(n);

m.setBody(“{ return ($1 <= 0) ? 1 : (n($1 – 1) * $1); }”);

n.setBody(“{ return m($1); }”);

cc.setModifiers(cc.getModifiers() & ~Modifier.ABSTRACT);

添加域,应该赋初始值。

注意:Javassist生成的类一般不需要特殊辅助即可运行,但有些特殊情况需要用到一下包
javassist.runtime

4.冻结class

如果一个CtClass对象通过writeFile(),toClass()或者toBytecode()转换成了class文件,那么 Javassist会冻结这个CtClass对象。后面就不能继续修改这个CtClass对象了。这样是为了警告开发者不要修改已经被JVM加载的 class文件,因为JVM不允许重新加载一个类。一个被冻结的CtClass可以通过解冻来使得后续可以继续修改这个类的定义。例如,

CtClass cc = …;

cc.writeFile();

cc.defrost();

cc.setSuperclass(…);// 可以修改,因为这个class没有被冻结

在调用defrost()方法后,CtClass对象有可以被编辑了。

如果ClassPool.doPruning被设置成true,那么Javassist会在冻结一个对象的时候对这个对象进行精简。为了减少 ClassPool的内存占用,精简的时候会丢弃class中不需要的属性。例如Code_attribute结构(即是方法体)会被丢弃。因此,如果一 个CtClass对象被精简了,那么方法的字节码是不能访问的,留下的只有方法名,方法的签名和annotation。被精简的CtClass对象不能够 再被defrost。ClassPool.doPruning的默认值是false。
如果要阻止对某一个具体的CtClass对象的精简,需要在这个对象上先调用stopPruing()方法:

CtClass cc = …;

cc.stopPruning(true);

cc.writeFile(); // 转换成一个class文件,cc没有被精简

这个CtClass对象cc没有被精简。所以在调用writeFile()方法后可以被解冻。

5.类搜索路径

静态方法ClassPool.getDefault()返回的默认ClassPool的类搜索路径和当前的JVM保持一致。如果一个程序运行在 JBoss或者Tomcat这样的web service上时,ClassPool可能会找不到用户的class,因为像这样的web service除了用system class loader外,还会用多个其他的class loader。在这种场景下,需要把额外的搜索路径注册到ClassPool上去。假设下面的pool表示一个ClassPool:

pool.insertClassPath(new ClassClassPath((this.getClass()));

这个语句把装载this所引用的class的class path注册到ClassPool上。你可以用任何的Class对象做为参数来替代this.getClass(),用来装载Class的class path会被注册进来。

你也可以用一个目录名作为类搜索路径。例如,下面的代码吧/usr/local/javalib加到当前的搜索路径里:

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.包里的类。例如,要加载org.javassist.test.Main,这个类会被从:
http://www.javassist.org:80/java/org/javassist/test/Main.class
更进一步说,你可以直接给ClassPool类传入一个byte数组,然后从这个数组里创建一个CtClass对象。要实现这个,可以使用ByteArrayClassPath。例如,

ClassPool cp = ClassPool.getDefault();

byte[] b = aByteArray;

String name = className;

cp.insertClassPath(new ByteArrayClassPath(name, b));

CtClass cc = cp.get(name);

取得的CtClass对象被b所指定的class文件所代表的类。如果调用get()方法的时候,如果传入的名称和之前指定的name一致的话,ClassPool会从指定的ByteArrayClassPath读入一个class文件。

如果你不知道这个类的全局限定名,那么你就可以用ClassPool的makeClass()方法:

ClassPool cp = ClassPool.getDefault();

InputStream ins = anInputStreamForReadingAClassFile;

CtClass cc = cp.makeClass(ins);

makeClass()会返回从指定的输入流创建的CtClass对象。你可以makeClass()来更快地把一个class文件加入到 ClassPool里。这样做会在搜索路径里包含一个很大的jar文件的时候提高性能。因为ClassPool是即时读取class文件的,它可能会在每 次加载类的时候都需要搜索整个jar文件。makeClass()可以用来优化这个搜索。这个由makeClass()创建的CtClass会保留在 ClassPool里,并且class文件不需要再次读入了。

用户也可以扩展类搜索路径。他们可以实现ClassPath接口,这样就可以让一些不符合规范的资源也能够加入到类搜索路径里。

6.其他注意事项

该框架支持泛型,使用范型时,以普通类型对待。

Bytecode level API 字节码级别的API提供了更强的能力,包括添加和删除成员(包括方法)。

Javassist不支持可变参数,将转成数组来访问。

作者:IT男
一个IT男生活的点点滴滴,让你对IT男有一个崭新的认识!
原文地址:javassist这货到底干嘛的?, 感谢原作者分享。

发表评论