添加line命令,用来观测方法内的局部变量,可以通过行号或者LineCode来指定位置
为什么要做这个呢?
- 符合Debug的习惯,查看方法内的局部变量是理所当然的想法
- 协助快速定位排查问题,若通过加日志观测的话自由度会更大,但retransform会比较麻烦,而且往往随着排查深入,可能要加多个地方
- 有看到isuues里边也有一些小伙伴提到这个,需求有其合理性
- 所在业务已经使用1年有余,虽使用频率不如watch、trace,但仍能提高问题排查效率(上次提的PR没有通过,这次完善了一些点🙏)
为什么要定义新的命令?
- 跟观测非常契合的命令是
watch,但是watch的对象是method,而我们观测local variables的时候,用行来作为定位点是比较合适的(因为同一个变量会被多次重复赋值),两者的回调监听是不一致的 - 独立一个命令也能降低对旧命令的影响,也能降低实现的复杂度
实现思路:
- 定义新的
Advice变量varMap,类型是HashMap,key是变量名,value是变量值 - 插桩的位置是命令参数传入的 LineNumber 或 LineCode(自定义的特殊值) 之前,并使用-1来表示方法退出前的插桩点
- 监听器的注册需要做相应的区分处理
已经做的基本测试:
- 基础功能实现,能观测到定义的
local variables,并能在express和condition-express中使用 - 重复插桩处理,增加了对应的
LocationFilter,避免重复插桩 - 多人同时监听使用
- 与
trace及watch命令的同时使用
为什么会需要使用LineCode?
主要是kotlin编译后的字节码跟源码相差甚远,如下: kotlin源码:
/*56*/ fun index(): JSONObject {
/*57*/ listOf(1,2,3,4,5,6).map {
/*58*/ listOf("a","b","c","d","e","f").map {
/*59*/ listOf(true,false,true,false,false).map {
/*60*/ println(it)
/*61*/ if (true) return JSONObject()
/*62*/ }
/*63*/ }
/*64*/ }
/*65*/ return ResponseBuilder().ok().data("Hello World!").build()
/*66*/ }
jad出来的样子:
public final JSONObject index() {
void var3_3;
void $receiver$iv$iv;
Iterable $receiver$iv;
/*57*/ Iterable iterable = $receiver$iv = (Iterable)CollectionsKt.listOf(1, 2, 3, 4, 5, 6);
Collection destination$iv$iv = new ArrayList(CollectionsKt.collectionSizeOrDefault($receiver$iv, 10));
/*83*/ for (Object item$iv$iv : $receiver$iv$iv) {
void $receiver$iv$iv2;
Iterable $receiver$iv2;
int n = ((Number)item$iv$iv).intValue();
Collection collection = destination$iv$iv;
boolean bl = false;
/*58*/ Iterable iterable2 = $receiver$iv2 = (Iterable)CollectionsKt.listOf("a", "b", "c", "d", "e", "f");
Collection destination$iv$iv2 = new ArrayList(CollectionsKt.collectionSizeOrDefault($receiver$iv2, 10));
/*86*/ for (Object item$iv$iv2 : $receiver$iv$iv2) {
void $receiver$iv$iv3;
Iterable $receiver$iv3;
String string = (String)item$iv$iv2;
Collection collection2 = destination$iv$iv2;
boolean bl2 = false;
/*59*/ Iterable iterable3 = $receiver$iv3 = (Iterable)CollectionsKt.listOf(true, false, true, false, false);
Collection destination$iv$iv3 = new ArrayList(CollectionsKt.collectionSizeOrDefault($receiver$iv3, 10));
Iterator iterator2 = $receiver$iv$iv3.iterator();
if (iterator2.hasNext()) {
void it;
void it2;
void it3;
Object item$iv$iv3 = iterator2.next();
boolean bl3 = (Boolean)item$iv$iv3;
Collection collection3 = destination$iv$iv3;
boolean bl4 = false;
/*60*/ System.out.println((boolean)it3);
JSONObject jSONObject = new JSONObject();
return jSONObject;
}
/*91*/ List list = (List)destination$iv$iv3;
collection2.add(list);
}
/*92*/ List list = (List)destination$iv$iv2;
collection.add(list);
}
/*93*/ List cfr_ignored_0 = (List)var3_3;
return new ResponseBuilder().ok().data((Object)"Hello World!").build();
}
问题: 反编译后,它的行号分布是乱序的,另外实践中还发现有重复行号的问题,而且反编译后与源代码大相径庭,也生成了很多的额外的零时变量,也有很多行是没有行号的,所以使用行号定位的话,是不够完善的,有些点无法进行定位插入监听!
解法: 如何能监听到所有本地变量的变化过程呢? -> 变量在何时会被改变呢? -> 赋值、作为方法参数被调用 基于此,通过筛选方法中的InsnNode,只保留 VarInsnNode 和 MethodInsnNode 作为备选插入点,然后生成类似如下的标记行并以此为变量监测的插入点. 目前格式如: 行号 + LineCode + 指令(方法调用/变量赋值 )
/*61 */ (4076-1)->
invoke-method:java/lang/Integer#valueOf:(I)Ljava/lang/Integer;
/*61 */ (4076-2)->
invoke-method:java/lang/Integer#valueOf:(I)Ljava/lang/Integer;
/*61 */ (4076-3)->
invoke-method:java/lang/Integer#valueOf:(I)Ljava/lang/Integer;
/*61 */ (4076-4)->
invoke-method:java/lang/Integer#valueOf:(I)Ljava/lang/Integer;
/*61 */ (4076-5)->
invoke-method:java/lang/Integer#valueOf:(I)Ljava/lang/Integer;
/*61 */ (4076-6)->
invoke-method:java/lang/Integer#valueOf:(I)Ljava/lang/Integer;
/*61 */ (7a12-1)->
invoke-method:kotlin/collections/CollectionsKt#listOf:([Ljava/lang/Object;)Ljava/util/List;
/*84 */ (17cc-1)->
invoke-method:kotlin/collections/CollectionsKt#collectionSizeOrDefault:(Ljava/lang/Iterable;I)I
/*84 */ (ec06-1)->
invoke-method:java/util/ArrayList#<init>:(I)V
/*85 */ (2795-1)->
invoke-method:java/lang/Iterable#iterator:()Ljava/util/Iterator;
/*85 */ (b5c4-1)->
invoke-method:java/util/Iterator#hasNext:()Z
/*85 */ (dce9-1)->
invoke-method:java/util/Iterator#next:()Ljava/lang/Object;
/*86 */ (9699-1)->
invoke-method:java/lang/Number#intValue:()I
/*86 */ (81f3-1)->
assign-variable:it
/*62 */ (7a12-2)->
invoke-method:kotlin/collections/CollectionsKt#listOf:([Ljava/lang/Object;)Ljava/util/List;
/*87 */ (17cc-2)->
invoke-method:kotlin/collections/CollectionsKt#collectionSizeOrDefault:(Ljava/lang/Iterable;I)I
/*87 */ (ec06-2)->
invoke-method:java/util/ArrayList#<init>:(I)V
/*88 */ (2795-2)->
invoke-method:java/lang/Iterable#iterator:()Ljava/util/Iterator;
/*88 */ (b5c4-2)->
invoke-method:java/util/Iterator#hasNext:()Z
/*88 */ (dce9-2)->
invoke-method:java/util/Iterator#next:()Ljava/lang/Object;
/*89 */ (81f3-2)->
assign-variable:it
/*63 */ (fe63-1)->
invoke-method:java/lang/Boolean#valueOf:(Z)Ljava/lang/Boolean;
/*63 */ (fe63-2)->
invoke-method:java/lang/Boolean#valueOf:(Z)Ljava/lang/Boolean;
/*63 */ (fe63-3)->
invoke-method:java/lang/Boolean#valueOf:(Z)Ljava/lang/Boolean;
/*63 */ (fe63-4)->
invoke-method:java/lang/Boolean#valueOf:(Z)Ljava/lang/Boolean;
/*63 */ (fe63-5)->
invoke-method:java/lang/Boolean#valueOf:(Z)Ljava/lang/Boolean;
/*63 */ (7a12-3)->
invoke-method:kotlin/collections/CollectionsKt#listOf:([Ljava/lang/Object;)Ljava/util/List;
/*90 */ (17cc-3)->
invoke-method:kotlin/collections/CollectionsKt#collectionSizeOrDefault:(Ljava/lang/Iterable;I)I
/*90 */ (ec06-3)->
invoke-method:java/util/ArrayList#<init>:(I)V
/*91 */ (2795-3)->
invoke-method:java/lang/Iterable#iterator:()Ljava/util/Iterator;
/*91 */ (b5c4-3)->
invoke-method:java/util/Iterator#hasNext:()Z
/*91 */ (dce9-3)->
invoke-method:java/util/Iterator#next:()Ljava/lang/Object;
/*92 */ (e4e8-1)->
invoke-method:java/lang/Boolean#booleanValue:()Z
/*92 */ (81f3-3)->
assign-variable:it
/*64 */ (a406-1)->
invoke-method:java/io/PrintStream#println:(Z)V
/*65 */ (7338-1)->
invoke-method:com/alibaba/fastjson/JSONObject#<init>:()V
/*93 */ (088d-1)->
invoke-method:java/util/Collection#add:(Ljava/lang/Object;)Z
/*94 */ (088d-2)->
invoke-method:java/util/Collection#add:(Ljava/lang/Object;)Z
/*69 */ (f84c-1)->
invoke-method:com/seewo/study/minder/common/util/ResponseBuilder#<init>:()V
/*69 */ (3252-1)->
invoke-method:com/seewo/study/minder/common/util/ResponseBuilder#ok:()Lcom/seewo/study/minder/common/util/ResponseBuilder;
/*69 */ (06c3-1)->
invoke-method:com/seewo/study/minder/common/util/ResponseBuilder#data:(Ljava/lang/Object;)Lcom/seewo/study/minder/common/util/ResponseBuilder;
/*69 */ (a260-1)->
invoke-method:com/seewo/study/minder/common/util/ResponseBuilder#build:()Lcom/alibaba/fastjson/JSONObject;
使用示例:
源码(部分):
...
/*8*/ public class MathGame {
...
/*21*/ public void run() throws InterruptedException {
/*22*/ try {
/*23*/ int number = random.nextInt() / 10000;
/*24*/ List<Integer> primeFactors = primeFactors(number);
/*25*/ print(number, primeFactors);
/*26*/
/*27*/ } catch (Exception e) {
/*28*/ System.out.println(String.format("illegalArgumentCount:%3d, ", illegalArgumentCount) + e.getMessage());
/*29*/ }
/*30*/ }
...
/*63*/ }
使用行号:
$ line demo.MathGame run 25 -x 2
Press Q or Ctrl+C to abort.
Affect(class count: 1 , method count: 1) cost in 17 ms, listenerId: 2
method=demo.MathGame.run line=25
ts=2024-06-21 09:57:34.452; result=@HashMap[
@String[primeFactors]:@ArrayList[
@Integer[2],
@Integer[7],
@Integer[7],
@Integer[991],
],
@String[number]:@Integer[97118],
]
查看LineCode:
$ jad --lineCode demo.MathGame run
........
------------------------- lineCode location -------------------------
format: /*LineNumber*/ (LineCode)-> Instruction
/*23 */ (aacd-1)->
invoke-method:java/util/Random#nextInt:()I
/*23 */ (5918-1)->
assign-variable:e
/*24 */ (653f-1)->
invoke-method:demo/MathGame#primeFactors:(I)Ljava/util/List;
/*24 */ (d961-1)->
assign-variable:primeFactors
/*25 */ (416e-1)->
invoke-method:demo/MathGame#print:(ILjava/util/List;)V
/*27 */ (5918-2)->
assign-variable:e
/*28 */ (2455-1)->
invoke-method:java/lang/StringBuilder#<init>:()V
/*28 */ (4076-1)->
invoke-method:java/lang/Integer#valueOf:(I)Ljava/lang/Integer;
/*28 */ (b6e4-1)->
invoke-method:java/lang/String#format:(Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/String;
/*28 */ (850c-1)->
invoke-method:java/lang/StringBuilder#append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
/*28 */ (a53d-1)->
invoke-method:java/lang/Exception#getMessage:()Ljava/lang/String;
/*28 */ (850c-2)->
invoke-method:java/lang/StringBuilder#append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
/*28 */ (f7bb-1)->
invoke-method:java/lang/StringBuilder#toString:()Ljava/lang/String;
/*28 */ (2f1b-1)->
invoke-method:java/io/PrintStream#println:(Ljava/lang/String;)V
Affect(row-cnt:1) cost in 103 ms.
使用LineCode:
$ line demo.MathGame run 416e-1 -x 2
Press Q or Ctrl+C to abort.
Affect(class count: 1 , method count: 1) cost in 17 ms, listenerId: 2
method=demo.MathGame.run line=25
ts=2024-06-21 09:57:34.452; result=@HashMap[
@String[primeFactors]:@ArrayList[
@Integer[2],
@Integer[7],
@Integer[7],
@Integer[991],
],
@String[number]:@Integer[97118],
]
其它:
- 这边打包好的版本(arthas-bin.zip): https://ali-pro-pub.xbstatic.com/running-wechat-mp/uwiwlnvhhhwljhjxhnjvinmpyiphihhh
- 具体使用方法可以启动后通过
line -h查看 - commit: line command implement 是line命令的具体实现
- commit: add introduction for line command 是相关的命令说明
@hengyunabc 大佬麻烦看看
line -h可以正常输出,为什么观测变量时提示不支持命令呢,是不支持java8吗
@isadliliying 有时候 我们观察局部变量是观察一个变化过程,比如 方法methodX内部 局部变量 a,b, 从35到55行 a,b变量发生了哪些变化,也就是方法methodX 执行一次就打印20行的(55-35)变化结果类似于表格记录吧
java8不行吗?
我编译你这个分支好像不通过
line -h可以正常输出,为什么观测变量时提示不支持命令呢,是不支持java8吗
这个应该是你先启动了旧版本,然后再启动这个带line命令的版本,就会出现这个提示哈
java8不行吗?
你启动的是这个版本嘛?这个命令没有合并到主分支,正式版本里边是没有这个命令的
我编译你这个分支好像不通过
excludePattern 是有个新版本的依赖库,可以检查下你的 bytekit-core 版本

java8不行吗?