【Bistoury】Bistoury功能分析-在线debug

简介: Bistoury是由去哪儿网开源的一款应用诊断工具,适用于Java应用的在线调试。通过增强字节码,Bistoury能够在不停止应用的情况下设置断点并获取执行信息。启动被调试应用后,使用`quick_start.sh`命令启动Bistoury,并通过浏览器访问`localhost:9091`进行调试。默认账号密码为admin。Bistoury通过ASM字节码增强技术确保行号一致性,并利用行增强技术收集局部变量及调用栈信息。尽管社区已不活跃,但其设计理念仍具参考价值。

一、什么是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效果

选择应用
image.png
选择需要debug的类
image.png
如果没有配置代码仓库的地址,那就会展示Fernflower反编译后的代码
image.png
打断点,只能打1个断点,打多个需要先移除之前的。前端有校验行号对不对。
image.png
每隔几秒,会自动发请求获取断点的执行结果。触发成功后自动移除断点集合中的断点
image.png

四、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中的行号是一样的,如下图所示
image.png

还有一个问题,假如我在某一行增强了这些debug逻辑,那.class的行号是不是就发生了变化?如果我要增强别的行,我是不是要先还原之前的增强逻辑,保证行号的正确🤔?
不用的,增强某一行可以不添加行号,大概的效果就是这样,可以看到,bistoury虽然增强了17行,但是这一大坨代码都算17行
image.png

相当于这样的代码

@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的思路还是很值得我们学习的。后续我阅读源码有什么新的理解,还会继续在这补充。😄

目录
相关文章
|
6月前
|
存储 缓存 安全
【原理】【Java并发】【volatile】适合初学者体质的volatile原理
欢迎来到我的技术博客!我是一名热爱编程的开发者,梦想是写出高端的CRUD应用。2025年,我正在沉淀自己,博客更新速度也在加快。在这里,我会分享关于Java并发编程的深入理解,尤其是volatile关键字的底层原理。 本文将带你深入了解Java内存模型(JMM),解释volatile如何通过内存屏障和缓存一致性协议确保可见性和有序性,同时探讨其局限性及优化方案。欢迎订阅专栏《在2B工作中寻求并发是否搞错了什么》,一起探索并发编程的奥秘! 关注我,点赞、收藏、评论,跟上更新节奏,让我们共同进步!
313 8
【原理】【Java并发】【volatile】适合初学者体质的volatile原理
|
6月前
|
Java 调度
【源码】【Java并发】【线程池】邀请您从0-1阅读ThreadPoolExecutor源码
当我们创建一个`ThreadPoolExecutor`的时候,你是否会好奇🤔,它到底发生了什么?比如:我传的拒绝策略、线程工厂是啥时候被使用的? 核心线程数是个啥?最大线程数和它又有什么关系?线程池,它是怎么调度,我们传入的线程?...不要着急,小手手点上关注、点赞、收藏。主播马上从源码的角度带你们探索神秘线程池的世界...
283 0
【源码】【Java并发】【线程池】邀请您从0-1阅读ThreadPoolExecutor源码
|
4月前
|
关系型数据库 Java MySQL
【设计模式】【结构型模式】桥接模式(Bridge)
一、入门 什么是桥接模式? 桥接模式(Bridge Pattern)是一种结构型设计模式,核心思想是将抽象与实现分离,让它们可以独立变化。简单来说,它像一座“桥”连接了两个维度的变化,避免用继承导致代
308 10
|
5月前
|
存储 Java
【源码】【Java并发】【ThreadLocal】适合中学者体质的ThreadLocal源码阅读
前言 下面,跟上主播的节奏,马上开始ThreadLocal源码的阅读( ̄▽ ̄)" 内部结构 如下图所示,我们可以知道,每个线程,都有自己的threadLocals字段,指向ThreadLocalMap
459 81
【源码】【Java并发】【ThreadLocal】适合中学者体质的ThreadLocal源码阅读
|
7月前
|
存储 监控 Java
【Java并发】【线程池】带你从0-1入门线程池
欢迎来到我的技术博客!我是一名热爱编程的开发者,梦想是编写高端CRUD应用。2025年我正在沉淀中,博客更新速度加快,期待与你一起成长。 线程池是一种复用线程资源的机制,通过预先创建一定数量的线程并管理其生命周期,避免频繁创建/销毁线程带来的性能开销。它解决了线程创建成本高、资源耗尽风险、响应速度慢和任务执行缺乏管理等问题。
354 60
【Java并发】【线程池】带你从0-1入门线程池
|
6月前
|
设计模式 存储 SQL
【Java并发】【volatile】适合初学者体质的volatile
当你阅读dalao的框架源码的时候,你是否会见到这样一个关键字 - - - volatie,诶,你是否会好奇,为什么要加它?加了它有什么作用?
193 14
【Java并发】【volatile】适合初学者体质的volatile
|
5月前
|
Java
【源码】【Java并发】【ReentrantLock】适合中学者体质的ReentrantLock源码阅读
因为本文说的是ReentrantLock源码,因此会默认,大家对AQS有基本的了解(比如同步队列、条件队列大概> 长啥样?)。 不懂AQS的小朋友们,你们好呀!也欢迎先看看这篇
123 13
【源码】【Java并发】【ReentrantLock】适合中学者体质的ReentrantLock源码阅读
|
5月前
|
存储 缓存 安全
【Java并发】【ThreadLocal】适合初学体质的ThreadLocal
ThreadLocal 是 Java 中用于实现线程本地存储(Thread-Local Storage)的核心类,它允许每个线程拥有自己独立的变量副本,从而在多线程环境中实现线程隔离,避免共享变量带来的线程安全问题。
118 9
【Java并发】【ThreadLocal】适合初学体质的ThreadLocal
|
5月前
|
监控 Java API
【Java并发】【ReentrantLock】适合初学体质的ReentrantLock入门
前言 什么是ReentrantLock? ReentrantLock 是 Java 并发包 (java.util.concurrent.locks) 中的一个类,它实现了 Lock 接口,提供了与
231 10
【Java并发】【ReentrantLock】适合初学体质的ReentrantLock入门