javassist:字节码编辑器工具

简介:

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是反射实现的。当然,他们的效率不会有太大差异,各种缓存策略也保证了他们的执行效率;

下面是一些基础知识

If a CtClass object is converted into a class file by writeFile(), toClass(), or toBytecode(), Javassist freezes that CtClass object. Further modifications 
of that CtClass object are not permitted

A frozen CtClass can be defrost so that modifications of the class definition will be permitted

CtClasss 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

原文地址:https://www.cnblogs.com/windliu/p/6043878.html