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参数数组. 相当于new Object[]{$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),但效率非常低,可测试时使用,不建议在生产环境下使用。



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值