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( m()相当于m($1, 2... ) , 如 果 m 无 参 数 则 m ( 2...),如果m无参数则m( 2...)mm($)相当于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则 typeclassIntegertype相当于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)fact0cm.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一个代理对象,代理到常驻内存的字节码对象中,这样避免了每次反射的开销。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值