Java-debug-tool是一种支持方法级别动态追踪的技术工具,它具备动态获取方法执行视图的能力,提供了非常丰富的调试信息以供开发人员对问题进行快速排查;Java-debug-tool命令具有非常高的聚合性,使用一个命令可以获取到的信息非常多;如何记住命令参数是一个亟待解决的问题,下面将对每个命令从基础到高级用法进行详细介绍,以期能使开发人员可以灵活使用Java-debug-tool来进行问题排查。
findClass 命令用来在目标JVM中查找一个类的信息,这是本文介绍的命令中最简单最直接的一个命令,它的用法很简单,指定一个类名即可实现信息获取,当然,简单的命令却可以获取到非常重要的信息,下面罗列出了该命令可以获取到的类的相关信息。
- 类是否已经被加载;
- 加载类的类加载器;
- 类来源的资源包(jar包);
- 类包含的方法列表;
- 类包含的内部类列表;
下面是几种使用该命令的样式:
fc -class java.lang.String
fc -class String
fc -r java.lang.*
fc -class String -l 2
这个命令非常简单,可以在处理一些类冲突等问题的时候使用,这个简单的命令还会和获取方法运行时视图的命令息息相关,这一点后续再谈,下面是一张运行该命令的展示图:
这是Java-debug-tool中最为复杂,也是最为有用的命令,这个命令的特点是使用起来非常简单,但是想要发挥它真正的威力,或者是排查一些复杂问题的场景下还是需要学习一下,下面就从最简单的用法开始,逐步深入的介绍这个命令的精华所在。 在此之前,先简单描述一下该命令能输出一些什么样的调试信息,或者说,Java-debug-tool获取到的方法执行视图具体是什么样子的:
方法执行视图可以获取到的信息如下:
- 正在调试的方法信息(类.方法 调用线程);
- 方法调用堆栈;
- 调试耗时,包括对目标JVM造成的STW时间;
- 方法入参,包括入参的类型及参数值;
- 方法的执行路径;
- 代码执行耗时;
- 局部变量信息;
- 方法返回结果;
- 方法抛出的异常;
- 对象字段值快照;
下面,就由浅入深的介绍一下该命令的具体使用方法:
好吧,这是最简单的命令使用方法,也是开启动态获取方法执行视图旅行的开始:
mt -c io.javadebug.agent.Agent -m loadField
-c参数指定需要观察的类(全限定名),-m参数指定需要观察的具体的方法,这样就可以完成一次方法执行视图的获取。 当然,某些情况下可能会出现意外,更为复杂的场景先不考虑,一个比较明显的意外就是当目标方法重载了该怎么办?这个时候Java-debug-tool无法确定需要获取哪一个方法的执行视图,本次调试只能以失败告终,这个时候,上面介绍的fc命令就派上用场了,是否还记得fc命令可以获取到类的方法列表,看下面标红的信息(每个方法下面都会有),这个信息是方法的“签名”信息,每个方法都独一无二,当方法重载的情况下,只需要将方法”签名“指定为-d参数,即可顺利完成命令调试。
就像这样:
mt -c io.javadebug.agent.Agent -m loadField -d (Ljava/lang/String;)[B
这种入门级的命令使用下,Java-debug-tool是无法确定你的调试意图的,所以只要方法被调用了,就会结束调试,将获取到的方法执行视图返回给你,所以,你需要掌握更多的关于该命令的技能,才能向Java-debug-tool表达你明确的调试意图。
这其实也不算一种明确的调试意图,只是告诉Java-debug-tool起码我需要的是一次方法正常返回值的执行视图,而那些抛出异常的方法执行视图需要过滤掉。-t参数用于指定你的调试意图,这种”方法正常返回结果“的调试意图形象的称为"return":
mt -c io.javadebug.agent.Agent -m loadField -t return
看起来也比较简单,没有特别难理解的地方,虽然简单,却是你使用Java-debug-tool发出的第一个具备自主调试意图的命令,你主动告诉Java-debug-tool,我需要一次方法正常退出的执行视图,如果你觉得这种体验非常奇妙,那么接下来的命令使用将打开你新世界的大门。
与”方法正常退出“相比,”方法抛出异常“貌似更符合我们进行问题排查的场景描述,更具有针对性,毕竟,方法大多数情况下都是正常退出的,而抛出异常则认为方法无法处理(主动抛出异常),或者遇到了运行时异常(被动抛出),抛出异常的频率应该是极低的甚至是需要避免的,因而获取一次”方法抛出异常“的方法执行视图貌似更有价值,下面是向Java-debug-tool发出这种调试意图的命令样式:
mt -c io.javadebug.agent.Agent -m loadField -t throw
看起来除了-t变成了"throw"之外,和”方法正常退出“没有什么区别,确实如此,Java-debug-tool期望使用极简的命令实现复杂的功能,这种简洁对于快速排查问题是有必要的。
上面提到,方法抛出异常的原因有多种多样,不仅如此,抛出的异常类型也是多种多样的,在一些极端情况下,该死的方法频繁的抛出了多种不同的异常,搞得每次使用"throw"都无法获取到抛出某种类型的异常的方法执行视图,真是伤脑筋,还好Java-debug-tool考虑到了这种情况,在上一小节的基础上,只需要再指定一个参数-e即可实现这种获取”抛出指定异常“的方法执行视图,就像这样:
mt -c io.javadebug.agent.Agent -m loadField -t throw -e java.lang.NullPointerException
就像这个命令描述的一样,我想要获取一次抛出了”java.lang.NullPointerException“这种异常的方法执行视图,这样一看,我们的调试意图已经非常明确了。
有时候我们需要收集多种情况的方法调用,对比分析得出结论,这种情况下”record“模式就派上用场了,当然该模式的用处不仅如此,后面再细说。”record“模式和它的名字一样,就是用来记录方法调用的,它的工作就是根据参数来记录方法执行视图,以供后面进行分析,目前分析这里还没有任何功能支持(比如对比多次方法调用视图),只能通过开发者自己进行分析。该命令首先需要记录执行视图,之后才能打开记录的视图进行分析,下面是记录方法执行视图的命令样式:
mt -c io.javadebug.agent.Agent -m loadField -t record -n 10 -time 5
-n参数可以指定需要录制的视图数量,-time参数指定需要执行录制的时间限制,只要其中一个条件满足,命令就会停止工作。上面这条命令的含义是说,对loadFile方法录制最多10次视图,执行时间限制在5秒,当然,-n参数和-time参数都是可选的,默认最多录制10条,最多录制10秒。录制完成之后,就可以对这些方法执行视图进行分析了,下面是挑选某一个视图的命令样式:
mt -c io.javadebug.agent.Agent -m loadField -t record -u 0
使用-u参数指定一个数字,这个数字就代表视图在存储时的数组下标,上面这条命令的含义是说,将刚才录制好的第0号视图调出来我分析一下,录制的流量会长期有效,这一点需要特别注意。
就像上面说到的一样,大多数情况下,方法是正常退出的,问题就是虽然方法正常退出了,但是结果却不符合我们的预期,当然,这种时候使用上面提到的最为基础的命令即可进行一些问题排查,但是方法执行的路径和方法的入参强关联在一起,所以在调试的时候,特别希望能指定具体的参数来获取方法视图,这种场景Java-debug-tool描述为观察:”watch“,它的含义是等待某种事情发生的时候进行回调,更进一步,我们希望能获取到特定参数的方法执行视图。这种场景有点小复杂,但是不至于特别复杂,除了-t参数指定为”watch“之外,我们需要做的就是写一个Spring表达式,并通过-i参数告诉Java-debug-tool,这个表达式用来匹配参数,关于Spring表达式相关的技术,可以参考Spring Expression Language。不要太紧张,我们只需要非常少的表达式的知识即可实现我们筛选参数的目标。下面,我们就来实战一下:
mt -c io.javadebug.agent.Agent -m loadField -t watch -i #p0!=null&&#p1.length>0&&p2.getA().equals("Java-debug-tool")
Java-debug-tool已经将目标方法的参数列表名称编码处理,选定参数使用p开头,后面拼接上参数在参数列表中的顺序id(从0开始);比如p0选择的是方法的第一个参数,p2则是第三个参数,以此类推。 例子中的表达式的含义是:匹配这样一个方法调用,这个方法调用时,第一个参数不为null,并且第二个参数的length属性大于0(猜测是数组的长度大于0),并且第三个参数的a属性值为"Java-debug-tool"。 到此,我们貌似掌握了Java-debug-tool所有精华的部分,看起来到目前为止我们还没有遇到一个需要非常复杂的输入才能进行调试的场景,下面就来填补一下这个空缺,来点有意思的。
考虑一个场景,有一种参数执行完方法退出时不符合我们的预期,我们应该怎么使用Java-debug-tool来进行动态调试呢?"watch",这是目前为止大家能够想到的最为有力的方法,但是困难总是非常多的,恰好这种符合要求的参数执行的频率非常低,所以往往我们执行了很多次"watch"之后依然没有拿到结果,而是得到一个"命令执行超时"的可恶响应,这种情况下文会提到一种解决方案,本小节介绍一个更厉害的方法,你可以让方法按照你的预期参数执行一次,并且是立即执行。 这就有点意思了,和"watch"不一样的是,你可以得到一种”立即执行“的响应,不需要苦苦的等待甚至得到诸多”命令执行超时“的下场,这种模式称之为"custom",和名字一样,它是你自己私人定制的,这种场景稍微有点复杂,可以说是Java-debug-tool所有命令场景中最为复杂的,所以下面花点篇幅详细介绍一下,当然,Java-debug-tool很希望你永远用不到这个功能。
- 第一种获取方法”立即执行“效果的方法简单粗暴,它需要你将方法的参数提前进行编码,并将编码好的参数列表告诉Java-debug-tool,Java-debug-tool拿到参数之后就会在自己的线程内部发起一次方法调用,你需要清楚的知道,这种”立即执行“的代价很高,需要你充分考虑自己的调试场景,判断是否需要这么大张旗鼓的弄。参数列表编码的方法可以参考”io.javadebug.core.utils.JacksonUtils“这个类中的serialize方法,下面是一个例子:
mt -c io.javadebug.agent.Agent -m loadField -t custom -i encoded-params
- 第二种方法则有趣得多,当然也具备一定的操作难度,上文中提到了"record"这种模式的调试技术,它可以记录一些方法执行数据,然后慢慢分析这些方法执行视图来发现问题所在,"record"的另外一种使用场景就是通过记录起来的方法调用参数,稍加改造后再发起一次方法调用。这种方法需要你已经执行过"record"模式,否则无法完成。假设已经执行过"record"了,那么接下来首先要做的就是通过-u参数指定需要改造的参数,-u参数在上文介绍”record“的时候已经介绍过,就不再介绍;
mt -c io.javadebug.agent.Agent -m loadField -t custom -u 1
当然,这种用法的意义不大,因为我们在介绍"record"的时候已经介绍过已经记录的请求可以随时查看,这种用法只是重新发起了一次请求罢了;Java-debug-tool提供了一种称为”claw script“的脚本语言来对选择的参数进行改造,这种脚本非常简单,没有学习成本,当然它能做的事情也非常有限,下面是这种"claw script"可以实现的功能:
- 对于参数是基本类型的,直接赋新值
p0 = "string" p1 = "100" p2 = "1.234"
- 对于参数是数组、list、set的,并且元素是基本类型的参数,直接赋值:
p0 = "[[1], [2], [3]]" p2 = "[[a], [b], [c]]" p3 = "[[1.23] [2.34] [3.45]]"
- 对于对象,属性是基本类型的,可以赋值:
p0.a = "100" p1.a.b = "Java-debug-tool"
除了这三种情况,其他赋值不支持。”claw script“的语法非常简单,p+参数列表顺序即可指定具体的参数,然后”=“代表需要赋值,值需要使用双引号包裹起来,数组、list、set的值需要使用"[]"包裹起来,每一个元素再使用"[]"括起来即可,对于对象赋值,直接使用字段名字即可。了解了”claw script“,下面来看看如何来进行命令发送。 你依然可以使用-u参数指定需要变更哪一个记录好的参数列表(默认是第一个记录),之后通过-claw参数指定一个参数列表变更脚本,即可实现:
mt -c io.javadebug.agent.Agent -m loadField -t custom -u 0 -claw p0="100",p1.a="1.234"
至此,你已经掌握了如何主动发起方法调用的方法了。这种情况下,一般通过两个人来完成调试,一个人执行该命令发起一次方法调用,而另外一个人则使用"watch"模式等待这次方法调用,这种团队协作模式还可以扩展到多人调试模式,一个人模拟方法调用,其他多个人观察方法执行视图,对此作出分析,集思广益快速排出问题。 如果没有那么多人配合你进行调试,那该怎么办呢?一个可行的办法是另外开一个窗口,使用"watch"模式等待即将模拟的方法调用,然后执行模拟方法执行命令,另外一个窗口中就会获取到方法执行视图信息,之后就可以进行问题排查了。Java-debug-tool提供了一种”自导自演“的调试方法,可以在发起方法调用的窗口中获取到方法的执行视图,下面就来介绍这种方法。
通过上面的介绍,其实如何实现这种”自导自演“的技术已经非常明确了,Java-debug-tool要求你通过-e参数来提供一个Spring表达式,来匹配你即将发起调用的参数列表,实现上,Java-debug-tool将使用”watch“的套路来匹配方法调用:
mt -c io.javadebug.agent.Agent -m loadField -t custom -u 0 -claw p0="100",p1.a="1.234"
-e #p0==100&&p1.getA().equals(1.234)
这样,你就可以在同一个窗口中拿到发起调用之后的方法执行视图了。
其实Java-debug-tool的核心命令使用已经介绍完成,本小节包括下面的小节中介绍的都是一些”周边“功能。 考虑某种场景,你的目标方法非常长(几千行),那么方法执行的视图将非常长,这种时候找到具体的某一行的相关信息可不简单,这个时候,Java-debug-tool提供了一个参数可以来拯救你的视觉:
mt -c io.javadebug.agent.Agent -m loadField -l 10,12,50
-l参数用于指定需要输出调试信息的代码行号,比如上面的样例中,我们指定了仅需要输出视图中第10、12和50行的信息,其他行的信息不需要。
默认情况下,Java-debug-tool的方法视图包括方法执行前的对象字段值信息,如果你有需求需要在方法执行结束后查看对象字段值信息,那么可以使用一个特殊的参数-s来指定:
mt -c io.javadebug.agent.Agent -m loadField -s fd
这样,你就可以获取到方法执行前后的目标对象(或者类的静态字段)的字段信息。
上面提到如果我们需要”watch“的参数迟迟得不到匹配,那么就会得到可恶的”命令执行超时“的响应,如果你的心狠一点,那么指定一个命令执行超时时间是可以在一定程度上避免这种情况的发生的:
mt -c io.javadebug.agent.Agent -m loadField -t watch -i #p0==100 -timeout 100
-timeout用于指定命令执行的超时时间,只要指定了合法的值,Java-debug-tool就不会使用它的”两阶段超时协议“对你的命令进行干预,比如上面的样例代表我允许这个命令最多执行100秒,当然可能不是精确的100秒,因为Java-debug-tool的超时检测是有一定间隔的。
类热替换是指在运行时将类的字节码替换掉,使得某些方法的行为发生改变,Java-debug-tool支持这种替换,并且操作起来非常简单:
rdf -p [className1:class1Path className1:class2Path]
类回滚在Java-debug-tool中指将类的字节码回退到最初的版本,无论是方法执行视图获取,还是类热替换,都会更新类的字节码,如果想要回退到原来的版本,则执行类回滚命令即可实现:
back -c ClassName,ClassName
这样,类就会和原来一模一样的工作了。