• 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

  • 相关阅读:
    在CentOS 8 上 部署 .Net Core 应用程序
    Asp.Net Core 查漏补缺《一》 —— IStartFilter
    接口相关数据日志打印
    退款
    识别身份证
    生成二维码
    打造一款简单易用功能全面的图片上传组件
    Redis过期策略+缓存淘汰策略
    Redis主从复制
    .net 平台常用框架
  • 原文地址:https://www.cnblogs.com/windliu/p/6043878.html
Copyright © 2020-2023  润新知