arthas
arthas是阿里开源的非常强大的java进程分析工具,它有着非常丰富的功能,从查看堆内存信息、线程信息、增强字节码等等。这个工具其实应该叫一个工具库,或者工具箱,tool kit更贴切。因为arthas其实是整合了网上很多开源的工具,给他们缝合到了一个系统中。
例如:profiler火焰图是直接用的async-profiler;ognl直接就是apache commons-ognl;mc内存编译器使用的是SkaETL/compiler;jad反编译器使用的是cfr等等。这么多功能集合在一起,确实也不容易,况且arthas中还有很多功能是基于阿里的bytekit实现的字节码增强实现的,这几个功能反倒是我使用最多的功能:
watch监控方法,被调用时打印出入参。最好用的debug工具。trace监控方法中所有子方法的耗时。最好用的性能分析工具。
arthas缺点
现阶段我感觉arthas已经非常好了,但是在使用中还有觉得有些操作太麻烦:
watch等功能都是阻塞窗口的,多个函数的watch需要新开窗口。- 没有一个UI页面,都是shell运行,长时间不用就忘了指令语法了。
- 想要热替换整个类,或整个方法体,需要非常复杂的,编译,替换等流程,且经常无缘无故失败。
ognl主动触发的表达式,在spring项目不符合预期,因为用了默认类加载器;需要用vmtool配合一堆复杂的语法,spring项目非常受限。
swapper
JVMByteSwapTool或者简称swapper,是我自己写的一个工具,实现了个人比较常用的功能,和arthas有一部分功能重叠,也有一些是arthas不具有的功能,总体而言更加容易上手。
下载demo.jar和swapper.jar,这里使用的是v0.0.5版本。
$ wget https://github.com/sunwu51/jbs-demo/releases/download/1.0.0/demo.jar
$ wget https://github.com/sunwu51/JVMByteSwapTool/releases/download/v0.0.5/swapper.jar
接下来启动demo服务,这是个spring boot的简单项目,源码可以参考sunwu51/jbs-demo。
然后启动swapper,选择attach刚才的demo进程。
此时访问8000(如果已经占用会自动切换8001)端口可以得到这样一个页面:

默认会连接同域名的18000端口的Websocket服务,因为这里使用的gitpod服务,端口在域名中,这里修改域名重新点击连接,如果你是本地应该直接右侧状态是绿色正常链接成功了。

Decompile
先介绍这个功能,可以方便理解其他功能。输入类名进行反编译得到源码。

绿色按钮clear log可以清理日志区域,effected classes按钮可以展示当前被影响的类列表,reset按钮则是把所有的影响都删除。
Watch
刚才的/base64对应com.example.demo.DemoApplication#base64方法。输入类名#方法名即可对方法进行增强,监听并打印方法的出入参和耗时,如下:

此时effected列表有这个被增强的类。

可以通过反编译查看增强后的代码。

默认情况下会监听100次,100次之后,自动注销监听功能。可以修改系统属性来修改这个次数,后面Exec介绍。
这里为了避免干扰其他功能测试,先reset
OuterWatch
监听方法中子方法的调用,子方法支持*匹配任意类。

ChangeBody
修改某个方法的body,要和原方法有一样的返回值,在方法中$1 $2 ... 分别代表第1、第二..个方法的入参。
这里提供了javassist和asm两种底层实现,后者是对前者的逆向实现,两者都支持$1 $2 ..这种参数的表达。但是javassist可能在java17以上有兼容性问题,所以提供了asm作为备选方案,此外asm引擎是使用janino作为编译器支持较多的语法,而javassist使用内置编译器仅支持java4之前的语法,并且不支持int等基础类型的自动装箱。

ChangeResult
修改某个方法中调用子方法的返回内容,相比ChangeBody来说,ChangeResult影响面更小,用法也更灵活。与OuterWatch类似,这里的子方法也支持*匹配所有类,但是当InnerMethod匹配到多个有不同签名的方法时,就会check报错提醒。
这里支持$_作为当前子方法的返回值,直接$_=xx;即可跳过原函数执行,返回一个指定值。

同样也提供了javassist和ASM两种底层引擎,他们都支持$_是子函数返回值,$1 $2..是子函数入参,还支持$proceed是调用原方法,这里两个引擎稍有区别。
- javassist引擎,
$proceed($$)是调用原方法。 - ASM引擎,
$proceed()是调用原方法。
以encodeToString为例,我们调用原方法编码之后,在追后加一个0.0,如下:

再举个例子encodeToString的入参是byte[],字符1的ASCII是49,入参123对应的byte[]就是49,50,51。我们把第三个位置改成固定的52,然后运行base64,此时我们入参传入12x,第三位任意传什么字符,都会被换成52也就是4。

为什么两个引擎共存 当前
ASM的功能已经完成可以替代javassist为什么还保留了两个引擎选项,主要是没有做充分的测试,如果有一些边界情况ASM不好使的话,可以切回javassist,如果一段时间使用后,没有发现ASM有问题,就会只保留ASM引擎了。
Exec
主动触发一段函数执行,替代arthas的ognl功能,但是后者只能写ognl表达式,swapper这里可以用java代码编辑一个方法,并且内置了丰富的辅助功能:
- 模板代码中的
ctx变量,是获取当前spring的上下文,ctx.getBean(name|class)可以获取bean,这样就可以触发bean中的方法了。 Global类内置了一些辅助方法:info(obj)error(obj, e)打印并传递到页面日志ognl(str)执行ognl表达式,与arthas类似,但是这是在spring类加载器下执行,可以访问项目中的类beanTarget(bean)获取spring增强的代理对象的原始对象readFile(str)读取文本文件,返回行列表List<String>
demo.jar中有/user/{id}路径是读取h2中的mock的5条数据,假如我们把id=1的数据的name改为faker,如下直接通过ctx获取bean然后修改数据库。

maxHit 上面提到的
watch只能监听100次上限,也包括outerwatch和trace,这个限制其实是存到了System.getProperty("maxHit")中,可以通过两种方法修改这个限制。一种就是直接在Exec中执行System.setProperty("maxHit", "200"),另一种方法是在启动swapper.jar的时候,传递-Dw_maxHit=200,swapper.jar的w_开头的属性,都会被去掉w_,剩下的部分作为属性设置到目标jvm进程中。
ReplaceClass
上传class文件替换整个类,本地修改代码编译后,找到生成的class文件上传,并指定类名,即可完成替换。
但是注意,匿名类等,可能会生成一个XXX$1.class,如果修改的是这部分内容,尽量就不要使用这个功能了。
