Javassit提供了运行时操作Java字节码的方法,其效率低于asm。javassist主要是提供了代码级别的修改(也有bytecode级别),相比与asm的字节码级别的修改,学习成本低,开发效率高。因此,在实际应用中javassist是一个非常不错的选择。以下是在使用javassist的过程中碰到的问题及处理方法:
1、ClassLoader问题
我们知道java中有ExtClassLoader、AppClassLoader等来加载运行时需要的字节码,同时系统也允许我们自定义ClassLoader来实现不同的加载方式(如tomcat实现的加载机制)。在实际应用中会有这样的问题,如AClassLoader加载/home/admin/a/目录下的类A,BClassLoader加载/home/admin/b目录下的类B,类A想要引用B是无法引用成功的,因为类A的ClassLoader无法找到类B的定义。解决的方法就是加载B时指定BClassLoader去加载。对于Javassit来说,要想修改某个类,必须要先加载类信息,因此也存在类加载问题。知道了问题,处理起来就比较简单了,javassist中有一个ClassPath接口,该接口提供了查找类、加载类的字节码的方法。在遇到ClassLoader问题时,我们可以使用LoaderClassPath来处理,代码如下:
ClassPool pool = new ClassPool(true);
pool.appendClassPath(new LoaderClassPath(classLoader));
ClassPath还有其他的实现来应对不同的情况:ByteArrayClassPath、ClassClassPath、DirClassPath、JarClassPath、JarDirClassPath、UrlClassPath。
如果一个应用中有存在多个不同的ClassLoader,建议对不同的ClassLoader创建不同的ClassPool,示例代码:
private static ConcurrentHashMap<ClassLoader, ClassPool> CLASS_POOL_MAP = new ConcurrentHashMap<ClassLoader, ClassPool>();
/**
* 不同的ClassLoader返回不同的ClassPool
* @param loader
* @return
*/
public static ClassPool getClassPool(ClassLoader loader) {
if (null == loader) {
return ClassPool.getDefault();
}
ClassPool pool = CLASS_POOL_MAP.get(loader);
if (null == pool) {
pool = new ClassPool(true);
pool.appendClassPath(new LoaderClassPath(loader));
CLASS_POOL_MAP.put(loader, pool);
}
return pool;
}
2、内存占用问题
javassist在加载类时会用Hashtable将类信息缓存到内存中,这样随着类的加载,内存会越来越大,甚至导致内存溢出。如果你的应用中要加载的类比较多,建议在使用完CtClass之后删除缓存:CtClass.detach()。
3、class的NotFoundException问题
NotFoundException包括找不到类定义、找不到方法定义等等,我们这里主要讨论找不到类定义的情况。你可能会觉得奇怪,前面不是有这么多ClassPath实现,难道还有这些ClassPath没有覆盖的情况? 是的,确实存在这种状态。比如我们使用javassist生成了一个自定义的类C, 由于该类完全是在内存中生成的,你无法通过一个具体的路径找到它,因此如果你后续希望再引用C,你可能会找不到它。为什么是可能? javassist在加载类时会将其信息缓存起来,然而有的应用因为内存方面的考虑,会通过detach移除缓存信息。对于普通的类来说,缓存移除后通过添加LoaderClassPath或者其他ClassPath的方式可以重新加载,但是对于javassist动态生成的类来说,由于其只在内存中存在,因此无法再次找到其信息。 知道了问题以后,我们可以怎么处理呢?
a) 在CtClass.detach()之前,将生成的字节码保存到指定目录下:CtClass.writeFile(dir), 然后通过指定DirClassPath来重新加载信息。
b) 如果CtClass操作已经被封装,无法加入writeFile方法的话,可以在系统启动时指定静态变量CtClass.debugDump="/home/admin/code_cache/dump"(早期的版本中可能没有这个变量); 然后在需要对动态类进行二次代理时调用:
pool.appendClassPath(new DirClassPath("/home/admin/code_cache/dump"));
4、特殊变量
javassist提供了一些特殊的变量来方便你操作(http://jboss-javassist.github.io/javassist/tutorial/tutorial2.html#before):
$0, $1, $2, … $0表示this,其他的表示实际的参数
$args 参数数组. 相当于new Object[]{$1,$2,…},其中的基本类型会被转为包装类型
所
有
的
参
数
,
如
m
(
所有的参数,如m(
所有的参数,如m()相当于m($1,
2...
)
,
如
果
m
无
参
数
则
m
(
2...),如果m无参数则m(
2...),如果m无参数则m($)相当于m()
$cflow(…) 表示一个指定的递归调用的深度
$r 用于类型装换,表示返回值的类型.
w
将
基
础
类
型
转
换
为
一
个
包
装
类
型
.
如
I
n
t
e
g
e
r
a
=
(
w 将基础类型转换为一个包装类型.如Integer a=(
w将基础类型转换为一个包装类型.如Integera=(w)5;表示将5转换为Integer。如果不是基本类型则什么都不做。
$_
返回值,如果方法为void,则返回值为null; 值在方法返回前获得,
如果希望发生异常是有返回值(默认值,如nul),需要将insertAfter方法的第二个参数asFinally设置为true
$sig 方法参数的类型数组,数组的顺序为参数的顺序
t
y
p
e
返
回
类
型
的
c
l
a
s
s
,
如
返
回
I
n
t
e
g
e
r
则
type 返回类型的class, 如返回Integer则
type返回类型的class,如返回Integer则type相当于java.lang.Integer.class, 注意其与$r的区别
$class 方法所在的类的class
其中cflow的用法如下:
// 被修改的方法
int fact(int n) {
if (n <= 1)
return n;
else
return n * fact(n - 1);
}
// 修改前的调用
CtMethod cm = fact方法;
cm.useCflow(“fact”);
//此时
c
f
l
o
w
(
f
a
c
t
)
表
示
f
a
c
t
方
法
的
递
归
深
度
,
第
一
次
调
用
是
为
0
c
m
.
i
n
s
e
r
t
B
e
f
o
r
e
(
"
i
f
(
cflow(fact)表示fact方法的递归深度,第一次调用是为0 cm.insertBefore("if (
cflow(fact)表示fact方法的递归深度,第一次调用是为0cm.insertBefore("if(cflow(fact) == 0) {System.out.println(“fact " + $1);}”);
cflow使用场景举例:
应用需要监控方法的执行时间,并找出执行时间长的方法,如果遇到递归调用期望忽略内部递归的记录,只记录最外层的时间,此时可以使用cflow。
最后,顺便提醒javassist也提供了动态代理的接口(javassist.util.proxy.ProxyFactory),但效率非常低,可测试时使用,不建议在生产环境下使用。
1 因为tomcat和jboss使用的是独立的classloader,而Javassist是通过默认的classloader加载类,因此直接对tomcat cont
ext中定义的类做toClass会抛出ClassCastException异常,可以用tomcat的classloader加载字节码。
CtClass cc = ...;
Class c = cc.toClass(bean.getClass().getClassLoader());
2 发现在简单的测试中可以load的类,在tomcat中无法load。这是因为,ClassPool.getDefault()查找的路径和底层的JVM路径。而tomcat中定义了多个classloader,因此额外的class路径需要注册到ClassPool中。
pool.insertClassPath(new ClassClassPath(this.getClass()));
3 我想在运行时修改类的一个方法,但是JVM是不允许动态的reload类定义的。一旦classloader加载了一个class,在运行时就不能重新加载这个class的另一个版本,调用toClass()会抛LinkageError。因此需要绕过这种方式定义全新的class。而toClass()其实是当前thread所在的classloader加载class。
4 Javassist生成的字节码由于没有class声明,字节码创建变量及方法调用都需要通过反射。这点在在线的应用上的性能损失是不能接受的,受到NBeanCopyUtil实现的启发,可以定义一个Interface,Javassist的字节码实现这个Interface,而调用方通过这个接口调用字节码,而不是反射,这样避免了反射调用的开销。还有一点字节码new一个变量也是通过反射,因此通过代理的方法,将每个pv都需要new的字节码对象改为每次new一个代理对象,代理到常驻内存的字节码对象中,这样避免了每次反射的开销。