# Btrace 使用详解

  • 如何拦截方法

    如何拦截构造函数、重载方法、普通方法

  • 拦截时机

    如:入口、返回、异常 时

  • 拦截 this、参数、返回值等

  • 其他

这些在官方仓库都有拦截例子 (opens new window)

# 拦截方法

  • 普通方法

    @OnMethod(clazz="", method="")
    
    1
  • 构造函数

    @OnMethod(clazz="", method="<init>")
    
    1

    <init>:字节码中构造函数就是使用这个来标识的

  • 拦截同名函数:用参数区分

# 拦截构造函数

准备一段测试代码,接受一个 user 参数,然后再返回去

@RequestMapping("/constructor")
public User constructor(User user) {
  user.setName("mrcode");
  return user;
}
1
2
3
4
5

打印构造函数脚本

package cn.mrcode.stady.monitor_tuning.chapter4;

import org.openjdk.btrace.core.BTraceUtils;
import org.openjdk.btrace.core.annotations.*;
import org.openjdk.btrace.core.types.AnyType;

/**
 * Btrace 拦截构造函数
 */
@BTrace
public class PrintConstructor {
    /**
     * OnMethod 表示设置拦截哪个类 的 哪个方法,location 是具体的位置或则点
     *
     * @param pcn  探测到的类名
     * @param pmn  探测到的方法名称
     * @param args 入参
     */
    @OnMethod(
            clazz = "cn.mrcode.stady.monitor_tuning.chapter2.User", // 注意这里拦截的是 User 类
            method = "<init>" // 设置为构造方法
    )
    public static void anyRead(@ProbeClassName String pcn,
                               @ProbeMethodName String pmn,
                               AnyType[] args) {
        BTraceUtils.println(pcn + "," + pmn);
        BTraceUtils.printArray(args);
        BTraceUtils.println();
    }
}

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

访问 GET http://localhost:8080/ch4/constructor?id=1 查看脚本输出

mrcode@mrcode chapter4 % btrace 56414 PrintConstructor.java 
Attaching BTrace to PID: 56414
cn.mrcode.stady.monitor_tuning.chapter2.User,<init>
[1, mrcode, ]
1
2
3
4

# 拦截重载方法

先准备两个重载方法(同名,参数个数或类型不同)

    /**
     * btrace 拦截重载方法
     *
     * @param id
     * @return
     */
    @RequestMapping("/same1")
    public String same(Integer id) {
        return id + "";
    }

    @RequestMapping("/same2")
    public String same(Integer id, String name) {
        return id + "";
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

测试

### 拦截重载方法2
GET http://localhost:8080/ch4/same1?id=1

响应:1

### 拦截重载方法2
GET http://localhost:8080/ch4/same2?id=1&name=mrcode

响应:1,mrcode
1
2
3
4
5
6
7
8
9

构建 Btrace 脚本

package cn.mrcode.stady.monitor_tuning.chapter4;

import org.openjdk.btrace.core.BTraceUtils;
import org.openjdk.btrace.core.annotations.BTrace;
import org.openjdk.btrace.core.annotations.OnMethod;
import org.openjdk.btrace.core.annotations.ProbeClassName;
import org.openjdk.btrace.core.annotations.ProbeMethodName;
import org.openjdk.btrace.core.types.AnyType;

/**
 * Btrace 拦截重载方法
 */
@BTrace
public class PrintSame {
    /**
     * OnMethod 表示设置拦截哪个类 的 哪个方法
     *
     * @param pcn 探测到的类名
     * @param pmn 探测到的方法名称
     */
    @OnMethod(
            clazz = "cn.mrcode.stady.monitor_tuning.chapter4.Ch4Controller",
            method = "same"
    )
    public static void anyRead(@ProbeClassName String pcn,
                               @ProbeMethodName String pmn,
                               Integer id, String name  // 根据参数的个数拦截重载中的哪一个方法
    ) {
        BTraceUtils.println("拦截有 2 个入参的 same 方法");
        BTraceUtils.println(pcn + "," + pmn);
        BTraceUtils.println(id + "," + name);
        BTraceUtils.println();
    }

    @OnMethod(
            clazz = "cn.mrcode.stady.monitor_tuning.chapter4.Ch4Controller",
            method = "same"
    )
    public static void anyRead2(@ProbeClassName String pcn,
                                @ProbeMethodName String pmn,
                                Integer id
    ) {
        BTraceUtils.println("拦截只有一个入参的 same 方法");
        BTraceUtils.println(pcn + "," + pmn);
        BTraceUtils.println(id);
        BTraceUtils.println();
    }
}

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

测试输出

mrcode@mrcode chapter4 % btrace 56583 PrintSame.java
Attaching BTrace to PID: 56583
拦截只有一个入参的 same 方法
cn.mrcode.stady.monitor_tuning.chapter4.Ch4Controller,same
1

拦截有 2 个入参的 same 方法
cn.mrcode.stady.monitor_tuning.chapter4.Ch4Controller,same
1,mrcode
1
2
3
4
5
6
7
8
9

# 拦截时机

可以拦截的枚举在 org.openjdk.btrace.core.annotations.Kind,常用的有

  • Kind.ENTRY:入口,也是默认值
  • Kind.RETURN:返回
  • Kind.THROW:异常
  • Kind.Line:行

# 获取返回值

package cn.mrcode.stady.monitor_tuning.chapter4;

import org.openjdk.btrace.core.BTraceUtils;
import org.openjdk.btrace.core.annotations.*;
import org.openjdk.btrace.core.types.AnyType;

/**
 * Btrace 获取返回值
 */
@BTrace
public class PrintReturn {
    /**
     * OnMethod 表示设置拦截哪个类 的 哪个方法,location 是具体的位置或则点
     *
     * @param pcn    探测到的类名
     * @param pmn    探测到的方法名称
     * @param result 返回值
     */
    @OnMethod(
            clazz = "cn.mrcode.stady.monitor_tuning.chapter4.Ch4Controller",
            method = "arg1",
            location = @Location(Kind.RETURN)
    )
    public static void anyRead(@ProbeClassName String pcn,
                               @ProbeMethodName String pmn,
                               @Return AnyType result) { // 这里使用 @Return 定义获取返回值
        BTraceUtils.println(pcn + "," + pmn);
        BTraceUtils.println(result);
        BTraceUtils.println();
    }
}
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

测试输出

mrcode@mrcode chapter4 % btrace 56583 PrintReturn.java 
Attaching BTrace to PID: 56583
cn.mrcode.stady.monitor_tuning.chapter4.Ch4Controller,arg1
hello,mrcode
1
2
3
4

# 获取异常

如下代码


    /**
     * btrace 获取异常
     * @return
     */
    @RequestMapping("/exception")
    public String exception() {
        try {
            System.out.println("start");
            System.out.println(1/0);
            System.out.println("end");
        }catch (Exception e){
            // 模拟在一个多层调用中,把异常吞掉了
        }
        return "success";
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

当在一个复杂的调用中,异常又被吞掉的时候,在线上环境中,我们是很难发现这个代码运行异常了。

访问这个方法:看到日志中并没有异常信息

GET http://localhost:8080/ch4/exception

后台日志中只打印了一行:
start

1
2
3
4
5

编写脚本

package cn.mrcode.stady.monitor_tuning.chapter4;

import org.openjdk.btrace.core.BTraceUtils;
import org.openjdk.btrace.core.annotations.*;

/**
 * Btrace 获取异常; 其实使用的就是拦截的是 异常类 的构造函数
 */
@BTrace
public class PrintOnThrow {
    @TLS   // 表示使用 thred local 变量,多线程状态下,该值才是每个线程自己的值
    static Throwable currentException;

    /**
     * OnMethod 表示设置拦截哪个类 的 哪个方法,location 是具体的位置或则点
     */
    @OnMethod(
            clazz = "java.lang.Throwable",
            method = "<init>"
    )
    public static void onthrow(@Self Throwable self) {
        // 相当于拦截:new Throwable()
        currentException = self;
    }

    @OnMethod(
            clazz = "java.lang.Throwable",
            method = "<init>"
    )
    public static void onthrow1(@Self Throwable self, String s) {
        // 相当于拦截:new Throwable(String msg)
        currentException = self;
    }

    @OnMethod(
            clazz = "java.lang.Throwable",
            method = "<init>"
    )
    public static void onthrow2(@Self Throwable self, String s, Throwable cause) {
        // 相当于拦截:new Throwable(String msg,Throwable cause)
        currentException = self;
    }

    @OnMethod(
            clazz = "java.lang.Throwable",
            method = "<init>"
    )
    public static void onthrow3(@Self Throwable self, Throwable cause) {
        // 相当于拦截:new Throwable(Throwable cause)
        currentException = self;
    }

    /**
     * 当 Throwable 构造函数返回的时候,则打印传入构造函数的异常
     */
    @OnMethod(
            clazz = "java.lang.Throwable",
            method = "<init>",
            location = @Location(Kind.RETURN)
    )
    public static void onthrowreturn() {
        if (currentException != null) {
            // 打印异常堆栈
            BTraceUtils.Threads.jstack(currentException);
            BTraceUtils.println("======================");
            currentException = null;
        }
    }
}

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

启动脚本

mrcode@mrcode chapter4 % btrace 57971 PrintOnThrow.java 
Attaching BTrace to PID: 57971
Port 2020 unavailable.
1
2
3

发现异常了 2020 端口被占用,原因是一台机器上同一时间,貌似只能运行一个脚本,否则就需要自己重新指定一个没有被使用的端口

# mac 下找到占用该端口的进程,再 kill 掉
mrcode@mrcode chapter4 % lsof -i tcp:2020
COMMAND   PID   USER   FD   TYPE             DEVICE SIZE/OFF NODE NAME
webstorm 1694 mrcode   48u  IPv6 0x566cdbbd9b7e8cc3      0t0  TCP *:xinupageserver (LISTEN)
mrcode@mrcode chapter4 % kill 1694
1
2
3
4
5

发现拦截无效,不知道是什么原因(官方的示例代码也无法生效)。

# 拦截某一行代码

想要看某行代码有没有执行,打印下这一行的行号

package cn.mrcode.stady.monitor_tuning.chapter4;

import org.openjdk.btrace.core.BTraceUtils;
import org.openjdk.btrace.core.annotations.*;

@BTrace
public class PrintLine {

    @OnMethod(
            clazz = "cn.mrcode.stady.monitor_tuning.chapter4.Ch4Controller",
            method = "exception",
            location = @Location(value = Kind.LINE, line = 58)
    )
    public static void anyRead(@ProbeClassName String pcn, @ProbeMethodName String pmn, int line) {
        BTraceUtils.println(pcn + "," + pmn + "," + line);
        BTraceUtils.println();
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

运行脚本,访问后输出

mrcode@mrcode chapter4 % btrace 1448 PrintLine.java 
Attaching BTrace to PID: 1448
cn.mrcode.stady.monitor_tuning.chapter4.Ch4Controller,exception,58
1
2
3

如果将 line 修改为 -1 则会看到,会将执行过的行都打印

mrcode@mrcode chapter4 % btrace 1448 PrintLine.java   
Attaching BTrace to PID: 1448
cn.mrcode.stady.monitor_tuning.chapter4.Ch4Controller,exception,58

cn.mrcode.stady.monitor_tuning.chapter4.Ch4Controller,exception,59

cn.mrcode.stady.monitor_tuning.chapter4.Ch4Controller,exception,61

cn.mrcode.stady.monitor_tuning.chapter4.Ch4Controller,exception,64
1
2
3
4
5
6
7
8
9

# 拦截 this、参数、返回值等

  • this:@Self
  • 入参:可以用 AnyType,也可以用真实类型,同名的真实的
  • 返回:@Return

获取对象的值:

  • 简单类型:直接获取
  • 复杂类型:反射。类名 + 属性名

# 获取复杂类型参数

/**
* Btrace 获取复杂参数
*
* @param user
* @return
*/

@RequestMapping("/arg2")
public User grg2(User user) {
	return user;
}
1
2
3
4
5
6
7
8
9
10
11

脚本

package cn.mrcode.stady.monitor_tuning.chapter4;
import cn.mrcode.stady.monitor_tuning.chapter2.User;
import org.openjdk.btrace.core.BTraceUtils;
import org.openjdk.btrace.core.annotations.*;

import java.lang.reflect.Field;


@BTrace
public class PrintArgComplex {
	
	@OnMethod(
	        clazz="cn.mrcode.stady.monitor_tuning.chapter4.Ch4Controller",
	        method="grg2",
	        location=@Location(Kind.ENTRY)
	)
	public static void anyRead(@ProbeClassName String pcn, @ProbeMethodName String pmn, User user) {
		//print all fields
		BTraceUtils.printFields(user);
		// 第三方包需要获取需要使用类全限定名
		//print one field
		Field filed2 = BTraceUtils.field("cn.mrcode.stady.monitor_tuning.chapter2.User", "name");
		BTraceUtils.println(BTraceUtils.get(filed2, user));
		BTraceUtils.println(pcn+","+pmn);
		BTraceUtils.println();
    }
}

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

测试输出

mrcode@mrcode chapter4 % btrace -cp "/Users/mrcode/IdeaProjects/monitor-tuning/build/classes/java/main" 1813 PrintArgComplex.java
Attaching BTrace to PID: 1813
{id=1, name=mrcode, }
mrcode
cn.mrcode.stady.monitor_tuning.chapter4.Ch4Controller,grg2
1
2
3
4
5

注意这里:使用了一个 -cp 的参数,指向了这个目标项目的 class 文件目录,也就是说复杂参数引用,需要加入依赖包,否则就会直接报错找不到符号。这样一来,线上环境是如何的复杂,感觉使用很难用。

# 使用正则表达式拦截

package cn.mrcode.stady.monitor_tuning.chapter4;


import org.openjdk.btrace.core.BTraceUtils;
import org.openjdk.btrace.core.annotations.BTrace;
import org.openjdk.btrace.core.annotations.OnMethod;
import org.openjdk.btrace.core.annotations.ProbeClassName;
import org.openjdk.btrace.core.annotations.ProbeMethodName;

/**
 * btrace:拦截该类中的所有方法
 */
@BTrace
public class PrintRegex {

    @OnMethod(
            clazz = "cn.mrcode.stady.monitor_tuning.chapter4.Ch4Controller",
            method = "/.*/"  // 正则语法 /正则表达式/,.*:任意字符,零次或多次
    )
    public static void anyRead(@ProbeClassName String pcn, @ProbeMethodName String pmn) {
        BTraceUtils.println(pcn + "," + pmn);
        BTraceUtils.println();
    }
}

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

测试输出

mrcode@mrcode chapter4 % btrace 1978 PrintRegex.java
Attaching BTrace to PID: 1978
cn.mrcode.stady.monitor_tuning.chapter4.Ch4Controller,exception
1
2
3

# 打印环境变量

package cn.mrcode.stady.monitor_tuning.chapter4;


import org.openjdk.btrace.core.BTraceUtils;
import org.openjdk.btrace.core.annotations.BTrace;

@BTrace
public class PrintJinfo {
    static {
        BTraceUtils.println("System Properties:");
        BTraceUtils.printProperties();
        BTraceUtils.println("VM Flags:");
        BTraceUtils.printVmArguments();
        BTraceUtils.println("OS Enviroment:");
        BTraceUtils.printEnv();
        BTraceUtils.exit(0);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

测试输出

mrcode@mrcode chapter4 % btrace 1978 PrintJinfo.java 
Attaching BTrace to PID: 1978
System Properties:
gopherProxySet = false
awt.toolkit = sun.lwawt.macosx.LWCToolkit
file.encoding.pkg = sun.io
java.specification.version = 1.8
sun.cpu.isalist = 
sun.jnu.encoding = UTF-8
...
1
2
3
4
5
6
7
8
9
10

这里打印的信息就类似 jinfo 命令打印的信息。

# 注意事项

  • 默认只能本地运行

    想要调试远程的进程,需要修改源代码

  • 生产环境下可以使用,但是被修改的字节码不会被还原

    意思是说,btrace 进程退出后,被 btace 修改过的代码,还是会生效。

    没说清楚这些监控是否也还在执行呢?