collie

使用Java实现一个分布式调用链追踪系统


初识javassist

Javassist是一个开源的分析、编辑和创建Java字节码的类库。使用Javassist可以对java字节码进行修改,和Java的反射类似,但是比反射更强大。对比同样的Java字节码修改工具ASM,Javassist更加方便简介,但是性能上稍微差了一点。

写一个helloworld

我们的目标是用javassit生成一个类,可以输出“hello world”,我们的目标生成类是:

package com.github.blocks.bytecode.javassit;

public class HelloWorld {
    public static void main(String[] var0) {
        System.out.println("hello world");
    }

    public HelloWorld() {
    }
}

用javassit生成的代码如下:

public class JavassitHelloWorld {
    public static void main(String[] args) throws Exception{
        // 最核心的对象,可以创建对象
        ClassPool cp = ClassPool.getDefault();
        // 创建类,类的全类名
        CtClass ctClass = cp.makeClass("com.github.blocks.bytecode.javassit.HelloWorld");
		// 创建一个方法,返回值类型,方法名,参数类型,属于哪个类
        CtMethod main =
                new CtMethod(CtClass.voidType, "main", new CtClass[] {cp.get(String[].class.getName())}, ctClass);
        main.setModifiers(Modifier.PUBLIC+Modifier.STATIC);
        // 函数体
        main.setBody("{System.out.println(\"hello world\");}");
        // 把方法添加到类里
        ctClass.addMethod(main);

        // 创建无参数构造方法
        CtConstructor ctConstructor = new CtConstructor(new CtClass[]{}, ctClass);
        ctConstructor.setBody("{}");
        ctClass.addConstructor(ctConstructor);

        // 输出类内容,可以加上路径,会输出class文件
        ctClass.writeFile("/Users/dongzhonghua03/Documents/github/spring-cloud-blocks/byte-code-exp/src/main/java");

        // 测试调用
        Class<?> clazz = ctClass.toClass();
        Object obj = clazz.newInstance();
        Method m = clazz.getDeclaredMethod("main", String[].class);
        m.invoke(obj, (Object)new String[]{});
    }
}

javassist常见的用法

javassist中常用的类:

ClassPool可以理解为一个java类池,里面有一个Hashtable存放着所有创建的类和加载过得类,加载的类用到的才会存放到这个Hashttable里面。

CtClass表示一个java类封装的对象

CtMethod表示一个方法

CtField表是一个属性

CtConstructor表示构造方法

javassist的api还是比较方便使用的,如果创建一个类如上一个例子所示classPool.makeClass(“name”);

最后输出的话就是ctClass.writeFile()。也可以输出到bytecode用自定义的类加载器加载。

CtMethod 和 CtConstructor 提供了 insertBefore(),insertAfter() 和 addCatch() 方法。 它们可以将用 Java 编写的代码片段插入到现有方法中。也可以按行插入,使用insertAt()。

insertBefore() ,insertAfter() ,addCatch() 和 insertAt() 中的String对象是可以使用占位符的,如果说想获取当前方法的参数,可以用$$来表示,还有其他的:

符号

含义

$0, $1, $2, …

this and 方法的参数

$args

方法参数数组.它的类型为 Object[]

$$

所有实参。例如, m($$) 等价于 m($1,$2,)

$cflow()

cflow 变量

$r

返回结果的类型,用于强制类型转换

$w

包装器类型,用于强制类型转换

$_

返回值

$sig

类型为 java.lang.Class 的参数类型数组

$type

一个 java.lang.Class 对象,表示返回值类型

$class

一个 java.lang.Class 对象,表示当前正在修改的类

$process

方法的名称

目前大多数常用的操作差不1多就是这些了,其他详细的教程参考文章最后的参考文档。

使用javassist进行字节码插桩的一个demo

我们要进行字节码插桩进行方法监控和调用链追踪,其实就是在运行的时候修改字节码,把我们想调用的方法插入到字节码里面去,也就是在方法开始结束和抛异常的时候调用我们的方法,也就是我们上一节中定义的Spy方法,至于具体的逻辑则需要到Spy方法中进行,如果发送到消息队列,或者存到数据库等操作。

下面我们简单写一个demo,把我们需要做的事情用几十行代码来讲清楚:

public static void main(String[] args) throws Exception {
    ClassPool cp = ClassPool.getDefault();
    // 创建类,类的全类名
    CtClass ctClass = cp.makeClass("com.github.blocks.bytecode.javassit.HelloWorld");
    CtMethod main =
            new CtMethod(CtClass.voidType, "main", new CtClass[] {cp.get(String[].class.getName())}, ctClass);
    main.setModifiers(Modifier.PUBLIC + Modifier.STATIC);
    main.setBody("{System.out.println(\"hello world\");}");
    ctClass.addMethod(main);
    // 需要把原方法复制一个出来,因为原方法可能有多个返回值,所以我们想存返回值的时候比较麻烦,复制一个方法,在原方法调用这个方法,则比较好的解决了这个问题。
    CtMethod copyMain = CtNewMethod.copy(main, ctClass, null);
    copyMain.setName(main.getName() + "$");
    ctClass.addMethod(copyMain);
    System.out.println(main.getSignature());
    main.setBody("{main$($$);}");
    main.addCatch("{ throw $e;}", cp.get(IOException.class.getName()));
    main.addLocalVariable("start", cp.get(long.class.getName()));
    main.insertBefore("{  start = System.currentTimeMillis();}");
    main.insertAfter("{System.out.println(System.currentTimeMillis() - start);}");
    // 创建无参数构造方法
    CtConstructor ctConstructor = new CtConstructor(new CtClass[] {}, ctClass);
    ctConstructor.setBody("{}");
    ctClass.addConstructor(ctConstructor);
    // 输出类内容,可以加上路径,会输出class文件
    ctClass.writeFile("xx");
    // 测试调用
    Class<?> clazz = ctClass.toClass();
    Object obj = clazz.newInstance();
    Method m = clazz.getDeclaredMethod("main", String[].class);
    m.invoke(obj, (Object) new String[] {});

}

最后修改后的字节码反编译后的类变成了这样:

public class HelloWorld {
    public static void main(String[] var0) {
        long start = System.currentTimeMillis();

        try {
            main$(var0);
        } catch (IOException var6) {
            throw var6;
        }

        Object var5 = null;
        System.out.println(System.currentTimeMillis() - start);
    }

    public static void main$(String[] var0) {
        System.out.println("hello world");
    }

    public HelloWorld() {
    }
}

这里需要注意的点有以下几个:

  1. 我们需要复制原来的方法,原方法有多个返回值的时候比较方便处理。调用原来方法的方式是{main$($$);},其中$$表示当前方法的参数。
  2. 添加try catch的方法就是addCatch方法,catch里面做的事情就是方法参数,这里我是捕获异常之后又throw了出来,捕获的异常使用$e来表示。

一个基本的字节码插桩进行方法监控的demo就是这些了,到时候无非就是把一些位置的代码换成我们的Spy方法,但是其实整个项目其中的一部分核心功能就是这些了。

如果您喜欢,可以点个赞和关注,十分感谢。