在学习周志明老师的深入理解JVM虚拟机一书第四章时,对Btrace这个神奇的插件产生了兴趣。因为以前经常遇到,在某些情况下,比如生产环境出现问题,但是不能重启,又没有打日志,或者有些很奇怪的问题偶尔出现,重启应用又不出现的问题。如果能把这个Btrace工具应用起来,在某些束手无策的时候,可能有所裨益。
Btrace是什么?按照周老师的描述,在不停止目标程序运行的情况下,通过HotSpot虚拟机的HotSwap技术动态加入原本并不存在的调试代码。从这段话,我们可以得到以下信息,1.使用Btrace可以在不停止程序的情况下进行调试。2.应用于HotSpot虚拟机,如果是其他虚拟机,是不能用的。
然后我按照书上所载进行测试,完美通过,初步体验到了神奇之处。然后便思考如何应用于实际,比如Springboot中,继而开始了漫长踩坑之路。
环境
VM虚拟机,CentOS7,jdk1.8.0_151,一个SpringBoot的demo应用(用于打包测试),IDEA,WIN10, MAVEN,btrace安装包
btrace的安装包:https://github.com/btraceio/btrace/releases
(可选)btrace的源码包(下载地址:https://github.com/btraceio/btrace ,由于这里我是自己重新编译的包,所以下载的源码包,不想重新编译的童鞋可以尝试下载 release中编译的包),另外注意:如果想自己编译源码包,必须能够访问国外的网站,并且编译过程中会不断重试,原因都懂的。编译不是必须的,编译得到的包仅仅是用于引入IDEA中编辑脚本的时候用,执行的时候会用btrace自身的包执行。因此如果受困于网络情况的,可以不用编译,直接在https://github.com/btraceio/btrace 的releases中下载一个需要的版本,然后在maven中找到如下3个jar包(没有在center仓库),引入maven工程,编写脚本即可。
btrace
https://dl.bintray.com/btraceio/maven/
btrace2
http://jcenter.bintray.com/
com.sun.tools.btrace
btrace-boot
1.3.11.3
com.sun.tools.btrace
btrace-agent
1.3.11.3
com.sun.tools.btrace
btrace-client
1.3.11.3
1.首先需要安装btrace,在连接https://github.com/btraceio/btrace/releases 中下载一个需要的版本上传到服务器,配置好环境变量。btrace需要JAVA环境,因此Java的环境也需要配置好。如果要自己编译包,可按如下第2步开始,如果不,配置好pom,直接跳到第八步。
2. 进行编译。上传源码包,进入bin目录,运行编译命令,
./gradlew build
如果不能访问国外网站,会得到一个什么org.xxx.xxx connecte refuse 之类的错误。如果实在无法访问外网,就只有下载release里的包试试,或者下载我编译的包(最后分享链接地址)。
如果能没有什么连接拒绝的错误,那有可能得到这么一个错误:build javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorE , 错误的原因是HTTPS的证书不对。解决办法是
在浏览器里面访问一下那个地址,如果能连接上,浏览器会提示是否信任证书,点击信任。然后将证书导出(证书导出来是个.cer的文件,如果导出的证书没有那个类型的选项比如火狐,就换成谷歌浏览器,因为火狐那个证书试过没有用不知道为啥,如果不知道如何导出,可以参考https://blog.csdn.net/mzh1992/article/details/53887321)。
3.导入证书。
上传证书到CentOS,进行导入
keytool -import -alias LL1 -keystore /opt/jdk1.8.0_151/jre/lib/security/cacerts -file /root/LL1.cer
根据实际情况修改jdk路径
-file 指定自己的证书位置
需要输入密码changeit
4.证书导入成功后,重新
./gradlew build
顺利的话,就可以自动下载依赖的包进行编译了,因为是国外网络,可能会频繁出现什么jar找不到之类的,不断输入上面的命令重试就行了。
5.编译成功后,会在源码路径下生成一个build文件夹,此文件夹下的libs文件夹就是编译得到的jar包,如下:
6.将此jar包从CentOS虚拟机上拿下来,放到windows开发环境上,使用maven命令,安装到本地仓库。
mvn install:install-file -Dfile=e:\btrace-1.3.11.3.jar -DgroupId=com.sun.btrace -DartifactId=btrace -Dversion=1.3.11.3 -Dpackaging=jar
7.安装成功后,就可以在IDEA中创建一个maven工程,并且加入刚刚安装的jar包的依赖。
com.sun.btrace
btrace
1.3.11.3
然后,就可以在IDEA中编写btrace脚本。
8.关于如何编写btrace脚本,首先查看文档,源码包中有相关docs。这里说几个注意的点:
1.脚本中最好不要include出了Btrace,JDK以外的类,因为脚本是要编译的,如果include了第三方类,比如自己写的AbcServiceImpl,xxxBean,那么在编译的时候就要提供这些类,或者指定他们的位置,不然妥妥的classnotfound。如果要指定,使用-cp参数, 但是貌似不能指定多个目录,我试了反正一直报错。(查看btrace可以带什么参数,可以直接敲btrace,可以看到提示,注意将它加入环境变量)
2.如果拦截的方法的返回值,就是自定义类型,怎么办?用Btrace的AnyType代替。下面会提供几个我测试的demo,全部测试通过,可以参考。
9。按照官网的说法,如果脚本有误,有可能造成目标程序JVM崩溃,所以生产环境谨慎!!!使用之前,务必本地测试脚本无误!
10.将springboot的demo程序打成jar包,上传到CentOS,通过java -jar命令运行。
11.将编写好的btrace脚本上传的CentOS,首先使用jps 命令,找到运行的springboot的pid,然后使用如下命令启动btrace脚本。
btrace pid 脚本名 , 例如:
btrace 22225 BtraceDemo4.java
12.访问浏览器springboot的demo程序,触发到监控的那个方法执行,观察btrace是否拦截到了所需信息。
13. Windows下使用这个东西有点问题,总是提示设置BTRACE_HOME,但是设置了也还是提示,原因未知。
14.btrace默认有许多限制,如果想突破限制,在脚本中添加注解@BTrace(trusted = true)(见下面例子),然后命令行添加 -u 参数。
btrace -u 22225 BtraceDemo4.java
15.如果脚本报错:Port 2020 unavailable,这个错误的原因是,此端口已经被使用,有可能是在其他JVM实例上使用了btrace,就像在一个启动2个8080端口的Tomcat一样。可以通过-p参数指定一个端口,如:
btrace -u -p 2021 22225 BtraceDemo4.java
16.下面附上几个测试DEMO。更深的用法,更强大的功能待后续有时间了解。
package com.my.scripts;
import com.sun.btrace.AnyType;
import com.sun.btrace.BTraceUtils;
import com.sun.btrace.annotations.*;
import java.lang.reflect.Field;
import static com.sun.btrace.BTraceUtils.*;
/**
* Created by lpy on 2019/1/23
*/
@BTrace
public class BtraceDemo1 {
@OnMethod(clazz = "com.my.dao.DepartmentDao",
method = "getDepartment",
location = @Location(Kind.RETURN))
public static void func(@Self Object self,Integer id, @Return AnyType result) {
println("============================");
//打印方法参数
println(strcat("id:", str(id)));
BTraceUtils.print("attribute:");
//用此方法打印bean的所有属性
BTraceUtils.printFields(result);
BTraceUtils.print("departmentName:");
//打印实体类的某个属性
Field departmentName = BTraceUtils.field("com.my.entities.Department", "departmentName");
Object name = BTraceUtils.get(departmentName, result);
BTraceUtils.println(name);
//打印实体类,注意这里打印出来的是类似于result:com.my.entities.Department@195fb071的结果,即使实体类重写了
//toString方法,也不会打印,要想打印所有的属性,用BTraceUtils.printFields(result);方法,或者使用强力模式,手动调用String.valueOf方法。
println(strcat("result:", str(result)));
//这一行必须添加,因为由于btrace缓冲区的缘故,最后一行显示不出来,为了不影响查看结果,所以加一行这个
//保证想看的结果完全显示
println("============================");
}
}
package com.my.scripts;
import com.sun.btrace.AnyType;
import com.sun.btrace.BTraceUtils;
import com.sun.btrace.annotations.*;
import java.lang.reflect.Field;
import java.util.Map;
import static com.sun.btrace.BTraceUtils.*;
/**
* Created by lpy on 2019/1/23
*/
@BTrace
public class BtraceDemo2 {
@OnMethod(clazz = "com.my.dao.DepartmentDao",
method = "getDepHashMap",
location = @Location(Kind.RETURN))
public static void func(@Self Object self, @Return AnyType result) {
println("============================");
//打印HashMap
BTraceUtils.println(result);
//这一行必须添加,因为由于btrace缓冲区的缘故,最后一行显示不出来,为了不影响查看结果,所以加一行这个
//保证想看的结果完全显示
println("============================");
}
}
package com.my.scripts;
import com.sun.btrace.AnyType;
import com.sun.btrace.BTraceUtils;
import com.sun.btrace.annotations.*;
import static com.sun.btrace.BTraceUtils.println;
/**
* Created by lpy on 2019/1/23
*/
@BTrace(trusted = true)
public class BtraceDemo4 {
@OnMethod(clazz = "com.my.dao.DepartmentDao",
method = "btraceString",
location = @Location(Kind.RETURN))
public static void func(@Self Object self, @Return AnyType result) {
println("============================");
//打印String的返回值
BTraceUtils.println(result);
//如果调用了非BTraceUtils的方法,需要在上面@BTrace加上trusted ,并且在btrace执行脚本的时候
//添上-u 参数,具体有哪些参数可以,可以直接敲btrace 查看提示
BTraceUtils.println(String.valueOf(result));
//这一行必须添加,因为由于btrace缓冲区的缘故,最后一行显示不出来,为了不影响查看结果,所以加一行这个
//保证想看的结果完全显示
BTraceUtils.println("============================");
}
}
package com.my.scripts;
import com.sun.btrace.AnyType;
import com.sun.btrace.BTraceUtils;
import com.sun.btrace.annotations.*;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import static com.sun.btrace.BTraceUtils.*;
/**
* Created by lpy on 2019/1/23
*/
@BTrace(trusted = true)
public class BtraceDemo7 {
@OnMethod(clazz = "com.sefonsoft.miner.spark.UnaryFunctionComponent",
method = "process",
location = @Location(Kind.RETURN))
public static void func(@Self Object self, @ProbeClassName String pcn, @ProbeMethodName String pmn,
AnyType datas, AnyType paramPairs,
@Return AnyType result) {
//注意:如果方法写成这样,那么被拦截的目标方法的所有参数列表都会被放在AnyType[] datas里面。
//一般方法就3种写法,要么就和目标拦截方法的参数列表一模一样,连类型都一样;要么使用上面的AnyType;要么使用AnyType[]
//public static void func(@Self Object self, AnyType[] datas, @Return AnyType result) {
//打印方法执行栈
//jstack();
println("==============" + pcn + ":" + pmn + "==============");
println("datas:");
//如果能明确知道目标参数是数组类型,使用此方式打印数组的值
//BTraceUtils.printArray(datas);
//如果该数组是用其他类型来接收的,比如这里的AnyType,使用如下方法打印数组的值
boolean array = datas.getClass().isArray();
if(array){
int length = Array.getLength(datas);
for(int i=0 ;i < length; i++){
Object obj = Array.get(datas, i);
//第一种打印方法:打印对象属性,注意如果对象中还有封装的对象,该对象只能打出对象地址,打不出值
//BTraceUtils.printFields(obj);
//第二种打印方法:调用目标对象的toString方法
println(String.valueOf(obj));
//第三种打印方法:指定打印的对象的某个属性
/*
Field departmentName = BTraceUtils.field("com.miner.api.data.PortData", "value");
Object name = BTraceUtils.get(departmentName, obj);
BTraceUtils.println(name);*/
}
}
//这一行必须添加,因为由于btrace缓冲区的缘故,最后一行显示不出来,为了不影响查看结果,所以加一行这个
//保证想看的结果完全显示
println("============================");
}
}
package com.my.scripts;
import com.sun.btrace.AnyType;
import com.sun.btrace.annotations.*;
import java.lang.reflect.Array;
import static com.sun.btrace.BTraceUtils.*;
/**
* Created by lpy on 2019/1/23
*/
@BTrace(trusted = true)
public class BtraceDemo8 {
@OnMethod(clazz = "com.sefonsoft.miner.spark.UnaryFunctionComponent",
method = "previewOutputColumns",
location = @Location(Kind.RETURN))
public static void func(@Self Object self, @ProbeClassName String pcn, @ProbeMethodName String pmn, @Duration long duration,
AnyType datas, AnyType paramPairs,
@Return AnyType result) {
//注意:如果方法写成下面这样,那么被拦截的目标方法的所有参数列表都会被放在AnyType[] datas里面。
//一般方法就3种写法,要么就和目标拦截方法的参数列表一模一样,连类型都一样;要么使用上面的AnyType;要么使用AnyType[]
//public static void func(@Self Object self, AnyType[] datas, @Return AnyType result) {
//打印方法执行栈
//jstack();
//打印监控的类名,方法名
println("==============" + pcn + ":" + pmn + "==============");
//打印被监控的self的大小
println("size of:" + self + "=" + sizeof(self));
//打印方法执行的时间
println("duration(millis):" + duration/1000000);
println("datas:");
//如果能明确知道目标参数是数组类型,使用此方式打印数组的值
//BTraceUtils.printArray(datas);
//如果该数组是用其他类型来接收的,比如这里的AnyType,使用如下方法打印数组的值
boolean array = datas.getClass().isArray();
if(array){
int length = Array.getLength(datas);
for(int i=0 ;i < length; i++){
Object obj = Array.get(datas, i);
//第一种打印方法:打印对象属性,注意如果对象中还有封装的对象,该对象只能打出对象地址,打不出值
//BTraceUtils.printFields(obj);
//第二种打印方法:调用目标对象的toString方法
println(String.valueOf(obj));
//第三种打印方法:指定打印的对象的某个属性
/*
Field departmentName = BTraceUtils.field("com.miner.api.data.PortData", "value");
Object name = BTraceUtils.get(departmentName, obj);
BTraceUtils.println(name);*/
}
}
//这一行必须添加,因为由于btrace缓冲区的缘故,最后一行显示不出来,为了不影响查看结果,所以加一行这个
//保证想看的结果完全显示
println("============================");
}
}
可以参考的连接:
手动导入证书: https://blog.csdn.net/u012934723/article/details/78233388
编译过程中,必须能够访问这个地址,可以浏览器中试试看能不能访问,访问不了那么就 没法编译:https://services.gradle.org/distributions/
btrace使用小结:https://www.jianshu.com/p/ee6b5c13c45b
springboot结合btrace:https://blog.csdn.net/wangshuaiwsws95/article/details/86517411#commentBox
visualVm插件中心: https://visualvm.github.io/pluginscenters.html
maven手动install jar包:https://www.cnblogs.com/Kubility123/p/5666671.html
另:
JDK自带的 jvisualvm 工具可以安装btrace插件,但是第一,最好 别用这个插件去监控含有自己添加的btrace包的工程,因为两者的包版本很可能不一致,会报错。
第二是这个工具只能btrace追踪本地的Java程序,不能追踪的远程的服务器上的Java程序。可惜了...