从ASM到ByteKit再到Arthas:一条Java字节码增强链路
之前写过几篇 Java Agent 和 Javassist,也单独介绍过 Arthas 的 vmtool。这些内容放在一起看,会发现它们其实一直围绕着同一个问题:
不修改业务源码,能不能在一个 Java 方法执行前后,临时加上一些观察逻辑?
比如有这样一个方法:1
2
3
4
5
6
7
8
9public String placeOrder(String user, int quantity) {
if ("error".equals(user)) {
throw new IllegalArgumentException("user cannot be error");
}
validateQuantity(quantity);
String sku = queryInventory(user);
return saveOrder(user, sku, quantity);
}
我们希望知道:
- 调用时传了什么参数;
- 正常执行返回了什么;
- 抛异常时是什么异常;
- 整个方法以及内部几个调用分别耗时多久。
如果能改源码,加几行日志当然最简单。但在线上排查时,往往既不方便改代码,也不希望为了几行日志重新发布和重启应用。一般现在会使用arthas来查看参数,返回等,不过仔细研究下,ASM、ByteKit 和 Arthas,正好可以看成从底到顶的三种解决层次。
简单概括就是:
| 工具 | 它解决的问题 | 使用者面对的东西 |
|---|---|---|
| ASM | 怎样正确读写 class 字节码 | 指令、操作数栈、局部变量、栈帧 |
| ByteKit | 怎样更方便地表达诊断插桩 | @AtEnter、@AtExit、@Binding |
| Arthas | 怎样把插桩变成可以直接使用的排障工具 | watch、trace、jad、reset |
这里最容易产生的误解是把它们看成三套互相竞争的技术。实际上,ByteKit 构建在 ASM 之上,而 Arthas 的字节码增强又使用了 ByteKit。它们更像是同一条链路上的不同抽象层。
本文会沿着这条链路,从一个可以运行的例子开始,一层层看清楚它们分别做了什么。
完整示例的源码会在本文末尾直接渲染,本地工程放在:1
source/code/asm-bytekit-arthas-demo
下面的代码块来自这个目录中的真实文件,不依赖目录页面。示例要求 JDK 8 以上和 Maven 3.6 以上,本文实际使用 JDK 8、ASM 9.9.1、ByteKit 0.1.7 和 Arthas 4.3.2 验证。
先从 class 是怎样被改掉的说起
在看 ASM 的 API 之前,先把 Java 程序从源码到运行的过程简化一下:1
2
3
4
5
6
7
8
9
10
11
12
13DemoService.java
|
| javac
v
DemoService.class,也就是一组 byte[]
|
| ClassLoader.defineClass
v
JVM 中的 Class<?> 和方法实现
|
| 解释执行 / JIT 编译
v
机器指令
平时我们关注的是 .java 和对象,字节码工具关注的则是中间那段 byte[]。
Java 在 java.lang.instrument 包里提供了 Instrumentation 接口。Agent 可以向它注册一个 ClassFileTransformer:1
instrumentation.addTransformer(transformer, true);
当 JVM 加载类,或者对已加载的类执行 retransformClasses() 时,会调用 Transformer:1
2
3
4
5
6
7byte[] transform(
ClassLoader loader,
String className,
Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer
)
这里最重要的就是最后一个参数和返回值:1
2
3
4
5
6
7
8
9JVM 给 Transformer 原始 classfileBuffer
|
| 修改
v
Transformer 返回新的 byte[]
|
| 校验并安装
v
JVM 开始执行增强后的方法
如果 Transformer 返回 null,表示这个类不修改。返回原始数组也能达到类似效果,但按照接口语义,不处理时返回 null 更清楚。
这也是为什么 ASM、Javassist、Byte Buddy 和 ByteKit 虽然 API 完全不同,最后却都能挂到 Java Agent 上:它们最终都在完成同一件事,把一份 class 字节数组转换成另一份 class 字节数组。
premain 和 agentmain 又是什么关系
之前 Agent 示例里使用过 premain:1
2
3public static void premain(String agentArgs, Instrumentation instrumentation) {
instrumentation.addTransformer(new AsmTimingTransformer(), true);
}
配合 JVM 参数启动:1
java -javaagent:asm-agent.jar -jar app.jar
premain 在应用 main 方法之前执行,所以特别适合 APM、链路追踪、覆盖率等需要从启动阶段持续工作的工具。
但 Arthas 面对的是一个已经运行的 JVM,它使用的是另一条入口:1
2
3public static void agentmain(String agentArgs, Instrumentation instrumentation) {
// 动态挂载之后执行
}
两者拿到的都是 Instrumentation,主要区别只是进入目标 JVM 的时机:1
2
3启动时加载:-javaagent -> premain -> main
运行时加载:目标 JVM 已运行 -> Attach API -> agentmain
所以“Java Agent”和“Arthas 动态挂载”并不是两套原理。Arthas 只是把 Agent 从启动参数加载,变成了运行时 Attach。
第一层:直接使用 ASM 改方法
ASM 是一个很底层的字节码操作库。这里的“底层”并不是说它直接操作二进制位,而是说它暴露出来的概念已经非常接近 JVM class 文件:
- 类名使用
com/example/DemoService这种 internal name; - 方法由名字和 descriptor 一起确定;
- 方法内容是一条条 JVM 指令;
- 我们需要关心局部变量、操作数栈和 stack map frame。
为什么方法名还不够
Java 支持方法重载,下面两个方法名字相同:1
2placeOrder(String user, int quantity)
placeOrder(String user)
字节码里需要用 descriptor 区分。本文目标方法的 descriptor 是:1
(Ljava/lang/String;I)Ljava/lang/String;
拆开看并不复杂:1
2
3
4
5(
Ljava/lang/String; 第一个参数 String
I 第二个参数 int
)
Ljava/lang/String; 返回值 String
因此 ASM Transformer 里同时匹配类名、方法名和 descriptor:1
2
3
4
5private static final String TARGET_CLASS =
"com/nicksxs/bytecode/demo/app/DemoService";
private static final String TARGET_METHOD = "placeOrder";
private static final String TARGET_DESCRIPTOR =
"(Ljava/lang/String;I)Ljava/lang/String;";
这比只匹配方法名稳妥,否则一个类里所有重载方法都可能被增强。
ClassReader、Visitor 和 ClassWriter
ASM 最典型的处理流程是:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27ClassReader reader = new ClassReader(classfileBuffer);
ClassWriter writer = new ClassWriter(
reader,
ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS
);
ClassVisitor visitor = new ClassVisitor(Opcodes.ASM9, writer) {
public MethodVisitor visitMethod(
int access,
String name,
String descriptor,
String signature,
String[] exceptions) {
MethodVisitor mv = super.visitMethod(
access, name, descriptor, signature, exceptions);
if (TARGET_METHOD.equals(name)
&& TARGET_DESCRIPTOR.equals(descriptor)) {
return new TimingAdvice(mv, access, name, descriptor);
}
return mv;
}
};
reader.accept(visitor, ClassReader.EXPAND_FRAMES);
return writer.toByteArray();
可以把它理解成一条流水线:1
2
3
4
5
6
7
8
9原始 byte[]
|
ClassReader 解析 class 结构
|
ClassVisitor 访问类、字段和方法
|
MethodVisitor 在目标方法中插入指令
|
ClassWriter 重新生成 byte[]
ASM 使用 Visitor 模式的一个好处是可以边读边写,不一定要把整个 class 转成另一套庞大的对象模型。需要更方便地增删和遍历指令时,也可以使用 ASM Tree API,把类读成 ClassNode 和 MethodNode。
为什么插一行日志也会牵涉操作数栈
源码中的:1
System.out.println("hello");
落实到字节码,大致是:1
2
3GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
LDC "hello"
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
前两条指令把 PrintStream 对象和字符串压入操作数栈,第三条再把它们消费掉。顺序错了、类型错了或者某条分支上的栈高度不一致,JVM 验证类时就可能抛出 VerifyError。
本文示例使用 AdviceAdapter,它在 MethodVisitor 上又封装了一层,提供:1
2onMethodEnter()
onMethodExit(int opcode)
我们仍然是在生成 JVM 指令,只是少写了一些容易出错的样板代码。
在方法入口记录参数和开始时间
示例在入口先调用 System.nanoTime(),把结果放进一个新建的局部变量:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
protected void onMethodEnter() {
invokeStatic(SYSTEM_TYPE, NANO_TIME);
startNanosLocal = newLocal(Type.LONG_TYPE);
storeLocal(startNanosLocal);
getStatic(SYSTEM_TYPE, "out", PRINT_STREAM_TYPE);
newInstance(STRING_BUILDER_TYPE);
dup();
invokeConstructor(STRING_BUILDER_TYPE, STRING_BUILDER_INIT);
push("[ASM] enter placeOrder user=");
invokeVirtual(STRING_BUILDER_TYPE, APPEND_STRING);
loadArg(0);
invokeVirtual(STRING_BUILDER_TYPE, APPEND_STRING);
push(", quantity=");
invokeVirtual(STRING_BUILDER_TYPE, APPEND_STRING);
loadArg(1);
invokeVirtual(STRING_BUILDER_TYPE, APPEND_INT);
invokeVirtual(STRING_BUILDER_TYPE, TO_STRING);
invokeVirtual(PRINT_STREAM_TYPE, PRINTLN);
}
这里的 loadArg(0) 和 loadArg(1),最后会变成相应的局部变量加载指令。ASM 知道方法是不是静态方法,因此会帮我们处理 this 占用的第 0 个局部变量槽位。
一个方法不一定只有一个出口
正常返回可能使用:1
2
3
4
5
6IRETURN
LRETURN
FRETURN
DRETURN
ARETURN
RETURN
异常还会经过 ATHROW。如果直接找到最后一条指令插日志,很容易漏掉前面的提前返回和异常分支。
AdviceAdapter.onMethodExit() 会在这些退出指令前回调:1
2
3
4
5
6
7
8
9
10
protected void onMethodExit(int opcode) {
invokeStatic(SYSTEM_TYPE, NANO_TIME);
loadLocal(startNanosLocal);
math(SUB, Type.LONG_TYPE);
int elapsedLocal = newLocal(Type.LONG_TYPE);
storeLocal(elapsedLocal);
// 后面生成 println("cost=" + elapsed + " ns") 的指令
}
示例还根据 opcode == ATHROW 区分正常返回和显式抛出异常。
这里有个值得注意的细节:只在 ATHROW 前插代码,能捕获方法字节码里显式执行的 ATHROW,却不等同于给整个方法套了一层 try/finally。如果某个子调用抛出异常并直接向外传播,目标方法本身不一定存在一条可见的 ATHROW 指令。真正通用的异常退出增强,需要改写异常表,ByteKit 的 @AtExceptionExit 就封装了这部分处理。
COMPUTE_MAXS 和 COMPUTE_FRAMES 在算什么
每个方法在 class 文件里都需要声明:
- 最大操作数栈深度;
- 最大局部变量槽位数量;
- 分支合流位置的 stack map frame。
插入新指令和新局部变量以后,这些数据可能发生变化。示例让 ClassWriter 自动计算:1
ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS
这非常适合入门示例,但不是说以后就完全不用理解栈帧。复杂项目里,计算 frame 时可能需要解析类型的共同父类,从而牵涉目标 ClassLoader;不恰当地加载业务类,还可能带来类初始化或类加载隔离问题。Arthas 和 ByteKit 里有不少代码,就是在处理这些工程细节。
运行 ASM 示例
进入示例目录并构建:1
2cd source/code/asm-bytekit-arthas-demo
mvn clean package
先运行没有增强的版本:1
./run.sh baseline 4 10
输出只有应用自己的日志:1
2
3
4
5
6demo pid=21257
iterations=4
[APP] order created: user=user-1, sku=SKU-USER-1, quantity=1
[APP] order created: user=user-2, sku=SKU-USER-2, quantity=2
[APP] order created: user=user-3, sku=SKU-USER-3, quantity=3
[APP] failed: user cannot be error
再带上 ASM Agent:1
./run.sh asm 4 10
可以看到业务源码没有改变,但 placeOrder() 前后已经出现了 Agent 插入的输出:1
2
3
4
5
6
7
8
9[ASM agent] installed
[ASM agent] transforming com/nicksxs/bytecode/demo/app/DemoService
[ASM] enter placeOrder user=user-1, quantity=1
[ASM] return placeOrder cost=114967458 ns
[APP] order created: user=user-1, sku=SKU-USER-1, quantity=1
...
[ASM] enter placeOrder user=error, quantity=4
[ASM] throw placeOrder cost=478125 ns
[APP] failed: user cannot be error
这一层已经把字节码增强的基本原理跑通了。不过问题也很明显:为了获取两个参数和一个耗时,我们写了不少类型、descriptor、局部变量和指令生成代码。如果要支持任意方法的参数、返回值、异常和内部调用,代码量会迅速增加。
ByteKit 就是为了解决这一层的重复劳动。
第二层:ByteKit 把字节码操作变成诊断语义
ByteKit 官方对自己的定位很明确:
基于 ASM 提供更高层的字节码处理能力,面向诊断和 APM 领域,而不是一套通用字节码库。
所以 ByteKit 并没有取代 ASM。它做的是把诊断工具经常需要的动作整理成更容易描述的概念。
注入点解决“在哪里执行”
常见注入点包括:
| 注解 | 对应位置 |
|---|---|
@AtEnter | 方法刚进入 |
@AtExit | 方法正常返回 |
@AtExceptionExit | 方法异常退出 |
@AtInvoke | 方法内部调用另一个方法 |
@AtInvokeException | 内部调用抛出异常 |
@AtLine | 指定源码行 |
@AtFieldAccess | 访问字段 |
@AtSyncEnter / @AtSyncExit | 进入或退出同步块 |
ASM 层要自己寻找 RETURN、ATHROW、MethodInsnNode 或行号节点;ByteKit 层可以先说清楚“我要方法退出点”或者“我要内部调用结束点”。
Binding 解决“在这里能拿到什么”
注入点确定以后,还需要把运行时数据交给增强逻辑:
| Binding | 得到的数据 |
|---|---|
@Binding.This | 当前对象 this |
@Binding.Args | 方法参数数组 |
@Binding.Return | 返回值 |
@Binding.Throwable | 抛出的异常 |
@Binding.MethodName | 方法名 |
@Binding.MethodDesc | 方法 descriptor |
@Binding.Field | 当前对象的指定字段 |
@Binding.InvokeArgs | 被调用方法的参数 |
@Binding.InvokeReturn | 被调用方法的返回值 |
本文的 ByteKit 拦截器就变得非常直观:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23public class DemoInterceptor {
public static void atEnter(
.MethodName String methodName,
.Args Object[] args) {
System.out.println("[ByteKit] enter "
+ methodName + " args=" + Arrays.toString(args));
}
public static void atExit(.Return Object returnObject) {
System.out.println("[ByteKit] return " + returnObject);
}
public static void atExceptionExit(
.Throwable Throwable throwable) {
System.out.println("[ByteKit] throw "
+ throwable.getClass().getSimpleName()
+ ": " + throwable.getMessage());
}
}
同样是获取参数、返回值和异常,这段代码比直接写 ASM 更接近我们的真实意图。
ByteKit 不是通过反射“监听”方法
看到这些注解,很容易把它想成 Spring AOP 那样的代理调用。实际上 ByteKit 处理的仍然是目标方法本身的字节码。
示例的 Transformer 核心逻辑是:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22DefaultInterceptorClassParser parser =
new DefaultInterceptorClassParser();
List<InterceptorProcessor> interceptors =
parser.parse(DemoInterceptor.class);
ClassNode classNode = new ClassNode();
ClassReader reader = AsmUtils.toClassNode(
classfileBuffer, classNode);
for (MethodNode methodNode : classNode.methods) {
if (!"placeOrder".equals(methodNode.name)) {
continue;
}
MethodProcessor methodProcessor =
new MethodProcessor(classNode, methodNode);
for (InterceptorProcessor interceptor : interceptors) {
interceptor.process(methodProcessor);
}
}
return AsmUtils.toBytes(classNode, loader, reader);
它大致经历四步:
- 解析拦截器上的
@AtEnter、@AtExit和 Binding; - 用 ASM Tree API 把原始 class 读成
ClassNode; - 找到目标
MethodNode,由MethodProcessor插入指令; - 再把修改后的节点树写回
byte[]。
ByteKit 的价值不是绕过字节码,而是把一批成熟的字节码生成规则封装起来。
inline 到底内联了什么
示例的注解都设置了:1
inline = true
可以把它理解成:增强后并不是简单地在业务方法里调用:1
DemoInterceptor.atEnter(...);
而是尽量把拦截器方法里的指令复制到目标方法中。这样做可以减少一次回调调用,也降低目标 ClassLoader 必须能加载拦截器类的要求。
不过内联也不是免费午餐。增强逻辑太大,会让目标方法字节码膨胀;增强代码引用的其他类型,仍然需要目标类加载器能够解析。因此诊断插桩通常应该保持短小,把复杂状态管理放在稳定的桥接 API 后面。
为什么 ByteKit 有防重复增强
诊断工具可能在同一个类上连续执行 watch、trace 和 monitor。如果每次都无条件再插一遍入口和出口代码,目标方法会越来越大,输出也会重复。
ByteKit 提供了位置过滤机制,可以检查某个注入点是否已经存在。Arthas 的 Enhancer 还会专门检查目标方法中是否已经调用 SpyAPI.atEnter、atExit 和 atExceptionExit,避免重复插入同类探针。
这是 ASM 示例和真正工程化诊断工具之间很典型的差距:基础 API 能完成一次修改,框架还要负责多次修改之间的组合、去重和恢复。
运行 ByteKit 示例
执行:1
./run.sh bytekit 4 10
本文实际验证得到:1
2
3
4
5
6
7
8
9[ByteKit agent] installed
[ByteKit agent] transforming com/nicksxs/bytecode/demo/app/DemoService
[ByteKit] enter placeOrder args=[user-1, 1]
[ByteKit] return order created: user=user-1, sku=SKU-USER-1, quantity=1
[APP] order created: user=user-1, sku=SKU-USER-1, quantity=1
...
[ByteKit] enter placeOrder args=[error, 4]
[ByteKit] throw IllegalArgumentException: user cannot be error
[APP] failed: user cannot be error
到这里,我们已经用两种方式修改了同一个方法:1
2
3ASM:自己生成每一段参数加载、时间计算和输出指令
ByteKit:声明注入点和需要绑定的数据,由 ByteKit 生成指令
但这两种方式仍然需要我们自己写 Agent、打包、指定目标类并启动应用。线上排障真正想要的是:输入一条命令,就临时观察某个方法。
这就是 Arthas 所处的第三层。
第三层:Arthas 把动态增强变成排障命令
先启动不带任何 Agent 的 Demo,并让它持续执行:1
./run.sh app
另开一个终端,启动 Arthas:1
2curl -O https://arthas.aliyun.com/arthas-boot.jar
java -jar arthas-boot.jar --use-version 4.3.2
选择:1
com.nicksxs.bytecode.demo.app.DemoApplication
这一步之后,Arthas Agent 已经通过 Attach API 进入目标 JVM,并拿到了 Instrumentation。
第一步先确认类和 ClassLoader
1 | sc -d com.nicksxs.bytecode.demo.app.DemoService |
本文测试时的关键输出是:1
2
3
4class-info com.nicksxs.bytecode.demo.app.DemoService
code-source .../asm-bytekit-arthas-demo/target/classes/
class-loader +-sun.misc.Launcher$AppClassLoader@73d16e93
classLoaderHash 73d16e93
这一步并不多余。同一个类名可能被多个 ClassLoader 各加载一份,特别是在 Tomcat、插件系统和热部署环境里。如果不先确认 ClassLoader,后面的命令可能命中错误的类,或者提示匹配到了多个类。
也可以继续查看方法:1
sm com.nicksxs.bytecode.demo.app.DemoService
watch 观察一次方法的输入和输出
执行:1
2watch com.nicksxs.bytecode.demo.app.DemoService placeOrder \
'{params,returnObj,throwExp}' -x 2 -n 4
几个参数分别表示:
| 参数 | 含义 |
|---|---|
params | 当前调用的参数数组 |
returnObj | 正常返回值 |
throwExp | 异常,没有异常时为 null |
-x 2 | 对象展开深度为 2 |
-n 4 | 命中 4 次后自动退出 |
实际输出之一:1
2
3
4
5
6
7
8
9method=com.nicksxs.bytecode.demo.app.DemoService.placeOrder location=AtExit
[cost=123.336292ms] result=@ArrayList[
@Object[][
@String[user-25],
@Integer[25],
],
@String[order created: user=user-25, sku=SKU-USER-25, quantity=25],
null,
]
可以看到,watch 输出里的 location=AtExit,和 ByteKit 的 @AtExit 已经能对应起来。这里出现的 params、returnObj 和 throwExp,本质上也来自方法入口、正常出口和异常出口处插入的探针。
trace 为什么能看到内部调用
执行:1
2trace com.nicksxs.bytecode.demo.app.DemoService placeOrder \
'#cost > 0' -n 2
本文实测输出:1
2
3
4`---[116.058125ms] DemoService:placeOrder()
+---[25.93% 30.089459ms ] DemoService:validateQuantity() #10
+---[38.65% 44.852583ms ] DemoService:queryInventory() #11
`---[35.08% 40.714208ms ] DemoService:saveOrder() #12
异常分支则是:1
2
3`---[0.902ms] DemoService:placeOrder() [throws Exception]
`---throw:java.lang.IllegalArgumentException #7
[user cannot be error]
watch 主要关心目标方法的入口和出口,trace 还需要在方法内部的调用指令前后插入探针。对应到 ByteKit,就是 @AtInvoke、调用完成和调用异常等位置。
Arthas 源码里的 Enhancer 在 isTracing 为真时,会加入 SpyTraceInterceptor,遍历目标方法里的调用指令,并为每个调用注册 trace listener。最终看到的调用树,是这些 before/after/exception 事件按线程和调用层级组合出来的。
jad 看到的是 JVM 当前认识的类
1 | jad com.nicksxs.bytecode.demo.app.DemoService placeOrder |
它会把目标 JVM 中的 class 反编译出来,而不是去源码目录读取 DemoService.java。这在排查“代码明明改了,线上为什么还是旧逻辑”“同名 jar 到底加载了哪一份”时很有用。
本文 Demo 得到:1
2
3
4
5
6
7
8public String placeOrder(String user, int quantity) {
if ("error".equals(user)) {
throw new IllegalArgumentException("user cannot be error");
}
this.validateQuantity(quantity);
String sku = this.queryInventory(user);
return this.saveOrder(user, sku, quantity);
}
需要注意,watch -n 或 trace -n 达到次数退出后,对应 listener 会结束,Arthas 也可能重新组织该类上的增强。因此 jad 展示的是执行命令当时 JVM 中有效的字节码状态,不应该把它简单理解成磁盘源码查看器。
诊断结束为什么要 reset
1 | reset com.nicksxs.bytecode.demo.app.DemoService |
watch、trace、monitor 不是在 JVM 外面被动偷看,它们会修改目标类。诊断结束后执行 reset,可以移除 Arthas 对这个类的增强,让方法回到未观测状态。
官方文档也特别提醒,线上使用增强命令时要缩小类和方法匹配范围,并在结束后执行 reset 或 stop。
从源码看 Arthas 是怎样使用 ByteKit 的
如果只看命令,很容易觉得 Arthas 内部可能有一套完全不同的魔法。直接看 com.taobao.arthas.core.advisor.Enhancer,链路就很清楚了。
首先,Enhancer 自己就是:1
public class Enhancer implements ClassFileTransformer
它收到 classfileBuffer 后,用 ByteKit 提供的 AsmUtils 读取:1
2
3ClassNode classNode = new ClassNode(Opcodes.ASM9);
ClassReader classReader = AsmUtils.toClassNode(
classfileBuffer, classNode);
然后解析 Arthas 自己定义的拦截器:1
2
3
4
5
6
7
8
9DefaultInterceptorClassParser parser =
new DefaultInterceptorClassParser();
interceptorProcessors.addAll(
parser.parse(SpyInterceptor1.class));
interceptorProcessors.addAll(
parser.parse(SpyInterceptor2.class));
interceptorProcessors.addAll(
parser.parse(SpyInterceptor3.class));
如果是 trace,还会加入 SpyTraceInterceptor。找到匹配方法后,再执行:1
2
3
4
5
6MethodProcessor methodProcessor =
new MethodProcessor(classNode, methodNode, locationFilter);
for (InterceptorProcessor interceptor : interceptorProcessors) {
interceptor.process(methodProcessor);
}
最后重新生成 class:1
2
3byte[] enhanced = AsmUtils.toBytes(
classNode, inClassLoader, classReader);
return enhanced;
这和我们写的 ByteKit Demo 几乎是同一条主干,只是 Arthas 多了大量工程能力:
- 类名、方法名和 ClassLoader 匹配;
- 多命令 listener 管理;
- 防止重复插桩;
- trace 内部调用注册;
- CGLIB 构造函数等特殊 class 的处理;
- 原始类和增强状态管理;
- 命令退出后的恢复;
- 把事件送回 Arthas 自己的 ClassLoader。
因此可以把它们的关系写成:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19用户输入 watch / trace
|
v
Arthas 创建 Matcher 和 AdviceListener
|
v
Enhancer 作为 ClassFileTransformer
|
v
ByteKit 把 AtEnter / AtExit / AtInvoke 转换为增强动作
|
v
ASM 读取、修改并写回 class byte[]
|
v
Instrumentation.retransformClasses()
|
v
JVM 执行插入了 SpyAPI 调用的新方法
SpyAPI 为什么要放在特殊位置
Arthas 自己的核心类由独立 ClassLoader 加载,而业务类可能来自 AppClassLoader、WebAppClassLoader 或自定义 ClassLoader。业务方法增强后如果直接引用 Arthas Core 里的某个类,业务 ClassLoader 不一定能找到它。
Arthas 使用 java.arthas.SpyAPI 作为一座稳定的桥:1
2
3
4
5
6
7
8
9业务 ClassLoader 中被增强的方法
|
| SpyAPI.atEnter / atExit
v
可被目标类访问的 SpyAPI
|
| 转发事件
v
Arthas AdviceListener
Enhancer.transform() 的开头还会检查目标 ClassLoader 能否加载 SpyAPI,不能加载就放弃增强。这正是字节码工具里一个很现实的问题:生成一条方法调用指令并不难,难的是确保这条指令在目标类的加载环境里永远能解析成功。
我们的小 Demo 把 Agent 和应用都放在同一个 classpath 中,所以感觉不到这种复杂度。真正的诊断工具必须认真处理 ClassLoader 边界。
ASM、ByteKit、Arthas 和 Javassist 怎么选
之前的文章使用 Javassist,通过字符串形式插入代码:1
2method.insertBefore("System.out.println($args);");
method.insertAfter("System.out.println($_);");
它的优势是接近 Java 源码,入门很直观。把它和本文三个层次放在一起,可以这样理解:
| 工具 | 最适合的场景 | 优点 | 需要承担的复杂度 |
|---|---|---|---|
| Javassist | 快速生成或修改类、简单 Agent | 接近源码表达,容易上手 | 字符串代码、类型和复杂控制流处理 |
| ASM | 框架底层、需要精确控制和低开销 | 能控制每条指令,生态基础扎实 | 栈、frame、descriptor、异常表 |
| ByteKit | APM、诊断类插桩框架 | 注入点和 Binding 丰富,适合复用 | 仍要负责 Agent、匹配、生命周期 |
| Arthas | 直接排查运行中的 Java 应用 | 开箱即用、动态挂载、命令完整 | 需要控制线上影响和匹配范围 |
如果只是想确认线上某个参数或耗时,优先用 Arthas,而不是现场写 ASM。
如果在开发自己的诊断或 APM 能力,ByteKit 能减少大量通用插桩工作。
如果 ByteKit 没有提供需要的注入位置,或者正在实现更底层的字节码框架,再下沉到 ASM。
理解 ASM 的意义,不是以后所有事情都手写指令,而是知道上层工具究竟替我们处理了哪些问题。
retransform 也不是任意热更新
Instrumentation.retransformClasses() 可以替换方法实现,但它不是完整的类结构热更新。通常不能在重转换时随意:
- 新增或删除字段;
- 新增或删除方法;
- 修改方法签名;
- 改变继承关系;
- 改变 nest host、record component 等类结构属性。
watch 和 trace 只在已有方法内部增加指令,没有改变类的 schema,所以适合使用 retransform。
这也解释了为什么 Arthas 的 mc 加 retransform 能修改已有方法逻辑,却不能把一个全新的字段热塞进已经加载的类。限制来自 JVM 类重定义规则,不是 Arthas 少实现了一个参数。
多个 Agent 同时工作时发生什么
一个 JVM 可以注册多个 ClassFileTransformer。类加载或重转换时,它们会按照 JVM 规定的顺序依次收到字节码,后面的 Transformer 看到的可能已经是前面修改过的结果。1
2
3
4
5
6
7
8
9原始 class
|
APM Transformer
|
覆盖率 Transformer
|
Arthas Transformer
|
最终 class
如果某个工具:
- 生成了不合法的 frame;
- 没有保留其他工具插入的指令;
- 重复增强同一位置;
- 对异常表和构造函数处理不兼容;
就可能出现 VerifyError、ClassFormatError、方法过大,或者某个 Agent 的增强被另一个覆盖。
因此线上已经运行 SkyWalking、Pinpoint、JaCoCo 或安全 Agent 时,使用 Arthas 增强命令要更加谨慎。先精确匹配一个方法、限制命中次数,再观察是否存在兼容性问题。
字节码增强的性能开销在哪里
“没有重启应用”不等于“完全没有运行影响”。主要开销可以分成三部分。
重转换本身的开销
类被 retransform 后,JVM 之前为它生成的 JIT 编译代码可能失效,相关方法需要重新解释执行并再次编译。因此刚增强或刚 reset 后,可能看到 JIT 编译线程活跃,方法性能也可能短暂波动。
每次调用探针的开销
入口、出口和内部调用都会多执行一些指令。trace 的粒度比只观察一次出口的 watch 更细,通常也会有更多事件。
输出和表达式计算的开销
把一个很大的对象用 -x 5 展开、执行复杂 OGNL 表达式、对高频方法持续输出,开销往往比那几条入口探针本身更大。
所以线上使用时建议:1
2
3
4
5精确类名 > 大范围通配符
精确方法名 > 整个类所有方法
带 -n 次数限制 > 一直运行
较小的 -x 展开深度 > 深度遍历对象图
带条件表达式 > 输出所有请求
例如只观察慢于 100 毫秒的调用:1
2watch com.nicksxs.bytecode.demo.app.DemoService placeOrder \
'{params,returnObj,throwExp}' '#cost > 100' -x 2 -n 5
把整条链路再走一遍
现在回到最开始的问题:不修改源码,怎样观察 placeOrder()?
使用 ASM
我们自己完成:
- Agent 注册 Transformer;
- 匹配类名、方法名和 descriptor;
- 找到方法入口和每一个退出指令;
- 管理局部变量和操作数栈;
- 生成新的 class 字节数组。
使用 ByteKit
仍然需要 Agent 和 Transformer,但注入逻辑变成:1
2
3 + .Args
+ .Return
+ .Throwable
ByteKit 再把这些声明转换为 ASM 指令。
使用 Arthas
只需要输入:1
2watch ...
trace ...
Arthas 负责动态 Attach、类和方法匹配、ByteKit 插桩、listener 管理、结果展示以及恢复。
所以三者最准确的关系并不是:1
ASM vs ByteKit vs Arthas
而是:1
2
3
4Arthas
使用 ByteKit 表达诊断插桩
使用 ASM 操作字节码
通过 Instrumentation 交给 JVM
理解到这一层以后,再看 watch 输出里的 AtExit、trace 的内部调用树,以及 Agent 里的 ClassFileTransformer,它们就不再是几个零散的名词,而是同一条执行链路上的不同环节。
完整工程源码
前面为了讲清原理,只截取了每个环节最关键的代码。下面通过 Hexo 的 include_code 标签直接加载本地工程文件,博客页面会把它们渲染成完整的代码块。
工程结构如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15asm-bytekit-arthas-demo/
├── pom.xml
├── run.sh
├── arthas-commands.txt
└── src/main/java/com/nicksxs/bytecode/demo/
├── app/
│ ├── DemoApplication.java
│ └── DemoService.java
├── asm/
│ ├── AsmAgent.java
│ └── AsmTimingTransformer.java
└── bytekit/
├── ByteKitAgent.java
├── ByteKitTransformer.java
└── DemoInterceptor.java
Maven 配置
pom.xml 中除了 ASM 和 ByteKit 依赖,还通过 maven-jar-plugin 分别生成两个带不同 Premain-Class 的 Agent JAR。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.nicksxs</groupId>
<artifactId>asm-bytekit-arthas-demo</artifactId>
<version>1.0.0</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<asm.version>9.9.1</asm.version>
<bytekit.version>0.1.7</bytekit.version>
</properties>
<dependencies>
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm</artifactId>
<version>${asm.version}</version>
</dependency>
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm-commons</artifactId>
<version>${asm.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>bytekit-core</artifactId>
<version>${bytekit.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.14.1</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.4.2</version>
<executions>
<execution>
<id>asm-agent</id>
<phase>package</phase>
<goals>
<goal>jar</goal>
</goals>
<configuration>
<classifier>asm-agent</classifier>
<archive>
<manifestEntries>
<Premain-Class>com.nicksxs.bytecode.demo.asm.AsmAgent</Premain-Class>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
</configuration>
</execution>
<execution>
<id>bytekit-agent</id>
<phase>package</phase>
<goals>
<goal>jar</goal>
</goals>
<configuration>
<classifier>bytekit-agent</classifier>
<archive>
<manifestEntries>
<Premain-Class>com.nicksxs.bytecode.demo.bytekit.ByteKitAgent</Premain-Class>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>3.8.1</version>
<executions>
<execution>
<id>copy-runtime-dependencies</id>
<phase>package</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
示例应用
DemoApplication 会循环调用 DemoService.placeOrder()。每四次调用构造一次异常,方便同时验证正常返回和异常退出。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34package com.nicksxs.bytecode.demo.app;
import java.lang.management.ManagementFactory;
public class DemoApplication {
public static void main(String[] args) throws Exception {
int iterations = args.length > 0 ? Integer.parseInt(args[0]) : 5;
long pauseMillis = args.length > 1 ? Long.parseLong(args[1]) : 500L;
System.out.println("demo pid=" + currentPid());
System.out.println("iterations=" + (iterations == 0 ? "infinite" : iterations));
DemoService service = new DemoService();
int index = 1;
while (iterations == 0 || index <= iterations) {
String user = index % 4 == 0 ? "error" : "user-" + index;
try {
String result = service.placeOrder(user, index);
System.out.println("[APP] " + result);
} catch (Exception e) {
System.out.println("[APP] failed: " + e.getMessage());
}
index++;
Thread.sleep(pauseMillis);
}
}
private static String currentPid() {
String runtimeName = ManagementFactory.getRuntimeMXBean().getName();
int separator = runtimeName.indexOf('@');
return separator > 0 ? runtimeName.substring(0, separator) : runtimeName;
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40package com.nicksxs.bytecode.demo.app;
public class DemoService {
public String placeOrder(String user, int quantity) {
if ("error".equals(user)) {
throw new IllegalArgumentException("user cannot be error");
}
validateQuantity(quantity);
String sku = queryInventory(user);
return saveOrder(user, sku, quantity);
}
private void validateQuantity(int quantity) {
sleep(20L);
if (quantity <= 0) {
throw new IllegalArgumentException("quantity must be positive");
}
}
private String queryInventory(String user) {
sleep(40L);
return "SKU-" + user.toUpperCase();
}
private String saveOrder(String user, String sku, int quantity) {
sleep(30L);
return "order created: user=" + user + ", sku=" + sku + ", quantity=" + quantity;
}
private void sleep(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IllegalStateException("interrupted", e);
}
}
}
ASM Agent
Agent 入口只负责注册 Transformer,具体的类匹配和指令生成都在 AsmTimingTransformer 中。1
2
3
4
5
6
7
8
9
10
11package com.nicksxs.bytecode.demo.asm;
import java.lang.instrument.Instrumentation;
public class AsmAgent {
public static void premain(String agentArgs, Instrumentation instrumentation) {
System.out.println("[ASM agent] installed");
instrumentation.addTransformer(new AsmTimingTransformer(), true);
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115package com.nicksxs.bytecode.demo.asm;
import java.io.PrintStream;
import java.lang.instrument.ClassFileTransformer;
import java.security.ProtectionDomain;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.commons.AdviceAdapter;
import org.objectweb.asm.commons.Method;
public class AsmTimingTransformer implements ClassFileTransformer {
private static final String TARGET_CLASS = "com/nicksxs/bytecode/demo/app/DemoService";
private static final String TARGET_METHOD = "placeOrder";
private static final String TARGET_DESCRIPTOR = "(Ljava/lang/String;I)Ljava/lang/String;";
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer) {
if (!TARGET_CLASS.equals(className)) {
return null;
}
System.out.println("[ASM agent] transforming " + className);
ClassReader reader = new ClassReader(classfileBuffer);
ClassWriter writer = new ClassWriter(reader, ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS);
ClassVisitor visitor = new ClassVisitor(Opcodes.ASM9, writer) {
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature,
String[] exceptions) {
MethodVisitor methodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions);
if (!TARGET_METHOD.equals(name) || !TARGET_DESCRIPTOR.equals(descriptor)) {
return methodVisitor;
}
return new TimingAdvice(methodVisitor, access, name, descriptor);
}
};
reader.accept(visitor, ClassReader.EXPAND_FRAMES);
return writer.toByteArray();
}
private static final class TimingAdvice extends AdviceAdapter {
private static final Type SYSTEM_TYPE = Type.getType(System.class);
private static final Type PRINT_STREAM_TYPE = Type.getType(PrintStream.class);
private static final Type STRING_BUILDER_TYPE = Type.getType(StringBuilder.class);
private static final Type STRING_TYPE = Type.getType(String.class);
private static final Method NANO_TIME = new Method("nanoTime", Type.LONG_TYPE, new Type[0]);
private static final Method STRING_BUILDER_INIT = new Method("<init>", Type.VOID_TYPE, new Type[0]);
private static final Method APPEND_STRING = new Method("append", STRING_BUILDER_TYPE,
new Type[] { STRING_TYPE });
private static final Method APPEND_INT = new Method("append", STRING_BUILDER_TYPE,
new Type[] { Type.INT_TYPE });
private static final Method APPEND_LONG = new Method("append", STRING_BUILDER_TYPE,
new Type[] { Type.LONG_TYPE });
private static final Method TO_STRING = new Method("toString", STRING_TYPE, new Type[0]);
private static final Method PRINTLN = new Method("println", Type.VOID_TYPE, new Type[] { STRING_TYPE });
private int startNanosLocal;
private TimingAdvice(MethodVisitor methodVisitor, int access, String name, String descriptor) {
super(Opcodes.ASM9, methodVisitor, access, name, descriptor);
}
protected void onMethodEnter() {
invokeStatic(SYSTEM_TYPE, NANO_TIME);
startNanosLocal = newLocal(Type.LONG_TYPE);
storeLocal(startNanosLocal);
getStatic(SYSTEM_TYPE, "out", PRINT_STREAM_TYPE);
newInstance(STRING_BUILDER_TYPE);
dup();
invokeConstructor(STRING_BUILDER_TYPE, STRING_BUILDER_INIT);
push("[ASM] enter placeOrder user=");
invokeVirtual(STRING_BUILDER_TYPE, APPEND_STRING);
loadArg(0);
invokeVirtual(STRING_BUILDER_TYPE, APPEND_STRING);
push(", quantity=");
invokeVirtual(STRING_BUILDER_TYPE, APPEND_STRING);
loadArg(1);
invokeVirtual(STRING_BUILDER_TYPE, APPEND_INT);
invokeVirtual(STRING_BUILDER_TYPE, TO_STRING);
invokeVirtual(PRINT_STREAM_TYPE, PRINTLN);
}
protected void onMethodExit(int opcode) {
invokeStatic(SYSTEM_TYPE, NANO_TIME);
loadLocal(startNanosLocal);
math(SUB, Type.LONG_TYPE);
int elapsedLocal = newLocal(Type.LONG_TYPE);
storeLocal(elapsedLocal);
getStatic(SYSTEM_TYPE, "out", PRINT_STREAM_TYPE);
newInstance(STRING_BUILDER_TYPE);
dup();
invokeConstructor(STRING_BUILDER_TYPE, STRING_BUILDER_INIT);
push(opcode == ATHROW ? "[ASM] throw placeOrder cost=" : "[ASM] return placeOrder cost=");
invokeVirtual(STRING_BUILDER_TYPE, APPEND_STRING);
loadLocal(elapsedLocal);
invokeVirtual(STRING_BUILDER_TYPE, APPEND_LONG);
push(" ns");
invokeVirtual(STRING_BUILDER_TYPE, APPEND_STRING);
invokeVirtual(STRING_BUILDER_TYPE, TO_STRING);
invokeVirtual(PRINT_STREAM_TYPE, PRINTLN);
}
}
}
ByteKit Agent
ByteKit 版本同样由 Agent 注册 Transformer,但插入点和运行时数据改用注解与 Binding 描述。1
2
3
4
5
6
7
8
9
10
11package com.nicksxs.bytecode.demo.bytekit;
import java.lang.instrument.Instrumentation;
public class ByteKitAgent {
public static void premain(String agentArgs, Instrumentation instrumentation) {
System.out.println("[ByteKit agent] installed");
instrumentation.addTransformer(new ByteKitTransformer(), true);
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49package com.nicksxs.bytecode.demo.bytekit;
import java.lang.instrument.ClassFileTransformer;
import java.security.ProtectionDomain;
import java.util.List;
import com.alibaba.bytekit.asm.MethodProcessor;
import com.alibaba.bytekit.asm.interceptor.InterceptorProcessor;
import com.alibaba.bytekit.asm.interceptor.parser.DefaultInterceptorClassParser;
import com.alibaba.bytekit.utils.AsmUtils;
import com.alibaba.deps.org.objectweb.asm.ClassReader;
import com.alibaba.deps.org.objectweb.asm.tree.ClassNode;
import com.alibaba.deps.org.objectweb.asm.tree.MethodNode;
public class ByteKitTransformer implements ClassFileTransformer {
private static final String TARGET_CLASS = "com/nicksxs/bytecode/demo/app/DemoService";
private static final String TARGET_METHOD = "placeOrder";
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer) {
if (!TARGET_CLASS.equals(className)) {
return null;
}
try {
System.out.println("[ByteKit agent] transforming " + className);
DefaultInterceptorClassParser parser = new DefaultInterceptorClassParser();
List<InterceptorProcessor> interceptors = parser.parse(DemoInterceptor.class);
ClassNode classNode = new ClassNode();
ClassReader reader = AsmUtils.toClassNode(classfileBuffer, classNode);
for (MethodNode methodNode : classNode.methods) {
if (!TARGET_METHOD.equals(methodNode.name)) {
continue;
}
MethodProcessor methodProcessor = new MethodProcessor(classNode, methodNode);
for (InterceptorProcessor interceptor : interceptors) {
interceptor.process(methodProcessor);
}
}
return AsmUtils.toBytes(classNode, loader, reader);
} catch (Throwable throwable) {
throwable.printStackTrace(System.err);
return null;
}
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27package com.nicksxs.bytecode.demo.bytekit;
import java.util.Arrays;
import com.alibaba.bytekit.asm.binding.Binding;
import com.alibaba.bytekit.asm.interceptor.annotation.AtEnter;
import com.alibaba.bytekit.asm.interceptor.annotation.AtExceptionExit;
import com.alibaba.bytekit.asm.interceptor.annotation.AtExit;
public class DemoInterceptor {
public static void atEnter(.MethodName String methodName, .Args Object[] args) {
System.out.println("[ByteKit] enter " + methodName + " args=" + Arrays.toString(args));
}
public static void atExit(.Return Object returnObject) {
System.out.println("[ByteKit] return " + returnObject);
}
public static void atExceptionExit(.Throwable Throwable throwable) {
System.out.println("[ByteKit] throw " + throwable.getClass().getSimpleName()
+ ": " + throwable.getMessage());
}
}
运行脚本和 Arthas 命令
run.sh 把原始应用、ASM Agent、ByteKit Agent 和 Arthas 目标应用四种运行方式放在了一起。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "$0")" && pwd)"
MODE="${1:-baseline}"
ITERATIONS="${2:-5}"
PAUSE_MILLIS="${3:-300}"
MVN_BIN="${MVN_BIN:-mvn}"
JAVA_BIN="${JAVA_BIN:-java}"
if [[ ! -d "$ROOT_DIR/target/classes" ]]; then
if ! command -v "$MVN_BIN" >/dev/null 2>&1; then
echo "mvn was not found. Install Maven or set MVN_BIN to its absolute path." >&2
exit 1
fi
"$MVN_BIN" -q -f "$ROOT_DIR/pom.xml" package
fi
CLASSPATH="$ROOT_DIR/target/classes:$ROOT_DIR/target/dependency/*"
MAIN_CLASS="com.nicksxs.bytecode.demo.app.DemoApplication"
VERSION="1.0.0"
case "$MODE" in
baseline)
exec "$JAVA_BIN" -cp "$CLASSPATH" "$MAIN_CLASS" "$ITERATIONS" "$PAUSE_MILLIS"
;;
asm)
exec "$JAVA_BIN" \
-javaagent:"$ROOT_DIR/target/asm-bytekit-arthas-demo-$VERSION-asm-agent.jar" \
-cp "$CLASSPATH" "$MAIN_CLASS" "$ITERATIONS" "$PAUSE_MILLIS"
;;
bytekit)
exec "$JAVA_BIN" \
-javaagent:"$ROOT_DIR/target/asm-bytekit-arthas-demo-$VERSION-bytekit-agent.jar" \
-cp "$CLASSPATH" "$MAIN_CLASS" "$ITERATIONS" "$PAUSE_MILLIS"
;;
app)
exec "$JAVA_BIN" -cp "$CLASSPATH" "$MAIN_CLASS" 0 1000
;;
*)
echo "usage: $0 {baseline|asm|bytekit|app} [iterations] [pauseMillis]" >&2
exit 1
;;
esac1
2
3
4
5
6
7
8# Run ./run.sh app first, then attach Arthas to DemoApplication.
sc -d com.nicksxs.bytecode.demo.app.DemoService
sm com.nicksxs.bytecode.demo.app.DemoService
watch com.nicksxs.bytecode.demo.app.DemoService placeOrder '{params,returnObj,throwExp}' -x 2 -n 4
trace com.nicksxs.bytecode.demo.app.DemoService placeOrder '#cost > 0' -n 3
jad com.nicksxs.bytecode.demo.app.DemoService placeOrder
reset com.nicksxs.bytecode.demo.app.DemoService
完整验证清单
示例目录下依次执行:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16cd source/code/asm-bytekit-arthas-demo
# 编译应用和两个 Agent
mvn clean package
# 1. 原始应用
./run.sh baseline 4 10
# 2. ASM 增强
./run.sh asm 4 10
# 3. ByteKit 增强
./run.sh bytekit 4 10
# 4. 为 Arthas 保持应用运行
./run.sh app
另一个终端中:1
2curl -O https://arthas.aliyun.com/arthas-boot.jar
java -jar arthas-boot.jar --use-version 4.3.2
挂载后依次执行:1
2
3
4
5
6
7
8
9
10
11sc -d com.nicksxs.bytecode.demo.app.DemoService
watch com.nicksxs.bytecode.demo.app.DemoService placeOrder \
'{params,returnObj,throwExp}' -x 2 -n 4
trace com.nicksxs.bytecode.demo.app.DemoService placeOrder \
'#cost > 0' -n 3
jad com.nicksxs.bytecode.demo.app.DemoService placeOrder
reset com.nicksxs.bytecode.demo.app.DemoService
到这里,ASM、ByteKit 和 Arthas 的三个层次就都不只是概念,而是可以在同一个业务方法上亲手验证的结果了。