gradle学习二 利用javassist api修改class字节码

一 前言

Javassist (Java Programming Assistant) makes Java bytecode manipulation simple. It is a class library for editing bytecodes in Java; it enables Java programs to define a new class at runtime and to modify a class file when the JVM loads it. Unlike other similar bytecode editors, Javassist provides two levels of API: source level and bytecode level. If the users use the source-level API, they can edit a class file without knowledge of the specifications of the Java bytecode. The whole API is designed with only the vocabulary of the Java language. You can even specify inserted bytecode in the form of source text; Javassist compiles it on the fly. On the other hand, the bytecode-level API allows the users to directly edit a class file as other editors.

Javassist 提供了java类库,用于方便操控Java字节码。功能包括:运行时创建java class,修改class。与其他同类工具(asm等)不同的是,Javassist提供了两个层面的API:

1.java代码层

2.字节码层

通过java代码层,开发者即时对字节码不是很熟悉,也可以非常方便快速的完成字节码的修改。

二 示例

定义一个Bean类,代码如下

public class Bean {

    public static void show(String text) {
        System.out.print("hey man " + text);
    }

    public static void main(String[] argv) {
        show("add timing ");
    }
}

直接运行,会打印
hey man add timing

如果想要统计show方法的执行时间,我们可以在写代码的时候直接在show方法体开始和结束的地方加上计时的代码,这样很简单。但是假如我们期望动态修改show方法,直接修改Test.class类字节码,那么可以通过Javassist来完成。具体方法如下:

import java.io.IOException;
import java.lang.reflect.InvocationTargetException;

import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.CtNewMethod;
import javassist.NotFoundException;


public class JassistLearn {

    public static void main(String[] argv) {
        try {
            CtClass clas = ClassPool.getDefault().get("Bean");
            addTiming(clas,"show");
        } catch (NotFoundException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (CannotCompileException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }

    private static void addTiming(CtClass clas, String mname)
        throws NotFoundException, CannotCompileException, IOException {
        /* Retrieves methods with the specified name among the methods
        * declared in the class.  Multiple methods with different parameters
        * may be returned.*/
        CtMethod mold = clas.getDeclaredMethod(mname);
        //修改当前方法名称为一个临时名称
        String nname = mname+"Impl";
        mold.setName(nname);
         /* Creates a copy of a method with a new name.
         * This method is provided for creating
         * a new method based on an existing method.
         * This is a convenience method for calling
         * {@link CtMethod#CtMethod(CtMethod, CtClass, ClassMap) this constructor}.
         * See the description of the constructor for particular behavior of the copying.
         * 拷贝一个新的方法,内容跟当前方法一致,方法名与当前方法原有名称一致。后面主要修改新方法的
         * 方法体的内容。 
         * */
        CtMethod mnew = CtNewMethod.copy(mold, mname, clas, null);
        String type = mold.getReturnType().getName();
        StringBuffer body = new StringBuffer();
        //在新方法体开始处增加一个变量start标识当前执行时间点
        body.append("{\nlong start = System.currentTimeMillis();\n");
        if (!"void".equals(type)) {
            body.append(type + " result = ");
        }
        //原方法运行,格式为方法名+参数,($$)代表参数
        body.append(nname + "($$);\n");
        //在原方法执行后打印当前时间点。统计前后时间差可以算出原方法的执行耗时
        body.append("System.out.println(\"Call to method " + mname +
            " took \" +\n (System.currentTimeMillis()-start) + " +
            "\" ms.\");\n");
        if (!"void".equals(type)) {
            body.append("return result;\n");
        }
        body.append("}");
        //将方法体设置到新方法内  
        mnew.setBody(body.toString());
        //将新方法添加到原方法所在类
        clas.addMethod(mnew);
        //更改原方法所在class文件,刷新到磁盘,完成字节码修改
        clas.writeFile("D:/eclipse/server/JavaSistLearn/bin");
    }
}

代码注释包含了每一步对应的意义,执行过程总结一下主要是:
1. 通过ClassPool找到Bean.class类文件
2. 修改show方法名称为一个临时名称showImpl
3. 拷贝一个新方法,方法名是show,方法体与showImpl方法的内容相同
4. 修改新方法的方法体,包含三部分:方法开始添加start变量标记当前时刻;执行showImpl;计算当前时间与start的差值,算出showImpl的执行耗时
5. 将新方法添加到Bean.calss文件中;同步更新Bean.class文件到磁盘

通过上面五个步骤就完成了Bean.class字节码的修改

执行JassistLearn,打印数据如下:

hey man add timing Call to method show took 0 ms.

通过JD-DUI工具查看Bean class文件,代码如下:

import java.io.PrintStream;

public class Bean
{
  public static void showImpl(String text)
  {
    System.jdField_out_of_type_JavaIoPrintStream.print("hey man " + text);
  }

  public static void main(String[] argv) {
    show("add timing ");
  }

  public static void show(String paramString)
  {
    long l = System.currentTimeMillis();
    showImpl(paramString);
    System.jdField_out_of_type_JavaIoPrintStream.println("Call to method show took " + (System.currentTimeMillis() - l) + " ms.");
  }
}

工程下载地址

三 总结

通过示例看到通过javassist api可以很简便的在运行时修改class字节码,并且不要求使用者对字节码本身非常熟悉。利用这个特性可以实现aop,安卓应用热修复等功能。不过最新github上的javassist.jar在使用过程会报关于StackWalker.StackFrame的错误,需要使用Jdk1.9。

四 参考

Javassist github

Javassist 官网

Javassist api参考

猜你喜欢

转载自blog.csdn.net/rambomatrix/article/details/78602614
今日推荐