BTrace是Java的安全可靠的动态跟踪工具。 它的工作原理是通过 instrument + asm 来对正在运行的java程序中的class类进行动态增强。也就是说btrace可以在Java程序运行时,动态地向目标应用程序的字节码注入追踪代码。
说他是安全可靠的,是因为它对正在运行的程序是只读的。也就是说,他可以插入跟踪语句来检测和分析运行中的程序,不允许对其进行修改。因此他存在一些限制:
这些限制其实是可以使用unsafe模式绕过。通过在BTrace脚本中声明 @BTrace(unsafe = true) 注解 ,并且使用 -u 选项,指定以 unsafe 模式运行btrace即可
注:实际使用非安全模式跟踪时,发现一个问题,一个进程如果被安全模式btrace探测过一次, 后面再使用非安全模式进行探测时非安全模式不生效。
btrace的github地址:
https://github.com/btraceio/btrace
到github上下载btrace的压缩包,我这里下载的是1.3.11版本,下载地址如下:
https://github.com/btraceio/btrace/releases/tag/v1.3.11
解压下载的压缩包,解压后可以看到目录如下:
然后就是配置环境变量,首先创建一个BTRACE_HOME,该环境变量的值就是btrace的安装目录:
接着在path变量里,配置BTRACE_HOME环境变量下的bin目录:
配置好btrace后,我们有两种方式可以运行BTrace脚本:
btrace < pid > < trace_script >
我们先来演示第二种使用命令行的运行方式,在工程里新建一个简单的controller,用于演示如何利用BTrace脚本来实时获取方法的参数值:
package org.zero01.monitor_tuning.controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; /** * @program: monitor_tuning * @description: BTrace演示 * @author: 01 * @create: 2018-07-14 22:33 **/ @RestController @RequestMapping("/btrace") public class BTraceController { @RequestMapping("/arg1") public String arg1(@RequestParam("name") String name) { return "hello: " + name; } }
编写完这个Controller后就可以启动项目了,为了方便演示,我们直接将BTrace脚本写在这个工程里,实际上BTrace脚本与我们的项目代码是分离的,没有依赖关系的。但我们得引入编写BTrace脚本所需的jar包,pom.xml配置的依赖如下
<dependency> <groupId>com.sun.btrace</groupId> <artifactId>btrace-agent</artifactId> <version>1.3.11</version> <type>jar</type> <scope>system</scope> <systemPath>E:/BTrace/btrace-bin-1.3.11/build/btrace-agent.jar</systemPath> </dependency> <dependency> <groupId>com.sun.btrace</groupId> <artifactId>btrace-boot</artifactId> <version>1.3.11</version> <type>jar</type> <scope>system</scope> <systemPath>E:/BTrace/btrace-bin-1.3.11/build/btrace-boot.jar</systemPath> </dependency> <dependency> <groupId>com.sun.btrace</groupId> <artifactId>btrace-client</artifactId> <version>1.3.11</version> <type>jar</type> <scope>system</scope> <systemPath>E:/BTrace/btrace-bin-1.3.11/build/btrace-client.jar</systemPath> </dependency>
注:systemPath配置的是BTrace安装目录里的jar包路径,如果不想指向路径的话,可以使用maven命令,把这些jar包安装到本地的maven仓库中,命令示例如下:
mvn install:install-file -Dfile=E:/BTrace/btrace-bin-1.3.11/build/btrace-boot.jar -DgroupId=com.sun.btrace -DartifactId=btrace-boot -Dversion=1.3.11 -Dpackaging=jar
然后新建一个btrace包,在该包内新建一个 PrintArgSimple 类,代码如下:
package org.zero01.monitor_tuning.btrace; import com.sun.btrace.AnyType; import com.sun.btrace.BTraceUtils; import com.sun.btrace.annotations.BTrace; import com.sun.btrace.annotations.Kind; import com.sun.btrace.annotations.Location; import com.sun.btrace.annotations.OnMethod; import com.sun.btrace.annotations.ProbeClassName; import com.sun.btrace.annotations.ProbeMethodName; /** * @program: monitor_tuning * @description: BTrace脚本 * @author: 01 * @create: 2018-07-14 22:33 **/ @BTrace // 注明这是一个BTrace脚本 public class PrintArgSimple { // 指定需要拦截的方法 @OnMethod( // 类的路径 clazz = "org.zero01.monitor_tuning.controller.BTraceController", // 方法名 method = "arg1", // 在什么时候进行拦截 location = @Location(Kind.ENTRY) ) public static void anyRead(@ProbeClassName String pcn, // 被拦截的类名 @ProbeMethodName String pmn, // 被拦截的方法名 AnyType[] args // 被拦截的方法的参数值 ) { // 打印数组 BTraceUtils.printArray(args); // 打印行 BTraceUtils.println("className: " + pcn); BTraceUtils.println("MethodName: " + pmn); BTraceUtils.println(); } }
打开命令行,进入到PrintArgSimple.java文件所在的路径,然后使用命令运行该BTrace脚本:
C:/Users/admin>e: E:/>cd E:/Java_IDEA/monitor_tuning/src/main/java/org/zero01/monitor_tuning/btrace/ E:/Java_IDEA/monitor_tuning/src/main/java/org/zero01/monitor_tuning/btrace>jps 11300 7396 Jps 8504 MonitorTuningApplication 5656 RemoteMavenServer 12188 Main 13916 Launcher E:/Java_IDEA/monitor_tuning/src/main/java/org/zero01/monitor_tuning/btrace>btrace 8504 PrintArgSimple.java
注:因为代码中有中文注释,可能会报:编码GBK的不可映射字符 错误,但是并不影响运行,可以不用管它
在命令行中运行了BTrace脚本后,到浏览器上,访问我们之前所编写的接口,如下:
访问成功后,命令行输出如下,可以看到BTrace脚本成功拦截了我们输入的参数值,以及访问的controller和方法:
[小明, ] className: org.zero01.monitor_tuning.controller.BTraceController MethodName: arg1
以上就是如何在命令行里运行BTrace脚本。接着我们再来演示如何在JVisualVM里运行BTrace脚本,在此之前我们还需要安装一个BTrace的插件。关于BTrace插件的安装,我已经在 基于JVisualVM的可视化监控 一文中介绍过了,这里就不赘述了。复制好之前编写的BTrace脚本,然后按照下图粘贴到JVisualVM里运行即可:
注:在JVisualVM里运行BTrace脚本其实会有很多迷之问题,所以一般推荐使用命令行的运行方式
在上一小节中,我们简单介绍了BTrace的安装以及两种运行方式。并且也写了一个小demo用于动态地拦截方法参数,而本节将介绍一下如何拦截构造函数和同名方法。
首先我们新建一个实体类,代码如下:
package org.zero01.monitor_tuning.vo; public class User { private int id; private String name; public User(int id, String name) { super(); // 必须加上super不然无法拦截到参数 this.id = id; this.name = name; } ... gtter setter 略 ... }
在BTraceController类中,新增如下方法,并启动项目:
@RequestMapping("/constructor") public User constructor(User user) { return user; }
然后和之前一样,在btrace包下新建一个BTrace脚本,代码如下:
package org.zero01.monitor_tuning.btrace; import com.sun.btrace.AnyType; import com.sun.btrace.BTraceUtils; import com.sun.btrace.annotations.BTrace; import com.sun.btrace.annotations.OnMethod; import com.sun.btrace.annotations.ProbeClassName; import com.sun.btrace.annotations.ProbeMethodName; /** * @program: monitor_tuning * @description: 演示拦截构造函数 * @author: 01 * @create: 2018-07-14 22:33 **/ @BTrace public class PrintConstructor { @OnMethod( clazz = "org.zero01.monitor_tuning.vo.User", method = "<init>" // 指定拦截构造函数 ) public static void anyRead(@ProbeClassName String pcn, @ProbeMethodName String pmn, AnyType[] args) { BTraceUtils.println("ClassName: " + pcn); BTraceUtils.println("MethodName: " + pmn); BTraceUtils.printArray(args); BTraceUtils.println(); } }
同样的,在命令行中运行该脚本:
E:/Java_IDEA/monitor_tuning/src/main/java/org/zero01/monitor_tuning/btrace>jps 1184 MonitorTuningApplication 11300 15208 Launcher 5656 RemoteMavenServer 1052 Jps E:/Java_IDEA/monitor_tuning/src/main/java/org/zero01/monitor_tuning/btrace>btrace 1184 PrintConstructor.java
使用postman或浏览器访问接口:
访问成功后,命令行输出如下,则是成功拦截到了:
ClassName: org.zero01.monitor_tuning.vo.User MethodName: <init> [1, 小明, ]
演示完构造函数的拦截后,然后我们来看看如何拦截同名的方法,在 BTraceController 类里,增加如下两个同名方法:
@RequestMapping("/same1") public String same(@RequestParam("name") String name) { return "hello: " + name; } @RequestMapping("/same2") public User same(@RequestParam("id") int id, @RequestParam("name") String name) { return new User(id, name); }
然后在btrace包下新建一个BTrace脚本,代码如下:
package org.zero01.monitor_tuning.btrace; import com.sun.btrace.BTraceUtils; import com.sun.btrace.annotations.BTrace; import com.sun.btrace.annotations.OnMethod; import com.sun.btrace.annotations.ProbeClassName; import com.sun.btrace.annotations.ProbeMethodName; @BTrace public class PrintSame { @OnMethod( clazz = "org.zero01.monitor_tuning.controller.BTraceController", method = "same" ) public static void anyRead(@ProbeClassName String pcn, @ProbeMethodName String pmn, String name) { BTraceUtils.println("ClassName: " + pcn); BTraceUtils.println("MethodName: " + pmn); BTraceUtils.println("name: " + name); BTraceUtils.println(); } @OnMethod( clazz = "org.zero01.monitor_tuning.controller.BTraceController", method = "same" ) public static void anyRead(@ProbeClassName String pcn, @ProbeMethodName String pmn, int id, String name) { BTraceUtils.println("ClassName: " + pcn); BTraceUtils.println("MethodName: " + pmn); BTraceUtils.println("id: " + id); BTraceUtils.println("name: " + name); BTraceUtils.println(); } }
从以上的BTrace脚本中可以看到,其实拦截重载方法的话,只需要在BTrace脚本的方法中声明与之对应的参数即可。
接着就是在命令行里运行该脚本,访问相应的接口后,输出如下:
E:/Java_IDEA/monitor_tuning/src/main/java/org/zero01/monitor_tuning/btrace>jps 9024 MonitorTuningApplication 11300 3956 Launcher 5656 RemoteMavenServer 9272 Jps E:/Java_IDEA/monitor_tuning/src/main/java/org/zero01/monitor_tuning/btrace>btrace 9024 PrintSame.java ClassName: org.zero01.monitor_tuning.controller.BTraceController MethodName: same name: 小明 ClassName: org.zero01.monitor_tuning.controller.BTraceController MethodName: same id: 1 name: 小明
本小节将介绍常用的拦截方式,如何去拦截返回值、异常以及行号。
我们通过Kind来指定拦截方式,常用的拦截方式如下:
Kind.ENTRY // 入口拦截,默认值 Kind.RETURN // 拦截返回值 Kind.THROW // 发生异常时拦截 Kind.LINE // 拦截某一行
我们先来演示拦截返回值,就以之前编写的arg1方法作为示例。
在btrace包下新建一个BTrace脚本,代码如下:
package org.zero01.monitor_tuning.btrace; import com.sun.btrace.AnyType; import com.sun.btrace.BTraceUtils; import com.sun.btrace.annotations.BTrace; import com.sun.btrace.annotations.Kind; import com.sun.btrace.annotations.Location; import com.sun.btrace.annotations.OnMethod; import com.sun.btrace.annotations.ProbeClassName; import com.sun.btrace.annotations.ProbeMethodName; import com.sun.btrace.annotations.Return; @BTrace public class PrintReturn { @OnMethod( clazz="org.zero01.monitor_tuning.controller.BTraceController", method="arg1", location=@Location(Kind.RETURN) // 拦截返回值 ) public static void anyRead(@ProbeClassName String pcn, @ProbeMethodName String pmn, @Return AnyType result // 接收返回值) { BTraceUtils.println("ClassName: " + pcn); BTraceUtils.println("MethodName: " + pmn); BTraceUtils.println("ResultValue: " + result); BTraceUtils.println(); } }
在命令行里运行该脚本,访问相应的接口后,输出如下,可以看到成功拦截到了该方法的返回值:
E:/Java_IDEA/monitor_tuning/src/main/java/org/zero01/monitor_tuning/btrace>btrace 9024 PrintReturn.java ClassName: org.zero01.monitor_tuning.controller.BTraceController MethodName: arg1 ResultValue: hello: 小明
已经工作了的小伙伴们,应该会遇到一个常见的现象。那就是在很多旧项目的遗留代码中,总是能看到很多不妥的处理异常的方式。例如经常能看到把异常使用try-catch包起来,但是又不打印异常堆栈,也不抛出去。结果就是把异常隐藏了起来,没能让异常终止逻辑,导致了代码继续执行,还无法定位问题的所在。就如同以下这个方法一样:
@RequestMapping("/exception") public String exception() { try { System.out.println("start..."); System.out.println(1 / 0); System.out.println("end..."); } catch (Exception e) { } return "success"; }
我们访问以上这个接口,会看到返回的值是正确的,但是也会发现有部分逻辑没被执行,查看日志或控制台输出也没有异常信息。当出现这种情况,就真的两眼一抹黑了,根本无法排查问题(这里作为演示代码很简单,但是实际的项目代码可不是这样)。
还好我们现在知道了BTrace这样的调试工具,那么就可以利用BTrace脚本来拦截异常并打印异常堆栈,从而定位问题。
在btrace包下新建一个BTrace脚本,代码如下,这是我直接从官方示例里拿来的代码:
/* * Copyright (c) 2008, 2015, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Oracle designates this * particular file as subject to the Classpath exception as provided * by Oracle in the LICENSE file that accompanied this code. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ package org.zero01.monitor_tuning.btrace; import com.sun.btrace.BTraceUtils; import com.sun.btrace.annotations.BTrace; import com.sun.btrace.annotations.Kind; import com.sun.btrace.annotations.Location; import com.sun.btrace.annotations.OnMethod; import com.sun.btrace.annotations.Self; import com.sun.btrace.annotations.TLS; @BTrace public class PrintOnThrow { // store current exception in a thread local // variable (@TLS annotation). Note that we can't // store it in a global variable! @TLS static Throwable currentException; // introduce probe into every constructor of java.lang.Throwable // class and store "this" in the thread local variable. @OnMethod( clazz="java.lang.Throwable", method="<init>" ) public static void onthrow(@Self Throwable self) { // @Self其实就是拦截了this //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 onthrow1(@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 onthrow2(@Self Throwable self, Throwable cause) { //new Throwable(Throwable cause) currentException = self; } // when any constructor of java.lang.Throwable returns // print the currentException's stack trace. @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; } } }
在命令行里运行该脚本,访问相应的接口后,输出的异常堆栈如下:
可以看到,即便异常被隐藏了起来,而我们通过btrace脚本一样能把异常重新给揪出来,这样即便是线上正在运行的项目,我们也能够不关闭、不重启服务,就能够监控到项目里是否有发生异常。
我们还可以使用BTrace拦截某一行代码,以此判断该行代码是否有被执行。在btrace包下新建一个BTrace脚本,代码如下:
package org.zero01.monitor_tuning.btrace; import com.sun.btrace.BTraceUtils; import com.sun.btrace.annotations.BTrace; import com.sun.btrace.annotations.Kind; import com.sun.btrace.annotations.Location; import com.sun.btrace.annotations.OnMethod; import com.sun.btrace.annotations.ProbeClassName; import com.sun.btrace.annotations.ProbeMethodName; @BTrace public class PrintLine { @OnMethod( clazz="org.zero01.monitor_tuning.controller.BTraceController", method="exception", location=@Location(value=Kind.LINE, line=43) // 拦截第43行 ) public static void anyRead(@ProbeClassName String pcn, @ProbeMethodName String pmn, int line) { BTraceUtils.println("ClassName: " + pcn); BTraceUtils.println("MethodName: " + pmn); BTraceUtils.println("line: " + line); BTraceUtils.println(); } }
在命令行里运行该脚本,访问相应的接口后,输出的信息如下:
E:/Java_IDEA/monitor_tuning/src/main/java/org/zero01/monitor_tuning/btrace>btrace 14952 PrintLine.java ClassName: org.zero01.monitor_tuning.controller.BTraceController MethodName: exception line: 43
如果没有任何输出的话,就代表那一行没有被执行到,所以没被拦截。这种拦截某一行的方式,不适用于判断是否有异常,只能单纯用于判断某一行是否被执行了。
在以上小节中,我们已经知道了如何拦截简单的参入,本小节中,将介绍如何拦截复杂参数、环境变量以及使用正则匹配拦截。
拦截复杂参数就是拦截实体对象类型的参数,在 BTraceController 类里,增加如下方法:
@RequestMapping("/arg2") public User arg2(User user) { return user; }
使用BTrace拦截复杂参数,需要使用反射的方式进行拦截,也就是需要传递包名+属性名。在btrace包下新建一个BTrace脚本,代码如下:
package org.zero01.monitor_tuning.btrace; import java.lang.reflect.Field; import com.sun.btrace.BTraceUtils; import com.sun.btrace.annotations.BTrace; import com.sun.btrace.annotations.Kind; import com.sun.btrace.annotations.Location; import com.sun.btrace.annotations.OnMethod; import com.sun.btrace.annotations.ProbeClassName; import com.sun.btrace.annotations.ProbeMethodName; import org.zero01.monitor_tuning.vo.User; @BTrace public class PrintArgComplex { @OnMethod( clazz = "org.zero01.monitor_tuning.controller.BTraceController", method = "arg2", location = @Location(Kind.ENTRY) ) public static void anyRead(@ProbeClassName String pcn, @ProbeMethodName String pmn, User user) { //print all fields BTraceUtils.print("print all fields: "); BTraceUtils.printFields(user); //print one field Field oneFiled = BTraceUtils.field("org.zero01.monitor_tuning.vo.User", "name"); BTraceUtils.println("print one field: " + BTraceUtils.get(oneFiled, user)); BTraceUtils.println("ClassName: " + pcn); BTraceUtils.println("MethodName: " + pmn); BTraceUtils.println(); } }
在命令行里运行该脚本,访问相应的接口后,输出的信息如下:
E:/Java_IDEA/monitor_tuning/src/main/java/org/zero01/monitor_tuning/btrace>btrace -cp "E:/Java_IDEA/monitor_tuning/target/classes" 13336 PrintArgComplex.java print all fields: {id=0, name=小明, } print one field: 小明 ClassName: org.zero01.monitor_tuning.controller.BTraceController MethodName: arg2
注:这里使用到了一个 -cp 参数,该参数表示指定一个classpath路径
其实我们在编写BTrace脚本时,是可以使用正则表达式匹配类名和方法名的,并非必须要指定一个完整的名称。如下示例:
package org.zero01.monitor_tuning.btrace; import com.sun.btrace.BTraceUtils; import com.sun.btrace.annotations.BTrace; import com.sun.btrace.annotations.OnMethod; import com.sun.btrace.annotations.ProbeClassName; import com.sun.btrace.annotations.ProbeMethodName; @BTrace public class PrintRegex { @OnMethod( // 类名也可以使用正则表达式进行匹配 clazz = "org.zero01.monitor_tuning.controller.BTraceController", // 正则表达式需要写在两个斜杠内 method = "/.*/" ) public static void anyRead(@ProbeClassName String pcn, @ProbeMethodName String pmn) { BTraceUtils.println("ClassName: " + pcn); BTraceUtils.println("MethodName: " + pmn); BTraceUtils.println(); } }
通过编写BTrace脚本我们可以打印出JVM的信息以及环境变量等,就类似于jinfo命令一样。代码示例:
package org.zero01.monitor_tuning.btrace; import com.sun.btrace.BTraceUtils; import com.sun.btrace.annotations.BTrace; @BTrace public class PrintJinfo { static { // 打印系统属性 BTraceUtils.println("System Properties:"); BTraceUtils.printProperties(); // 打印JVM参数 BTraceUtils.println("VM Flags:"); BTraceUtils.printVmArguments(); // 打印环境变量 BTraceUtils.println("OS Enviroment:"); BTraceUtils.printEnv(); // 退出脚本 BTraceUtils.exit(0); } }
在命令行里运行该脚本,输出的信息如下:
在以上小节中,我们介绍了BTrace的常见用法,也编写了相应的demo进行一一演示。但是还有一些需要注意事项,在此说明一下: