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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350