Java 生产神器 BTrace

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u010695794/article/details/89529431

本文首发于个人微信公众号《andyqian》,期待你的关注!

前言

在《说说Java单元测试》文章中,强调了单元测试的重要性,也提倡大家一定要写单元测试,能帮我们筛选掉很多低级错误,找出一些没必要的bug,避免生产事故。单元测试通过后,我们开始集成,随着服务集成的日渐增多,业务逻辑也变得越来越复杂,在这样的前提下,解决bug就变得异常复杂。在本地环境中,我们可以通过日志分析 + debug的方式,进行排查解决,再不济,我们还可以开启远程调试进行解决。但当生产系统上出现了bug时,我们可不能开启远程调试,这样会造成线程阻塞,足以让生产系统爆炸的。那么,我们该怎么办呢?代码执行到哪一行了?我们两眼一抹黑,不知从何下手,真是一个悲伤的故事。当然了,我们也可以通过修改代码的方式新增日志,打印方法的入参,出参,上线,重启 ... 直至问题得以解决。办法虽然原始,好在能解决问题,但其过程非常痛苦,让人精疲力尽,有很多朋友估计都有过这样的经历。现在回过头来想,如果有这么一个工具,在不修改代码,不重启应用,不上线的前提下,就能查看到代码执行到哪一行,查看某个方法的出参,入参,查看方法的耗时,是该有多好。今天要介绍的就是这么一个工具 - BTrace ( Github主页:https://github.com/btraceio/btrace)

简介

BTrace 是一个安全,动态的Java追踪工具。通过动态(字节码),动态替换的原理以追踪正在运行的Java程序。简单的说,我们可以使用它在不修改代码,不重启应用,不上线的前提下,查看指定方法的运行环境,如:方法出入参,运行环境,方法耗时等运行时环境。正因为 BTrace 是动态追踪,尽量避免影响线上服务的可用性,在编写BTrace 脚本时,也有如下约定:

  1. 不能创建对象,数组。
  2. 不能throw,catch 异常。
  3. 不能有循环(for,while,do..while)。
  4. 不能实现接口。
  5. 不能有同步块及同步方法。
  6. 不能有断言语句。
  7. 不能有外部,内部,嵌套 或本地类。
  8. 不能进行任何实例好或静态方法调用,只能从com.sun.btrace.BtraceUtils类的公共静态方法。 ....

别看限制这么多,其实我们可操作的比这更多,其使用语法也非常简单,如下所示:

btrace <pid> <btrace-script>

其中 pid 是 需要追踪的 Java 进程 ID (可以通过 jps 命令查看) ,btrace-script 脚本是我们需要编写的追踪文件,语法会在下面详细介绍。

环境配置

1. 执行btrace环境

在使用之前,我们需要下载并安装,像设置JDK环境一样,设置Btrace环境即可。

以Linux为例:

  1. 下载 btrace:
wget https://github.com/btraceio/btrace/releases/download/v1.3.11.3/btrace-bin-1.3.11.3.tgz
  1. 设置环境,编辑 /etc/profile文件,向其添加如下内容,(其中 /usr/local/btrace/为 btrace 的安装路径)。
export BTRACE_HOME=/usr/local/btrace/;
export PATH=$PATH:$BTRACE_HOME;
  1. 使环境变量生效。
source /etc/profile;
  1. 设置完成后,可以使用btrace --version 命令验证其有效性。

2. BTrace 脚本编写环境

比较遗憾的是在 mvnrepository 仓库中,btrace是非常老的版本,通常建议通过Github下载其最新jar包上传至私服中使用,也可以引用本地进行使用,如下所示:

  1. 在 pom.xml 文件中的 properties 标签中,添加如下内容:
<btrace.home>/java/jar/btrace</btrace.home>

其中 /java/jar/btrace/为 btrace的本地路径,可修改为实际路径。

  1. 在 dependencies 标签下,添加如下依赖:
<!--btrace start-->
        <dependency>
            <groupId>com.sun.tools.btrace</groupId>
            <artifactId>btrace-agent</artifactId>
            <version>1.3.11.3</version>
            <scope>system</scope>
            <systemPath>${btrace.home}/build/btrace-agent.jar</systemPath>
        </dependency>
        <dependency>
            <groupId>com.sun.tools.btrace</groupId>
            <artifactId>btrace-boot</artifactId>
            <version>1.3.11.3</version>
            <scope>system</scope>
            <systemPath>${btrace.home}/build/btrace-boot.jar</systemPath>
        </dependency>
        <dependency>
            <groupId>com.sun.tools.btrace</groupId>
            <artifactId>btrace-client</artifactId>
            <version>1.3.11.3</version>
            <scope>system</scope>
            <systemPath>${btrace.home}/build/btrace-client.jar</systemPath>
        </dependency>
        <!--btrace end-->

也可以通过Maven plugin 插件的形式运行,这里不再演示,有兴趣的可以在其Github主页上查看方法。

编写 BTrace脚本

通过上面的一顿操作,是时候表演真正的技术了,下面以运行一段程序为例:

@POST
    @Path("/load")
    @Override
    public UserDTO loadUserInfo() {
        return getUserInfo("www.andyqian.com","andyqian","andyqian");
    }

    /**
     * 获取用户信息
     * @param blog             博客地址
     * @param name             andyqian
     * @param officialAccount  公众号
     * @return user DTO
     */
    private UserDTO getUserInfo(String blog,String name,String officialAccount){
        UserDTO userDTO = new UserDTO();
        userDTO.setBlog(blog);
        userDTO.setName(name);
        userDTO.setOfficialAccount(officialAccount);
        // 该段代码纯粹用于增加方法耗时, 无其他任何意义。
        try {
            Thread.sleep(1000);
        }catch (InterruptedException te){
            te.printStackTrace();
        }
        return userDTO;
    }

BTrace 脚本文件:

@BTrace
public class BtraceTest {

    /**
     * 打印用户信息
     */
    @OnMethod(clazz="com.jq.wechat.facade.srv.impl.AuthRestServiceImpl", method="getUserInfo",location=@Location(value=Kind.RETURN))
    public static void printUserInfo(@Duration long duration,String blog, String name, String officialAccount){
        //
        BTraceUtils.print("====method duration: "+duration/1000000+" 毫秒=== ");
        BTraceUtils.println("blog:"+blog+" name:"+name+" account: "+ officialAccount);
        //1. 建议最后一行加上这个,(因为在测试过程中,最后一行未能显示)
        BTraceUtils.println();
    }
}

执行脚本结果后,我们请求接口多次,其脚本追踪结果如下:

andy@andyqian:/java/andyqian/wechat$ btrace 18596 BtraceTest.java 
====method duration: 1000 毫秒=== blog:www.andyqian.com name:andyqian account: andyqian
====method duration: 1000 毫秒=== blog:www.andyqian.com name:andyqian account: andyqian
====method duration: 1000 毫秒=== blog:www.andyqian.com name:andyqian account: andyqian

备注:其中 18596 是我系统演示时的Java进程ID。你使用时,请使用 jps -l命令查看自己的进程ID。

当参数是对象,我们可以使 BTraceUtils.printFields()方法打印对象属性,也可以使用BTraceUtils.getInt(Field field, Object obj)打印对象中的指定Int类型属性。


写到这里,篇幅不知不觉已经很长了,上面介绍了BTrace的最基本用法,其实BTrace还是有很多用法值得我们掌握的,我们放在下一篇来介绍。


相关阅读:

你该了解的Java注释

说说单元测试

一个Java细节

初探JDK源码之字符集


 

                                                                                       扫码关注,一起进步

                                                                        个人博客: http://www.andyqian.com
 

猜你喜欢

转载自blog.csdn.net/u010695794/article/details/89529431