javassist使用中遇到的问题记录

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 参数数组. 相当于newObject[]{$1,$2,....},其中的基本类型会被转为包装类型
$$ 所有的参数,如m($$)相当于m($1,$2...),如果m无参数则m($$)相当于m()
$cflow(...) 表示一个指定的递归调用的深度
$r 用于类型装换,表示返回值的类型.
$w 将基础类型转换为一个包装类型.如Integer a=($w)5;表示将5转换为Integer。如果不是基本类型则什么都不做。
$_

返回值,如果方法为void,则返回值为null; 值在方法返回前获得,

如果希望发生异常是有返回值(默认值,如nul),需要将insertAfter方法的第二个参数asFinally设置为true

$sig 方法参数的类型数组,数组的顺序为参数的顺序
$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");

//此时$cflow(fact)表示fact方法的递归深度,第一次调用是为0
cm.insertBefore("if ($cflow(fact) == 0) {System.out.println(\"fact \" + $1);}");

cflow使用场景举例:

应用需要监控方法的执行时间,并找出执行时间长的方法,如果遇到递归调用期望忽略内部递归的记录,只记录最外层的时间,此时可以使用cflow。

最后,顺便提醒javassist也提供了动态代理的接口(javassist.util.proxy.ProxyFactory),但效率非常低,可测试时使用,不建议在生产环境下使用。

猜你喜欢

转载自youaremoon.iteye.com/blog/2279785