搭建IDEA Btrace环境,并进行SpringBoot工程动态跟踪

简介

    在学习周志明老师的深入理解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程序。可惜了...

你可能感兴趣的:(JVM)