本文翻译自 https://github.com/btraceio/btrace/wiki


BTrace 简介

BTrace 是一个用于 Java 的安全动态追踪工具。它通过动态修改正在运行的 Java 程序的字节码来工作,在运行的 Java 类上进行 Hotswap(热交换),从而插入追踪动作。

快速开始

在命令行键入 btrace <PID> AllMethods.java,其中 PID 可以是任何正在运行的目标 Java 进程 ID。

这个样本脚本 trace scriptAllMethods.java)会埋点所有 javax.swing.* 下的类的所有方法。埋点将在每个被埋点的方法进入时打印类名和方法名。

译注:原文中的 "instrument" 一词较难翻译。经过检索相关资料,在这篇文章的上下文中,将其翻译为“埋点”更易于理解。这个词的其他近似含义包括:插桩、调改、调控、监测等。

所有的输出会打印到标准输出(stdout)。当脚本运行期间,你可以键入 Ctrl-C 来停止 BTrace 工具运行。

片段 1 - AllMethods.java

package samples;

import com.sun.btrace.annotations.*;
import static com.sun.btrace.BTraceUtils.*;

/**
 * 这个脚本会追踪每个 javax.swing 包下的方法进入
 * 使用前三思,这个操作会显著地减慢你的程序的运行 
 */
@BTrace public class AllMethods {
    @OnMethod(
        clazz="/javax\\.swing\\..*/",
        method="/.*/"
    )
    public static void m(@ProbeClassName String probeClass, @ProbeMethodName String probeMethod) {
        print(Strings.strcat("entered ", probeClass));
        println(Strings.strcat(".", probeMethod));
    }
}

运行 BTrace 的细节

动态依附应用程序

此模式适用于快速依附于一个已经运行的程序、获取感兴趣的数据、再取消依附、删掉追踪代码的场景。

依附的命令行是:
btrace [-p <port>] [-cp <classpath>] <pid> <btrace-script> [<args>]

  • port:BTrace 代理监听的接口,这是一个可选参数。
  • classpath:目录、jar 文件的集合。用来在 BTrace 执行编译过程中找到这些要编译或链入的文件。默认是 .
  • btrace-script:我们的追踪脚本。如果它是一个 .java 文件,那么在它被提交前会被编译。否则这个参数就应该指明一个已经编译好的 .class 文件,其会被直接提交。

运行 btrace -h 会打印命令用法。

同时运行应用程序和 BTrace

在这个模式下,BTrace 的运行甚至要早于应用程序的初始代码运行。这给我们机会去跟踪应用程序生命周期中最早期的运行情况。

运行应用程序的同时开启 BTrace 代理的命令是:
java -javaagent:btrace-agent.jar=[<agent-arg>[,<agent-arg>]*]? <launch-args>

这个代理接受逗号分隔的参数列表:

  • noServer:不启动 socket server。
  • bootClassPath:> 译注:bootstrap classloader 加载 java.*javax.* 下的类时用的路径。
  • systemClassPath:> 译注:system classloader 加载类时用到的寻找路径。
  • debug:开关:verbose 调试信息(true/false)。
  • trusted:开关:不检查是否违反了 BTrace 的限制(true/false)。
  • dumpClasses:开关:dump 转换后的字节码到文件中(true/false)。
  • dumpDir:标明转换后的 class 文件要 dump 到哪个目录中。
  • stdout:开关:重定向 btrace 的输出到标准输出,而不是写入一个任意文件(true/false)。
  • probeDescPath:寻找探针描述符 XML 的路径。
  • startupRetransform:开关:在 attach 时将所有已经加载的经过转换的类转换回去(true/false)。
  • scriptdir:在 agent 开启时寻找脚本的路径。
  • scriptOutputFile:存有 btrace 的输出的文件路径。
  • script:在代理开始后要运行的脚本的列表,其用冒号分隔。

要运行的脚本必须提前已经用 btracec 编译成字节码(.class 文件)。

适应性埋点

BTrace 支持用埋点等级来做到适应性埋点。每个探针可以通过注解标记其最小生效等级。这样一些轻量级埋点可以总被设置成 0 级,而一些运行代价较大的埋点可以考虑设置成 100 以上。

埋点等级在运行期间是可以调整的,它甚至可以在一个探针的 handler 里进行调整,这样就可以让一些我们期待的特定触发点去动态地升高或降低埋点等级。

声明探针的生效埋点等级

每个探针的埋点等级可以通过 @Level 注解来定义。这个注解接受诸如 [=,>,<,<=,>=] 的字符串来声明他们等级。

// 下面的探针将会在过滤的埋点等级至少为 100 时才会激活
@Level("100") // @Level(">=100") 的效果是同样的
// 也可以根据需求设置成诸如 @Level("<100") @Level(">100") @Level("<=100") @Level("=100")
@OnMethod(...)
public void handler() {
   ...
}

调整埋点等级

探针的埋点等级可以通过调用 BTraceUtils.setInstrumentationLevel(level) 来设置。而 BTraceUtils.getInstrumentationLevel() 则可以获取当前的埋点等级。

性能效益

停用探针的 handler 只会造成整型比较和指令跳转的额外性能代价,而这些相对于整个探针 handler 的工作造成的性能代价是微不足道的。开、关埋点等级,二者间的性能差异也就在微秒之间。

BTrace 注解

方法注解

@OnMethod

用于标明在目标类、目标方法、目标方法的埋点位置。
@OnMethod(clazz=<cname_spec>[, method=<mname_spec>]? [, type=<signature>]? [, location=<location_spec>]?)
  • cname_spec = <class_name> | +<class_name> | /regex/
  • class_name:类全名。
  • +class_name:一个 + 号再加上类/接口全名,意味着目标类/接口的所有子类或实现类。
  • /regex/:能标明目标类名字的标准正则表达式。
  • mname_spec = <method_name> | /regex/
  • method_name:简单方法名(没有函数签名或返回类型)。
  • signature = <return_type> ((arg_type(,arg_type)*)?
  • return_type:方法的返回类型(例如:voidjava.lang.String)。
  • arg_type:参数类型。

被注解标注的 action 将会在匹配的方法运行到指定的位置时而触发。在 @OnMethod 注解中,被追踪的类用 clazz、被追踪的方法用 method 属性标明。clazz 可以是类全名(就像 java.awt.Component)或者是一个用左右两个斜杠括住的正则表达式。可以参看这两个例子 NewComponent.javaClassload.java

正则表达式可以匹配零到多个类,这些类都会被埋点。例如 /java\.awt\..+/ 匹配 java.awt 包下的所有类。并且,方法名同理也可以用正则表达式来标明。例如 MultiClass.java

还有一种方式可以抽象地标明被追踪的类或方法。被追踪的类和方法名可以被注解来标识。举个例子,如果 clazz 属性设置为 @javax.jws.WebService。BTrace 会给所有被添加了 WebService 注解的类埋点。类似地,目标程序里方法上的注解也可以被用来抽象地标明让 BTrace 埋点哪个方法。例如 WebServiceTracker.java

使用正则表达式来标明注解也是可以的,例如 @/com\.acme\..+/ 匹配了所有匹配指定正则表达式的注解所标注了的类。

还可以通过指定超类(> 译注:基类)的方式来指明要匹配该超类的所有子类。例如 +java.lang.Runnable 匹配了所有实现了 java.lang.Runnable 接口的类型。参考例子 SubtypeTracer.java

@OnTimer

用来标明追踪 action 必须每隔 N 毫秒周期性地执行。
@OnTimer([[value=]?<value>]?)
  • value:指定的时间周期。
    时间周期通过注解的 value 属性指定。参考例子 Histogram.java

@OnError

用来标明如果其他探针的追踪 action 抛出了异常,所要做的动作。
当任何相同 BTrace Class 中的其他 BTrace action 产生了异常,该注解标注的方法会被执行。

@OnExit

用来标注当 BTrace 代码执行了内建函数 exit(int) 去关闭 BTrace 会话时要执行的动作。
参考例子 ProbeExit.java

@OnEvent

用来关联追踪方法与 BTrace 客户端发送的外部事件。
@OnEvent([[value=]?<event_name>]?)
  • event_name:是这个 handler 要响应的 event 名字。

当 BTrace 客户端发送了一个事件,被该注解标注的 BTrace 方法会被执行。客户端发送的事件可以是用户的请求的某种形式(比如 Ctrl-C 或者 GUI 菜单)。字符串值可以被用来作为事件的名字。这样,无论这些“事件”什么时候被用户触发,这些特定的 action 都会执行。

现在,只要用户按下了 Ctrl-C(SIGINT),BTrace 的命令行客户端就可以发送事件。按下 Ctrl-C 后,一个命令行菜单会显示出来。你可以选择发送事件或者退出。HistoOnEvent.java

@OnLowMemory

用来追踪内存使用已超出限定值的事件。
可以参考例子 MemAlerter.java

@OnProbe

用来标注无需使用 BTrace 脚本里定义的 target。
@OnProbe(namespace=<namespace_name>, name=<probe_name>)

译注:原文 "used to specify to avoid using implementation internal classes in BTrace scripts"。
  • namespace_name:是一个任意的 Java 包的命名空间。
  • probe_name:是一个任意的探针名字。

@OnProbe 探针声明会映射到一个或多个 BTrace 代理声明的 @OnMethod。现在这个声明可以使用一个 XML 探针描述符来指定某个探针名(这个 XML 文件会被 BTrace 代理访问)。可以参考例子 SocketTracker1.java 和其使用的 XML 探针描述文件 java.net.socket.xml

译注:这里的含义其实就是,在 BTrace 脚本里声明探针位置的方式一般是使用 @OnMethod 注解指定。但是还有一种方式是不使用 @OnMethod 指定,而是额外写一个 XML 文件来间接地定义探针位置,里面可以定义一些探针标签,并且每个探针标签下可以再通过 @OnMethod 指定具体的位置。这样 BTrace 脚本里直接使用 @OnProbe 注解指定 XML 里面声明的探针名字就好了。可以对比下 SocketTracker.java(使用 @OnMethod 指定位置)和 SocketTracker1.java(使用 XML 声明探针,脚本里使用 XML 定义的探针)这两个文件就懂了。

当运行这个例子时,这个 XML 文件需要放置在目标 JVM 运行的目录里(或者在 btracer.bat 里去修改 probeDescPath 选项,去指明 .xml 文件的位置)。

@Sampled

为被注解了的 handler 开启 sampling。与 @OnMethod 注解一起连用。
@Sampled([kind=<sampler_kind>[,mean=<number>])
  • <sampler_kind>:只能是 Sampler.Const, Sampler.AdaptiveSampler.None 中的一个。

参数注解

@ProbeClassName

用来标识 handler 的这个入参是当前打断的方法是哪个类的。只能在被标注了 @OnMethod 注解的探针 handler 方法里使用。

@ProbeMethodName

用来标识 handler 的这个入参是当前打断的哪个方法。只能在被标注了 @OnMethod 注解的探针 handler 方法里使用。
@ProbeMethodName([fqn=(true|false)]?)
  • fqn:表示应该使用方法全名而不是简单名,默认是 false

@Self

表示该参数是用来访问当前打断的成员方法所依附的实例。对一个 handler 参数使用该注解会有效地过滤掉其他匹配的静态方法。

译注:这个的含义就是通过名字、签名等方式匹配出来的目标方法可能是 static 的也可能是非 static 的。使用这个注解,会直接表明我们要的是非 static 的,因为只有非 static 的方法才有一个隐式的 this 指针指向一个实例,即该参数。

@Return

该注解标识该参数是打断的目的方法的返回值。只可以用于 location=@Location(Kind.RETURN) 的情况。并且该注解会造成,如果目的方法没有返回值那么就不会触发改 handler。

@Duration

目标方法的执行耗时会通过注解了 @Duration 的参数提供。只可以用于 location=@Location(Kind.RETURN)long 的情况。

@TargetInstance

这个注解标识该参数提供访问当前打断的成员方法所依附的实例(和 @Self 很像)但是可以用于 location=@Location([Kind.CALL|Kind.FIELD_GET|Kind.FIELD_SET),即非 static 方法被调用、字段被设置或者读取的情况。

@TargetMethodOrField

这个注解标识该参数提供方法或字段的访问(类似于 @ProbeMethodName),适用于 location=@Location([Kind.CALL|Kind.FIELD_GET|Kind.FIELD_SET)
@TargetMethodOrField([fqn=(true|false)]?)
  • fqn:表示是否使用全名而不是简单名,默认为 false

btracec

btracec 是用来把追踪脚本(trace scripts)编译为 class 文件的命令行工具。

其命令用法是:
btracec [-cp <classpath>] [-d <directory>] <one-or-more-BTrace-.java-files>

  • classpath:是用来编译 BTrace 程序的 classpath。默认是 .
  • directory:是编译后的 class 文件存放的目录。默认是 .

与通常使用的 javac 不同的是,btracec 能够在编译期间检查脚本正确性,防止其在运行期间产生关于脚本正确性的初级错误。

btracer

btracer 是一个脚本。用于方便地在开启应用程序同时打开 btrace。

其命令行语法是:
btracer <pre-compiled-btrace.class> <vm-arg> <application-args>

  • pre-compiled-btrace.class:通过 btracec 编译出的 trace 脚本 class 文件。
  • vm-args:虚拟机参数,例如 -cp app.jar Main.class-jar app.jar
  • application-args:指定应用程序的参数。

Trace 脚本

Trace 脚本定义了要追踪的是什么并且怎样追踪。它们就是一些看起来被加上了许多注解的普通 Java 类。注解(annotations)标识了埋点应该在哪里放置,并且什么数据传给 action。

脚本元素(Script Elements)

探针点(Probe Point)

即在 location 或发生 event 时,trace action 的一系列语句才会被执行。探针点指的是在目标程序中,我们添加的一些感兴趣的点或者事件发生的时刻。

追踪动作(Trace Actions or Actions)

当一个探针被触发时,要执行的一系列指令语句。

动作方法(Action Methods)

当探针被触发时,会被运行的那些在脚本里定义的 static 方法。

限制

为了保证在目标程序中动态注入代码的安全性,有下面几点强制的限制:

  • 不允许 new 一个对象。
  • 不允许 new 一个数组。
  • 不允许手动抛出异常。
  • 不允许捕获任何异常。
  • 不允许随意调用实例的方法或 static 方法,在 BTrace 程序中只能调用在该程序中声明的方法(> 译注:存疑)。
  • 不允许声明外部、内部、嵌套、局部类。
  • 不允许有 synchronized 块或 synchronized 方法。
  • 不允许有循环语句(forwhiledo...while)。
  • 不允许继承类(超类只能是 java.lang.Object)。
  • 不允许实现接口。
  • 不允许有 assert 断言语句。
  • 不允许有字面值。

说明:本文基于 BTrace 官方 Wiki 翻译,主要适用于 Java 8 及以下版本。在 Java 9 及更高版本中,由于模块系统(JPMS)和安全策略的变化,BTrace 的部分功能可能受到限制或需要额外配置才能正常运行。