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对象是可以使用占位符的,如果说想获取当前方法的参数,可以用$$来表示,还有其他的:
符号 | 含义 |
|
|
| 方法参数数组.它的类型为 |
| 所有实参。例如, |
|
|
| 返回结果的类型,用于强制类型转换 |
| 包装器类型,用于强制类型转换 |
| 返回值 |
| 类型为 java.lang.Class 的参数类型数组 |
| 一个 java.lang.Class 对象,表示返回值类型 |
| 一个 java.lang.Class 对象,表示当前正在修改的类 |
| 方法的名称 |
目前大多数常用的操作差不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() {
}
}
这里需要注意的点有以下几个:
- 我们需要复制原来的方法,原方法有多个返回值的时候比较方便处理。调用原来方法的方式是{main$($$);},其中$$表示当前方法的参数。
- 添加try catch的方法就是addCatch方法,catch里面做的事情就是方法参数,这里我是捕获异常之后又throw了出来,捕获的异常使用$e来表示。
一个基本的字节码插桩进行方法监控的demo就是这些了,到时候无非就是把一些位置的代码换成我们的Spy方法,但是其实整个项目其中的一部分核心功能就是这些了。
如果您喜欢,可以点个赞和关注,十分感谢。