zl程序教程

您现在的位置是:首页 >  Java

当前栏目

动态追踪技术之BTrace

2023-02-18 16:46:04 时间

BTrace是什么

BTrace 是一个开源项目。旨在为 java 提供安全可靠的动态跟踪分析工具。Btrace 基于动态字节码修改技术 (Hotswap) 来实现运行时 java 程序的跟踪和替换。Btrace的脚本是用纯java 编写的,基于一套官方提供的 annotation,使跟踪逻辑实现起来异常简单。

BTrace安装

下载链接:https://github.com/btraceio/btrace/releases , 目前最新版本为2.2.2

配置BTRACE_HOME环境变量

Path添加%BTRACE_HOME%\bin

cmd命令行输入btrace --version,出现以下界面表示成功

BTrace 注解

BTrace 注解指定应该将工具放置在何处,以及应该向追踪操作提供哪些数据。BTrace 注解可以分为3类:类注解、方法注解和参数注解。

类注解

@Btrace:表示这个类是一个BTrace脚本

方法注解

  1. @OnMethod:用于指定跟踪方法到目标类,目标方法和目标位置
  2. @OnTimer:用于指定跟踪操作定时执行。
  3. @OnError:当 trace的代码抛异常或者错误时,该注解的方法会被执行,如果同一个trace脚本中其他方法抛异常,该注解方法也会被执行。
  4. @OnEvent:用于将跟踪方法与BTrace客户端发送的“外部”事件关联起来。
  5. OnExit:用于指定BTrace代码调用"exit(int)"内置函数以完成跟踪"session"时运行的操作。
  6. @OnLowMemory:用于跟踪超过内存阈值事件
  7. @OnProbe:用于指定以避免在BTrace脚本中使用实现内部类
  8. @Sampled:为带注释的处理程序启用采样。与@OnMethod注释一起使用

参数注解

  1. @ProbeClassName:用于标记处理方法的参数,仅用户@OnMethod,该参数的值就是被跟踪的类名称
  2. @ProbeMethodName:用于表姐处理方法的参数,仅用户 @OnMethod,该参数值是被跟踪方法名称
  3. @Self:当前截取方法的封闭实例参数
  4. @Return:当前截取方法的的返回值,只对location=@Location(Kind.RETURN)生效
  5. @Duration:当前截取方法的执行时间
  6. @TargetInstance:当前截取方法内部调用的实例
  7. @TargetMethodOrField:当前截取方法内部被调用的方法名

BTrace启动方式

上篇文章介绍Java Agent的时候说过,它有两种加载方式:静态加载和动态加载。Java Agent又是BTrace底层技术之一,所以BTrace也有类似于Java Agent的加载方式的启动方式:动态启动方式和静态启动方式。

动态启动方式

动态启动方式用于快速附加到已经运行的应用程序、获取感兴趣的数据和分离、删除任何跟踪代码。

语法:btrace [-p <port>] [-cp <classpath>] <pid> <btrace-script> [<args>]

  • 「port」是 BTrace 代理监听的端口,可选参数。classpath 是 BTrace 在编译期间搜索类的目录和 jar文件的集合。默认为“.”
  • 「pid」是被追踪的 Java 程序的进程ID
  • 「btrace-script」是btrace脚本程序。

静态启动方式

静态启动方式和Java Agent静态加载方式一样,在这种模式下,BTrace 甚至在应用程序启动代码运行之前就已启动。这使我们有机会追踪在应用程序生命周期的早期执行的代码。

语法:java -javaagent:btrace-agent.jar=[<agent-arg>[,<agent-arg>]*]? <launch-args>

agent-arg参数之间采用逗号进行分隔:

  • 「noServer」 - 不启动socket套接字服务器
  • 「bootClassPath」 - 要使用的引导类路径
  • 「systemClassPath」 - 要使用的系统类路径
  • 「debug」 - boolean类型(true/false),是否打开详细的调试消息
  • 「trusted」- boolean类型(true/false),是否检查 btrace 限制违规
  • 「dumpClasses」 - boolean类型(true/false),是否将转换后的字节码转储到文件中
  • 「dumpDir」 - 指定转换后的类将转储到的文件夹
  • 「stdout」 - boolean类型(true/false),是否将 btrace 输出重定向到 stdout,而不是将其写入任意文件
  • 「probeDescPath」 - 搜索探测描述符 XML 的路径
  • 「startupRetransform」 - boolean类型(true/false),是否在附加时启用所有已加载类的重新转换
  • 「scriptdir」 - 包含要在代理启动时运行的脚本的目录的路径
  • 「scriptOutputFile」 - btrace脚本运行结果将要存储的路径
  • 「script」 - 在代理启动时运行的追踪脚本,脚本之间使用冒号进行分隔

要运行的脚本必须已经被btracec编译为字节码(一个*.class*文件)。

实战测试

这里只测试动态运行方式。

  1. 新建一个maven项目,引入BTrace的包

解压后会有个libs文件夹,BTrace的包可以引入本地的

<dependency>
    <groupId>org.openjdk.btrace</groupId>
    <artifactId>btrace-agent</artifactId>
    <version>${btrace.version}</version>
    <scope>system</scope>
    <systemPath>D:\software\btrace-v2.2.1-bin\libs\btrace-agent.jar</systemPath>
</dependency>

<dependency>
    <groupId>org.openjdk.btrace</groupId>
    <artifactId>btrace-boot</artifactId>
    <version>${btrace.version}</version>
    <scope>system</scope>
    <systemPath>D:\software\btrace-v2.2.1-bin\libs\btrace-boot.jar</systemPath>
</dependency>

<dependency>
    <groupId>org.openjdk.btrace</groupId>
    <artifactId>btrace-client</artifactId>
    <version>${btrace.version}</version>
    <scope>system</scope>
    <systemPath>D:\software\btrace-v2.2.1-bin\libs\btrace-client.jar</systemPath>
</dependency>
  1. 新建一个MainTest 测试类
public class MainTest {

    public static void main(String[] args) throws InterruptedException {
        while (true){
            print(UUID.randomUUID().toString());
            TimeUnit.SECONDS.sleep(2);
        }
    }

    private static void print(String name){
        System.out.println("时间:"+LocalDateTime.now()+","+"hello "+name);
    }
}
  1. 新建一个打印方法耗时的BTrace脚本
import org.openjdk.btrace.core.BTraceUtils;
import org.openjdk.btrace.core.annotations.*;

@BTrace
public class PrintMethodTime {

    @OnMethod(
            clazz = "com.example.jvmlearing.agent.MainTest",
            method = "print",
            location = @Location(value = Kind.CALL
                    , clazz = "/.*/" , method = "/.*/"
                    , where = Where.AFTER)
    )
    public static void method(@ProbeClassName String probeClass,
                         @ProbeMethodName String probeMethod ,
                         @Duration long duration) {
        BTraceUtils.print("class name= " + probeClass);
        BTraceUtils.print("method name =" + probeMethod);
        BTraceUtils.print("duration="+duration);
    }
}

参数解释:

  • @Location:拦截的时机,类似于AOP,在方法的执行前,执行后等时机进行拦截。
  • Kind@Location作用的探测点的种类
  • Where:探测点的位置
  1. 进入脚本所在文件夹,测试脚本 先通过jps命令获取到MainTest 测试类的进程id,然后通过btrace命令绑定该进程id,之后就能看到脚本运行结果了

这里只是简单的使用一下BTrace,在BTrace解压后的samples文件夹下有很多例子,有兴趣的可以去看一下。

BTrace限制

BTrace最终借Instrument实现class的替换。出于安全考虑,Instrument在使用上存在诸多的限制。因此BTrace脚本也有很多限制,限制如下:

  • 不允许创建对象
  • 不允许创建数组
  • 不允许抛异常
  • 不允许catch异常
  • 不允许随意调用其他对象或者类的方法,只允许调用com.sun.btrace.BTraceUtils中提供的静态方法(一些数据处理和信息输出工具)
  • 不允许改变类的属性
  • 不允许有成员变量和方法,只允许存在static public void方法
  • 不允许有内部类、嵌套类
  • 不允许有同步方法和同步块
  • 不允许有循环(for, while, do..while)
  • 不允许随意继承其他类(当然,java.lang.Object 除外)
  • 不允许实现接口
  • 不允许使用 assert
  • 不允许使用 Class 对象 如此多的限制,其实可以理解。BTrace要做的是,虽然修改了字节码,但是除了输出需要的信息外,对整个程序的正常运行并没有影响。

总结

其实作为 Java的动态追踪技术,站在比较底层的角度上来说,底层无非就是基ASM、Java Attach API、Instrument开发的创建。Arthas 都是针前面这些技术的一个封装而已。