阿尔萨斯 原理探究 仿写arthas-TimeTunnel 流量采集

前言

对于TimeTunnel功能块的编写真的可以用心累来形容,总结一句话就是太想当然了!!在开始做这块功能的时候,原本大体的思路就是在watch方法的基础上请求参数进行序列化的操作,但是这个序列化的过程就没有那么容易了!考虑一下下面这种情况:

1
public int doAdd(int x,String s,long l ,Job j,Main n,double d) {}

考虑到我们将使用反射来完成回放功能,那么你需要将当前的类、方法名、方法参数类型、请求参数都保留下来,前面三个是好做的,但是如果你需要保留的参数并没有实现 Serializable 接口,那么该类就不能被序列化,并且就算该类实现了Serializable接口,也不能保证该类成员对象都能被序列化的!!(与深拷贝有点相似,需要考虑到整个对象的引用情况)

于是我换了一种思路,采用FastJson将对象以字符串的形式进行序列化,要回调的时候再通过JSON.parseObject实现对象的转换!!

先看看我的做的TimeTunnel效果图:对正在运行的函数doAdd和doDelet实现方法级别的流量录制.

1
2
3
4
5
6
7
8
9
10
2
1
-------------------
开始采集方法:doAdd

2
开始采集方法:doDelet

1
-------------------

对类、方法、方法参数以元数据存储一份,每一次的请求参数以当前时间轴进行保存:

你想输入的替代文字

实现序列化的两种方式

下面我主要来分享我对请求参数序列化实现的两种方式的一些思考。

为每个涉及到的类进行加强

刚刚在前言提到了,如果一个类包含对另外一个类的引用,那么想要为这个对象进行序列化的同时,也要为涉及到的所有类实现Serializable接口!

这个实现方式的大体思路是:通过广度优先遍历的方式对涉及到的类进行遍历,然后通过ASM为每个类实现Serializable接口。下面的方法为例:

1
public int doAdd(int x,String s,long l ,Job j,Main n,double d){}

方法开始时,将参数入到遍历队列中:【int x , String s , long l , Job j , Main n , double d】,对列表的第一个对象进行出栈,出栈后的对象进入到类加强列表中,因为int x 是一个基本类型数据,所以不需要进行加强,一直到Job j 时,将Job j放入到加强队列中:【Job j】,同时将Job类中引用到的所有类,放入到遍历队列中。

1
2
3
4
5
public class Job  implements Serializable{

public Test test =1;
public String str = "job";
}

现在遍历队列为:【Main n , double d , Test test , String str】

加强队列为:【Job j】

将遍历队列中的所有对象按照上述流程进行遍历,就可以得到需要加强的所有类了,最后通过asm动态的为加强类继承Serializable接口。

这种方式我没有具体的去实现过,主要是考虑到需要对涉及到类进行全部加强算是一个性能比较差的实现,所以在最终实现的时候还是通过FastJson来完成。

通过FastJson将参数以字符串形式进行保留

其实说简单点就是对原来的方法进行加强,当然这个加强是虚拟机层面上的AOP。原来的方法如下:

1
2
3
public int doAdd(int x,String s,long l ,Job j,Main n,double d) {
return n.x + x;
}

需要加强为如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public int doAdd(int x,String s,long l ,Job j,Main n,double d) throws Exception{
String clazz = Thread.currentThread() .getStackTrace()[1].getClassName();
String method = Thread.currentThread() .getStackTrace()[1].getMethodName();
String path = clazz+"@"+method+"#"+System.currentTimeMillis();
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(new File(path),true));
String[] args = new String[6];
args[0] = JSON.toJSONString(x);
args[1] = JSON.toJSONString(s);
args[2] = JSON.toJSONString(l);
args[3] = JSON.toJSONString(j);
args[4] = JSON.toJSONString(n);
args[5] = JSON.toJSONString(d);
out.writeObject(args);

return n.x + x;
}

有几点需要注意的是:因为涉及到IO,所以需要为该方法处理异常的情况,这个通过asm就可以做到:

1
MethodVisitor mv = super.visitMethod(access, name, desc, signature, new String[]{"java/lang/Exception"});

还需要注意的是局部变量所在下标的位置,比如doAdd方法又6个参数,那么你就需要提前计算好所有会被用到的变量对应的下标,比如clazz、method、path都是需要根据当前方法来计算的,是不能写死的,核心的思路基本在visitCode体现了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
@Override
public void visitCode() {
try {
dumpAdvice( "./" + advice.getTargetClass().getName() + "@" + advice.getMethodname());
}catch (Exception e){}

int nextIndex = getNextIndex();

// String clazz = Thread.currentThread() .getStackTrace()[1].getClassName();
int clazzNameIndex = nextIndex++;
mv.visitMethodInsn(INVOKESTATIC, "java/lang/Thread", "currentThread", "()Ljava/lang/Thread;", false);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Thread", "getStackTrace", "()[Ljava/lang/StackTraceElement;", false);
mv.visitInsn(ICONST_1);
mv.visitInsn(AALOAD);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StackTraceElement", "getClassName", "()Ljava/lang/String;", false);
mv.visitVarInsn(ASTORE, clazzNameIndex);

// String method = Thread.currentThread() .getStackTrace()[1].getMethodName();
int methodNameIndex = nextIndex++;
mv.visitMethodInsn(INVOKESTATIC, "java/lang/Thread", "currentThread", "()Ljava/lang/Thread;", false);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Thread", "getStackTrace", "()[Ljava/lang/StackTraceElement;", false);
mv.visitInsn(ICONST_1);
mv.visitInsn(AALOAD);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StackTraceElement", "getMethodName", "()Ljava/lang/String;", false);
mv.visitVarInsn(ASTORE, methodNameIndex);


// String path = clazz+"@"+method+"#"+System.currentTimeMillis();
int pathIndex = nextIndex++;
mv.visitTypeInsn(NEW, "java/lang/StringBuilder");
mv.visitInsn(DUP);
mv.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);
mv.visitVarInsn(ALOAD, clazzNameIndex);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);

mv.visitLdcInsn("@");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);

mv.visitVarInsn(ALOAD, methodNameIndex);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);

mv.visitLdcInsn("#");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);

mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(J)Ljava/lang/StringBuilder;", false);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);
mv.visitVarInsn(ASTORE, pathIndex);

//
// // ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(new File(path),true));
int outFileIndex = nextIndex++;
mv.visitTypeInsn(NEW, "java/io/ObjectOutputStream");
mv.visitInsn(DUP);
mv.visitTypeInsn(NEW, "java/io/FileOutputStream");
mv.visitInsn(DUP);
mv.visitTypeInsn(NEW, "java/io/File");
mv.visitInsn(DUP);
mv.visitVarInsn(ALOAD, pathIndex);
mv.visitMethodInsn(INVOKESPECIAL, "java/io/File", "<init>", "(Ljava/lang/String;)V", false);
mv.visitInsn(ICONST_1);
mv.visitMethodInsn(INVOKESPECIAL, "java/io/FileOutputStream", "<init>", "(Ljava/io/File;Z)V", false);
mv.visitMethodInsn(INVOKESPECIAL, "java/io/ObjectOutputStream", "<init>", "(Ljava/io/OutputStream;)V", false);
mv.visitVarInsn(ASTORE, outFileIndex);


mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("开始采集方法:"+name + "\n");
mv.visitMethodInsn(org.objectweb.asm.Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);

// 计算申请来的数组存放的位置
int objectArrayIndex = nextIndex++;
if(parInputITypes !=null && parInputITypes.size()>0) {


/**申请空间advice保留请求参数的object[]**/

//object[] 数组大小为parInputITypes的大小
mv.visitIntInsn(BIPUSH, parInputITypes.size());
mv.visitTypeInsn(ANEWARRAY, "java/lang/String");
mv.visitVarInsn(ASTORE, objectArrayIndex);


int index = 1;
for (int i = 0; i < parInputITypes.size(); i++) {

IType iType = parInputITypes.get(i);

mv.visitVarInsn(ALOAD, objectArrayIndex);
// object[]的下标
mv.visitInsn(ICONST_0+i);
// 局部变量的下标
mv.visitVarInsn(iType.getLoadOpcode(), index);
// 如果是8种常量类型,则需要调用String.valueOf的方法
if(ASMTypeUtil.isBasicType(iType.classType)) {
String owner = ASMTypeUtil.getZxType(iType.classType);
String descriptor = "(" + iType.desc + ")L" + owner + ";";
mv.visitMethodInsn(INVOKESTATIC, owner, "valueOf", descriptor, false);
}
mv.visitMethodInsn(INVOKESTATIC, "com/alibaba/fastjson/JSON", "toJSONString", "(Ljava/lang/Object;)Ljava/lang/String;", false);
mv.visitInsn(AASTORE);

index += iType.classTypeByteSize;

}
}

mv.visitVarInsn(ALOAD, outFileIndex);
mv.visitVarInsn(ALOAD, objectArrayIndex);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/ObjectOutputStream", "writeObject", "(Ljava/lang/Object;)V", false);

super.visitCode();
}