Javassist 字节码插桩

Javassist基础

Javassist 使您可以 检查、编辑以及创建Java 二进制类。Javassist 使用javassist.ClassPool 类跟踪和控制所操作的类。这个类的工作方式是与JVM的 ClassLoader非常相似,但是有一个重要的区别是它不是将装载的、要执行的类作为应用程序的一部分连接,ClassPool使所装载的类可以通过 Javassist API 作为数据使用。可以使用默认的 ClassPool,获取方式:ClassPool.default(),它是从JVM搜索路径中装载的,也可以定义一个搜索您自己路径列表的 ClassPool,甚至可以直接从 字节数组 中装载二进制类,以及从头开始创建新类。

装载到 ClassPool 的类由 javassist.CtClass 实例表示。与标准的Java java.lang.Class类一样,CtClass提供了检查类数据的方法,它还定义了在类中添加新字段、方法和构造函数、以及改变类、父类和接口的方法。但是 Javassist 没有提供删除一个类的字段、方法或者构造函数的方法。

字段、方法和构造函数分别由 javassist.CtFieldjavassist.CtMethodjavassist.CtConstructor 的实例表示。这些类定义了修改由它们所表示的对象的所有的方法的方法,包括方法或者构造函数中实际字节码内容。

所有字节码的源代码,Javassist 让你可以完全替换一个方法或构造函数的字节码正文,或者在正文的开始或者结束位置选择性的添加字节码。Javassist 方法将您提供的源代码高效地编译为Java字节码,然后将它插入到目标方法或者构造方法的正文中。

Javassist 接受的源代码 与 Java 语言的并不完全一致,不过主要区别只是增加了一些特殊的标识符,用于表示方法或者构造函数参数方法返回值 和其他在插入的代码中可能用到的内容。

对于在传递给 Javassist 的源代码中可以做的事情有一些限制。第一项限制是使用的格式,它必须是 单条语句或者。在大多数情况下这算不上是限制。下面是一个使用特殊Javassist标识符中表示前两个参数的例子:

System.out.println("arg1: " + $1);
System.out.println("arg1: " + $2);

对于源代码的一项更实质性的限制是:不能引用在所添加的声明或者块外声明的局部变量。这意味着,在方法开始和结尾处都添加了代码,那么一般不能将在开始处添加的代码中的信息传递给结尾处添加的代码。

实践:测量执行一个方法所花费的时间

这在源代码中可以很容易的完成,只要在方法开始时记录当前时间、之后在方法结束时再次检查当前时间并计算两个值的差。如果没有源代码,那么这种计时信息就要困难的多。

我们定一个类 StrBuilder

package com.test.monitorMethodInvokeTime;
public class StrBuilder {

    public String buildString(int length){
        String result = "";
        for (int i = 0; i < length; i++){
            result += (char) (i%26 + 'a');
        }
        return result;
    }

}

接下来,监测 buildString 方法的执行所花费时间。

思路一:
//使用默认的ClassPoll,默认从JVM加载class路径加载,本项目使用IntelliJ,编译以后的文件在 out/production/路径下
CtClass clas = ClassPool.getDefault().get("com.test.monitorMethodInvokeTime.StrBuilder");
CtMethod ctMethod = clas.getDeclaredMethod("buildString");

/**
  构造body
*/
StringBuilder builder = new StringBuilder();
builder.append("{");
builder.append("long start = System.currentTimeMillis(); \n");
builder.append("String result = \"\"; \n");
builder.append(" for (int i = 0; i < $1; i++){  \n");
builder.append("  result += (char) (i%26 + 'a'); \n");
builder.append(" } \n");
builder.append("long end = System.currentTimeMillis();\n");
builder.append("System.out.println(\"耗时:\" + (end - start) + \"ms\");\n");
builder.append(" return result; \n");
builder.append("}");

ctMethod.setBody(builder.toString());

//写回文件。我是用的 IntelliJ IDEA
clas.writeFile("/Users/jxf/workspace/Java/project/JavassistManual/out/production/JavassistManual/");

经过这一段代码之后,我们来看一下生成的 class字节码文件:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package com.test.monitorMethodInvokeTime;

public class StrBuilder {
    public StrBuilder() {
    }

    public String buildString(int var1) {
        long var2 = System.currentTimeMillis();
        String var4 = "";

        for(int var5 = 0; var5 < var1; ++var5) {
            var4 = String.valueOf(var4).concat(String.valueOf((char)(var5 % 26 + 97)));
        }

        long var6 = System.currentTimeMillis();
        System.out.println("耗时:" + (var6 - var2) + "ms");
        return var4;
    }
}

查看生成的class字节码,已经实现的方法执行时间的监控。

思路二:

使用方法代理来处理:

CtClass clas = ClassPool.getDefault().get("com.test.monitorMethodInvokeTime.StrBuilder");
String method = "buildString"
CtMethod ctMethod = clas.getDeclaredMethod(method);

//我们把实际方法进行一个重命名:buildString$impl
String nName = method + "$impl";
mOld.setName(nName);

//从 buildString$impl 复制出一个新的方法,新的方法名字叫:buildString
CtMethod mNew = CtNewMethod.copy(mOld, method, clas, null);

/**
构建新方法的方法体
*/
String returnType = mOld.getReturnType().getName();
StringBuffer body = new StringBuffer();
body.append("{");
body.append("long start = System.currentTimeMillis(); \n");
if (!"void".equals(returnType)){
    body.append(returnType + " result = ");
}
body.append(nName + "($$);\n");  //$$ 代表把此方法的所有参数,统统传入下一个方法中。

body.append("long end = System.currentTimeMillis();\n");
body.append("System.out.println(\"耗时:\" + (end - start) + \"ms\");\n");

if(!"void".equals(returnType)){
    body.append("return result;\n");
}
body.append("}");
mNew.setBody(body.toString());

//把生成的方法添加到classs
clas.addMethod(mNew);

//写回文件。我是用的 IntelliJ IDEA
clas.writeFile("/Users/jxf/workspace/Java/project/JavassistManual/out/production/JavassistManual/");

我们来看一下,生成的字节码:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package com.test.monitorMethodInvokeTime;

public class StrBuilder {
    public StrBuilder() {
    }

    public String buildString$impl(int length) {
        String result = "";

        for(int i = 0; i < length; ++i) {
            result = result + (char)(i % 26 + 97);
        }

        return result;
    }

    public String buildString(int var1) {
        long var2 = System.currentTimeMillis();
        String var4 = this.buildString$impl(var1);
        long var5 = System.currentTimeMillis();
        System.out.println("耗时:" + (var5 - var2) + "ms");
        return var4;
    }
}

实践:创建一个全新的Class

假如创建一个如下的类:

package com.test;
public class Person {
    private String name = "Jack";
    public void setName(String var1) {
        this.name = var1;
    }
    public String getName() {
        return this.name;
    }
    public Person() {
        System.out.println(" constructor ");
    }
    public Person(String var1) {
        this.name = var1;
    }
    public void printName() {
        System.out.println(this.name);
    }
}

看一下 Javassist 的代码:

        ClassPool pool =  ClassPool.getDefault();

        //1、创建一个空类
        CtClass cc = pool.makeClass("com.test.Person");

        //2、增加一个字段 private String name;
        CtField ctField = new CtField(pool.get("java.lang.String"), "name", cc);
        //访问级别 private
        ctField.setModifiers(Modifier.PRIVATE);
        // Person 添加 name属性,并设置初始值:Jack
        cc.addField(ctField, CtField.Initializer.constant("Jack"));

        //3、生成 getter、setter 方法
        cc.addMethod(CtNewMethod.setter("setName", ctField));
        cc.addMethod(CtNewMethod.getter("getName", ctField));

        //4、添加无参构造函数
        CtConstructor cons = new CtConstructor(new CtClass[]{}, cc);
        cons.setBody("{System.out.println(\" constructor \");}");
        cc.addConstructor(cons);

        //5、添加有参构造函数
        cons = new CtConstructor(new CtClass[]{pool.get("java.lang.String")}, cc);
        // $0 == this $1、$2、$3 代表参数
        cons.setBody("{$0.name = $1;}");
        cc.addConstructor(cons);

        //6、创建一个名为 printName 方法
        CtMethod ctMethod = new CtMethod(CtClass.voidType, "printName", new CtClass[]{}, cc);
        ctMethod.setModifiers(Modifier.PUBLIC);
        ctMethod.setBody("{ System.out.println(name);}");
        cc.addMethod(ctMethod);

        //将创建的 CtClass 输出到 文件
        cc.writeFile("/Users/jxf/workspace/Java/project/JavassistManual/out/production/JavassistManual/");
        //也可直接在内存中生成Class类,直接使用
        Class cls = cc.toClass();

至此,对于Javassist的常用做了一个实验,其中还有一些其它的用法,就需要在日常使用中慢慢实践。

感谢:
https://www.ibm.com/developerworks/cn/java/j-dyn0429/index.html?ca=drs-
https://www.cnblogs.com/rickiyang/p/11336268.html

你可能感兴趣的:(Javassist 字节码插桩)