概述
如果你看到这个标题想到的是 Arthas ,那么恭喜你,你想的跟我一样~
如果你看到这个标题一点想法的没有,那你可能真的需要系统性学习一下 arthas 了。arthas 是一款线上监控诊断工具,可以在不修改应用的情况下,对业务问题进行诊断。现在不用上几手 arthas ,都不好意思出门跟人调 bug了。
如果你想到的是 bpf,bcc 之类的,那你是真个大佬,大哥带带弟弟.jpg
准备工作
首先写一段简单的 java 代码,用来测试
import java.util.Arrays;
import java.util.Random;
import java.util.concurrent.TimeUnit;
/**
* 求和测试类
*
* @author xisha
* @date 2022/5/9 7:33 下午
*/
public class SumDemo {
public static void main(String[] args) throws Exception {
SumDemo demo = new SumDemo();
while (true) {
demo.test();
TimeUnit.SECONDS.sleep(3);
}
}
public void test() {
Random random = new Random(System.currentTimeMillis());
int numberSize = random.nextInt(5);
int[] numbers = new int[numberSize];
for (int i = 0; i < numberSize; i++) {
numbers[i] = random.nextInt(10);
}
System.out.println(sum(numbers));
}
public Integer sum(int... numbers) {
return Arrays.stream(numbers).sum();
}
}
代码很简单,就是一直生成几个随机数,然后求和,跑起来的效果大概如下
然后下载 arthas curl -O arthas.aliyun.com/arthas-boot…
更多下载方式见 arthas.aliyun.com/doc/install…
然后 java -jar arthas-boot.jar 选择我们的 SumDemo 进行 attach
成功之后的界面大概如下图
常用功能
- 查看 sum 方法每次传入的参数都是啥。
- 修改sum方法的返回,使其返回一个固定的值。
- 查看 SumDemo 类的代码。
如果大佬对上面三个功能了如指掌,那么直接跳到下一章节即可。
watch 参数
watch SumDemo sum '{params,returnObj,throwExp}' -n 5 -x 3
反编译
有时候我们想看看线上的代码是不是我们想要的,我们可以通过 jad 命令进行在线反编译。 jad SumDemo
效果大概如下图所示,我们可以可以方便的看到这个类的代码,类加载器,类文件的路径(这个在确定线上包的版本的时候十分有用)
更多 jad 的用法可以查看 arthas.aliyun.com/doc/jad.htm…
热更新一条龙
现在让我们尝试修改 sum 函数的返回值,使其返回一个固定的值 public Integer sum(int... numbers) { return 49; //return Arrays.stream(numbers).sum(); } 改完之后通过 arthas 编译一个 class
首先我们需要找到 SumDemo 的类加载器的hash值 输入 sc -d *SumDemo , 对应的 classLoaderHash 就是我们需要的hash值,如图所示,我这边的 hash 值是 277050dc
然后用这个类加载编译一下刚才改好的 java 文件 mc -c 277050dc ./SumDemo.java
如上图所示,编译就算是成功了,如果不成功的话,可能是有些特殊情况,比如用了lombok,泛型对不上等等,要是失败了…… 那就算了,你跟这个命令莫得缘分。
然后我们使用 redefine 命令,
redefine classpath ,就可以热更新这个类了。
可以看到,热更新之后,我们的代码就只返回 49了。
注意,这里为什么先讲 jad 再讲 热更新呢? 因为 jad 命令跟 redefine 命令是冲突的,jad 会还原 redefine 产生的字节码,详见文档arthas.aliyun.com/doc/redefin…
为所欲为
上面讲了这么多,都是一些问题调试的功能,怎么就为所欲为了呢?
使用spring context 为所欲为
在 spring 的项目中,如果你有【保存 spring 上下文,以便自己下次使用】的习惯(如下代码),
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
@Component
public class SpringBeanUtil implements ApplicationContextAware {
private static ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext)
throws BeansException {
SpringBeanUtil.applicationContext = applicationContext;
}
public static <T> T getBean(Class<T> clazz) {
return (T) applicationContext.getBean(clazz);
}
public static Object getBean(String name) throws BeansException {
return applicationContext.getBean(name);
}
}
那么你可以用 arthas 获取这个上下文,然后再获取相应的bean,然后就可以为所欲为了。对应的 arthas 代码如下
ognl '#context=@SpringBeanUtil@applicationContext, #context.getBean("yourbeanName").doSomething()'
注意:加载对应的类可能需要对应的类加载器Hash,可以使用 sc -d *SpringBeanUtil
命令来获取类加载器 Hash
,然后再使用 ognl
的时候加上 -c 选项即可。
使用 tt 为所欲为
如果没有【保存 spring 上下文】也不打紧,一般的 spring项目 都会有 http 接口(甚至健康检查的接口就是 http 接口),这个时候我们可以使用 tt 命令为所欲为。原理是找一个调用链路里有 spring 上下文的类,然后基于这个类记录这个请求,然后通过重放这个请求,达到执行任意 bean 代码的目的。比如 RequestMappingHandlerAdapter#invokeHandlerMethod
这个就很合适。具体过程如下:
我们先使用 tt 命令记录这个请求
tt -t org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter invokeHandlerMethod
然后等一个请求
大概像这样,然后再用 tt 命令,-i 可以指定重放的请求,-w 可以执行ognl,然后为所欲为的代码大概如下
tt -i 1000 -w 'target.getApplicationContext().getBean('yourbeanName').doSomething()'
基于 dubbo 为所欲为
有人说我莫得 http 接口,全是 dubbo 的调用,那这个咋整?不慌的,dubbo 某个拓展里自动为我们保存了 spring 上下文,直接为所欲为即可。代码如下:
ognl '#context=@com.alibaba.dubbo.config.spring.extension.SpringExtensionFactory@contexts.iterator.next, #context.getBean("yourBeanName").doSomeThing()'
vmtool 获取实例为所欲为
如果我的应用啥也没有,没有dubbo,甚至都不是 spring 的,那我要如何为所欲为呢? 这个时候我们可以使用 vmtool,直接获取对象进行为所欲为。当我看到这个工具的时候真是直呼好家伙!太强了简直! 以我们的SumDemo 为例,我们可以直接获取这个类进行函数调用,示例代码如下 vmtool --action getInstances --className SumDemo --express 'instances[0].test()'