一、什么是Bistoury
去哪儿开源的应用诊断工具
详细可以看看这篇:
去哪儿一站式 Java 应用诊断解决方案 - Bistoury
二、快速开始
启动需要被debug的应用
java -jar web-quick-practice-1.0-SNAPSHOT.jar
启动bistoury
./quick_start.sh -p <pid> -i 127.0.0.1 start
然后就可以在localhost:9091访问,启动的东西比较多,所以还是要等下的。账号密码默认admin
三、Bistoury在线debug效果
选择应用
选择需要debug的类
如果没有配置代码仓库的地址,那就会展示Fernflower反编译后的代码
打断点,只能打1个断点,打多个需要先移除之前的。前端有校验行号对不对。
每隔几秒,会自动发请求获取断点的执行结果。触发成功后自动移除断点集合中的断点
四、Bistoury在线debug具体实现
行增强,增强了什么?
如果源代码是下面这样的话,我们在第四行添加断点
@GetMapping("/hello")
public String hello() {
String str = "hello";
return str; // 在这里添加断点
}
代码就会被增强为这样。增强的逻辑大概是,判断这个断点是否存在 --> 存储局部变量 --> 命中断点(这里会将断点从集合中移除)--> 之后把局部变量、静态变量、全局变量、调用栈存到DefaultSnapshotStore中(后续QDebugSearchCommand,会去查这些debug数据)
@GetMapping({
"/hello"})
public String hello() {
String str = "hello";
if (BistourySpys1.hasBreakpointSet("org/example/HelloContoller.java", 4)) {
BistourySpys1.putLocalVariable("this", this);
BistourySpys1.putLocalVariable("str", str);
if (BistourySpys1.isHit("org/example/HelloContoller.java", 4)) {
BistourySpys1.dump("org/example/HelloContoller.java", 4);
BistourySpys1.fillStacktrace("org/example/HelloContoller.java", 4, new Throwable());
BistourySpys1.endReceive("org/example/HelloContoller.java", 4);
}
}
return str;
}
怎么保证反编译后的行号和.class记录的行号一样?
传给前端的时候,会带上行号之间的映射,前端的行号校验也是根据这个映射来的
//
// Source code recreated from a .class file by Bistoury
// (powered by Fernflower decompiler)
//
package org.example;
import java.lang.Exception;
import java.lang.Integer;
import java.lang.RuntimeException;
import java.lang.String;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import org.example.HelloContoller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloContoller {
@GetMapping({
"/hello"})
public String hello() {
String str = "hello";
return str;
}
@GetMapping({
"/hasError"})
public String hasError() {
String str = "hello";
int i = 1 / 0;
str = str + i;
return str;
}
@GetMapping({
"/throwE"})
public String throwE() {
String str = "hello";
try {
if ("hello".equals(str)) {
throw new RuntimeException("哈哈哈哈");
} else {
return str;
}
} catch (Exception var3) {
throw var3;
}
}
@GetMapping({
"/stream"})
public String stream() {
Integer a = 111;
Integer b = 222;
List<Integer> list = Arrays.asList(a, b, 5);
List<Integer> numberList = (List)list.stream().filter((number) -> {
return number < 100;
}).collect(Collectors.toList());
return numberList.toString();
}
@GetMapping({
"/streams"})
public String streams() {
Integer a = 111;
Integer b = 222;
List<Integer> list = Arrays.asList(a, b, 5);
List<Integer> res = (List)list.stream().map((num) -> {
num = num + 100;
return num;
}).collect(Collectors.toList());
return res.toString();
}
}
Lines mapping:
17 <-> 23
19 <-> 24
25 <-> 29
27 <-> 30
29 <-> 31
30 <-> 32
35 <-> 37
38 <-> 40
39 <-> 41
41 <-> 45
42 <-> 46
44 <-> 43
49 <-> 52
50 <-> 53
51 <-> 54
52 <-> 54
54 <-> 54
55 <-> 55
56 <-> 55
57 <-> 57
59 <-> 58
65 <-> 63
66 <-> 64
67 <-> 65
68 <-> 65
70 <-> 65
72 <-> 66
73 <-> 67
74 <-> 68
75 <-> 69
77 <-> 70
五、行debug原理
行增强
怎么找到要增强的行?怎么样保证我们增强的行号是正确的?
我们发现,源代码里的行号和.class中的行号是一样的,如下图所示
还有一个问题,假如我在某一行增强了这些debug逻辑,那.class的行号是不是就发生了变化?如果我要增强别的行,我是不是要先还原之前的增强逻辑,保证行号的正确🤔?
不用的,增强某一行可以不添加行号,大概的效果就是这样,可以看到,bistoury虽然增强了17行,但是这一大坨代码都算17行
相当于这样的代码
@GetMapping({
"/hello"})
public String hello() {
if (BistourySpys1.hasBreakpointSet("org/example/HelloContoller.java", 17)) {
BistourySpys1.putLocalVariable("this", this);if (BistourySpys1.isHit("org/example/HelloContoller.java", 17)) {
BistourySpys1.dump("org/example/HelloContoller.java", 17);BistourySpys1.fillStacktrace("org/example/HelloContoller.java", 17, new Throwable());BistourySpys1.endReceive("org/example/HelloContoller.java", 17);}}String str = "hello"; //17行
// 18行
return str; //19行
}
在asm中,我们可以通过MethodVisitor的visitLineNumber方法来获取行号
public void visitLineNumber(int line, Label start) {
获取指定行号前的所有局部变量
以我们的hello方法举例子
@GetMapping("/hello")
public String hello() {
String str = "hello"; // 17行
// 18行
return str; // 19行
}
执行javap -c -l HelloContoller.class之后,我们可以看到局部变量表(LocalVariableTable),这里我们需要关注的是Start、Length和Name这三个字段。
Start: 表示局部变量在字节码中开始可见的位置,这个值是一个字节码偏移量,从方法开始计算。
Length: 表示局部变量在字节码中可见的范围长度。
Name:表示局部变量的名称。
public java.lang.String hello();
Code:
0: ldc #2 // String hello
2: astore_1
3: aload_1
4: areturn
LineNumberTable:
line 17: 0
line 19: 3
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lorg/example/HelloContoller;
3 2 1 str Ljava/lang/String;
我们可以看到,我们java中的str字段偏移量为3变为可用,也是对应了代码中的第19行,有效长度是2(为什么长度是2?这个length是字节长度),下面是这个指令的占字节大小。我们可以看到 aload_1 和 areturn 的长度就是2
0: ldc #2 // 这个指令实际上占两个字节:一个字节是操作码,一个字节是常量池索引
2: astore_1 // 占一个字节
3: aload_1 // 占一个字节
4: areturn // 占一个字节
上面的东西了解就可以了,在ASM中的MethodVisitor类有一个visitLocalVariable方法,我们可以很轻松的获取到局部变量的开始和结束行号
public void visitLocalVariable(String name, String descriptor, String signature, Label start, Label end, int index) {
结尾
Bistoury是一个很优秀的debug工具,可惜社区已经不活跃了,但是它实现debug的思路还是很值得我们学习的。后续我阅读源码有什么新的理解,还会继续在这补充。😄