简介:
javassist是一款可以在运行时生成字节码的工具,可以通过它来构造一个新的class对象、method对象,这个class是运行时生成的。可以通过简短的几行代码就可以生成一个新的class type
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get("test.Rectangle");
cc.setSuperclass(pool.get("test.Point"));
//CtClass cc = pool.makeClass("Point");
cc.writeFile("path");
上面的代码就会把新生成的class 文件写到文件系统中,javassist封装了从 方法字符串 到 字节码的逻辑,用户可以方便的像写程序一样,生成一个新类。
运行时动态生成class,有点类似cglib,和cglib不同的是,javassist可以直接编辑类里面的属性、方法源码,而cglib没有封装这些接口。
举例说明,在实现一个aop功能时,javassist可以通过重新写method的源码来实现,而cglib需要实现类型MethodInvoker这种接口。
javassist可以修改method的源代码,执行这些代码不需要反射,就像执行提前编写好的硬代码一样,而cglib的callback是反射实现的。当然,他们的效率不会有太大差异,各种缓存策略也保证了他们的执行效率;
下面是一些基础知识
CtClassCtClasss cc = ...;
:
cc.writeFile();
cc.defrost();
cc.setSuperclass(...); // OK since the class is not frozen.
After defrost()
is called, the CtClass
object can be modified again.
如果想让CtClass不可修改,可以使用stopPruning
CtClasss cc = ...;
cc.stopPruning(true); //不能修剪
:
cc.writeFile(); // convert to a class file.
// cc is not pruned.
一般来说运行时再修改CtClass风险太大,不建议修改它,尤其是执行toClass方法后
Class clz = cc.toClass();
T instance = (T) clz.newInstance();
由于ClassPool.getDefault() 搜索class时用的classpath和当前的是JVM级的class path是相同的,同时它的classLoader是当前线程上下文的classloader,也就是App classloader,因此如果一个应用使用tomcat启动时,可能无法找到当前webapp对应的用户的classpath(一个tomcat占用一个jvm,而一个tomcat可以运行多个webapp,每个webapp都是不同的class loader、classpath),也就无法搜索到对应的class。
可以使用
pool.insertClassPath(new ClassClassPath(this.getClass()));
来插入classpath,这样,不同的webapp 搜索class 时,也就能找到自己的class,而不会交叉。
规避内存溢出
如果ClassPool中有非常多的CtClass
,有可能会导致内存溢出,因此提供了一个方法,可以把不用的CtClass
删掉
CtClass cc = ... ;
cc.writeFile();
cc.detach();
调用detach()方法后,就不能再操作CtClass对象了,但是可以通过 pool.get("test.Rectangle") 来重新加载该对象
或者重新创建一个ClassPool(按照文档的意思,是里面的CtClass你也丢弃了),没有引用链的老ClassPool就被垃圾回收了,包括里面的CtClass
ClassPool cp = new ClassPool(true);
// if needed, append an extra search path by appendClassPath()
//因为 new ClassPool(true)相当于ClassPool cp = new ClassPool();cp.appendSystemPath(); // or append another path by appendClassPath()
个人理解在tomcat启动的应用,构建ClassPool有两种方式
1:
ClassPool parent = ClassPool.getDefault();
ClassPool pool = new ClassPool(parent);
//tomcat下启动,不同的webapp有不同的classpath
pool.insertClassPath(new ClassClassPath(ProxyFactory.class));
2:
ClassPool pool = new ClassPool(false);
//tomcat下启动,不同的webapp有不同的classpath
pool.insertClassPath(new ClassClassPath(ProxyFactory.class));
区别就是第2中没有parent classloader,全部的CtClass都在自己的ClassPool对象中,而1中,有些CtClass可能被放到parent中
下面的ClassPool对象,通过get方法获取CtClass的源码
/**
* @param useCache false if the cached CtClass must be ignored.
* @return null if the class could not be found.
*/
protected synchronized CtClass get0(String classname, boolean useCache)
throws NotFoundException
{
CtClass clazz = null;
if (useCache) {
clazz = getCached(classname);
if (clazz != null)
return clazz;
}
if (!childFirstLookup && parent != null) { //childFirstLookup默认值为false
clazz = parent.get0(classname, useCache);
if (clazz != null)
return clazz;
}
clazz = createCtClass(classname, useCache);
if (clazz != null) {
// clazz.getName() != classname if classname is "[L<name>;".
if (useCache)
cacheCtClass(clazz.getName(), clazz, false);
return clazz;
}
if (childFirstLookup && parent != null)
clazz = parent.get0(classname, useCache);
return clazz;
}
Classloader部分:待续
顺便一提tomcat中启动应用的classloader结构,下图中从下到上的关系中,上为parent。
Bootstrap classloader 是最底层的ClassLoader,它没有parent,加载的是java最核心的类和包,相传它是C++直接写的;
ExtClassLoader加载的是扩展包%JAVA_HOME%/jre/lib/ext目录下的一些包,它的parent是null,表示它是仅次于Bootstrap ClassLoader,也属于最底层的ClassLoader;
AppClassLoader就是我们运行一个普通的java 程序时,我们自己写的类会使用AppClassLoader来加载;
基于tomcat容器,tomcat会在AppClassLoader上创建子ClassLoader StandardClassLoader,而此时的AppClassLoader仅会加载tomcat的bootstrap.jar和juli.jar,StandardClassLoader则会加载更多的tomcat的lib包,作为一个基础的ClassLoader,这么做的用意是防止tomcat的lib包影响到tomcat的正常启动;
WebAppClassLoader是扔进tomcat中的引用代码(即我们自己的类)的类加载器,它的parent是StandardClassLoader;
同时,WebAppClassLoader加载的类,它的线程上下文ClassLoader也会被设置成WebAppClassLoader,如果一个tomcat中有两个应用,很显然,他们是两个类加载器,因此,javassist文档中的注意事项也是可以忽略的,原因是ClassPool.getDefault()方法会使用线程上下文ClassLoader,因此它会找到一个正确的ClassLoader,也就对应了一个正确的classpath
内省和定制
内省这里简单理解成调用属性域的get,set方法,在javassist中,通过java 反射api实现,比如你通过定制在CtClass中新添了一个属性,然后你可以调用set方法来给该属性赋值
下面来说说定制,javassist中,method对象的原型是CtMethod
CtMethod ctMethod = CtNewMethod.make(sb.toString(), cc);
上面的代码是增加一个新的方法,sb.toString()代表的是这个方法的字符串,如“public int geti(){return i;}”,它是一个完整的方法体
同时ctMethod有多个方法可以操作
获取CtMethod 对象后,还可以再操作这个方法,如insertBefore(),insertAfter()等等
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get("Point");
CtMethod m = cc.getDeclaredMethod("move");
m.insertBefore("{ System.out.println($1); System.out.println($2); }");
cc.writeFile();
标识符,它们都以$开头,适用于编写方法,获取或操作方法的参数
$0, $1, $2, ... $0表示this, $n,...获取第n个参数
$args An array of parameters. The type of $args is Object[].
int 会被转换成Integer,再用$args[0]时,会转回int
$$ All actual parameters.
For example, m($$) is equivalent to m($1,$2,...)
$cflow(...) cflow variable 返回递归调用成层数,0表示调用一次
$r The result type. It is used in a cast expression. 与$w相反
$w The wrapper type. It is used in a cast expression. ($w)$1可以把int转成Integer
$_ The resulting value
$sig An array of java.lang.Class objects representing the formal parameter types.
$type A java.lang.Class object representing the formal result type.
$class A java.lang.Class object representing the class currently edited.
在编码过程中,我遇到过int 直接放入Object[]时,VefifyTypeError,即Object[]中只能放封装类型,如Integer,而不能放原始类型int,而平常我们写代码试,把int放入Object[]时没有报错的原因是java编译器在编译成class文件时,已经将int做了转换。而目前的javassist还没有这么智能,但是预留了$w来处理这个问题。
关于各种标识符,可以参考官方文档:http://jboss-javassist.github.io/javassist/tutorial/tutorial2.html
使用javassist的代码:
https://github.com/jianliu/lsf/blob/master/src/main/java/com/liuj/lsf/client/ProxyFactory.java