JVM 对于每个Java 开发者都不会太陌生,但是你真的弄懂了JVM吗?人人都知道要做JVM调优,但是如果能在Java层面直接对话JVM,那将能完成很多你以前无法完成的操作.虽然部分功能通过纯Java的方式也能轻松做到.但中间多出的抽象成本也是非常大的.这样来看,与虚拟机对话似乎是必须的.
想要对其深入了解,JVM相关的知识是必不可少的.推荐圣经---<深入理解Java虚拟机>
ASM
ASM作为JDK与Spring中内置的字节码框架,其速度最快,相应的抽象层更少.但毋庸置疑,学会如何熟练使用还是非常必要的.
ASM的介绍
ASM分为三个主要的功能
- Genrate(无中生有---生成新的)
- Tranformation(偷天换日---替换修改)
- Analysis(细致入微---全局分析)
其中又有三个核心类ClassWrite, ClassReader, ClassVisitor.对于Genrate操作,需要ClassWrite与ClassVisitor同时操作.而Transformation操作则需要三者同时协作.对于Analysis只需要ClassReader介入即可.
Generate
既然是生成新的类,那就不可避免要提他的一个关键类Label,它是实现指令流的关键.可以把JVM的指令想象成一条河中的流水,永远从上游向下游动.JVM的指令并不像Java代码一样丰富.关键的循环, try-catch, if, switch...等一系列的操作全都要用到Label类.
而ASM是观察者模式设计而成的,我们在ClassWrite的实例直接调用其中的visitXxx方法,去生成一个类对应的细节.而在JVM中方法内部的逻辑存在于Code属性表中.当通过cw.visitMethod生成一个目标方法后他就会返回一个MethodVisitor的子类对象,用于生成Code属性表中内部的指令(方法体中的逻辑),通过名为visitXxxInsn的有多个参数的方法(但并非绝对,一是根据指令的属性,是否操作数据来决定的因此可能会出现调用单个参数的visitInsn的情况...)进行方法体的构筑.
Label类
而它的使用其实也非常简单,直接new它的对象即可.再通过MethodVisitor在操作Code表中的指令时通过其中的 visitLabel方法设置锚点.通过visitJumpInsn根据判断指令或跳转指令直接跳转到对应的锚点.以下就是生成for循环的例子.
csharp
/**
* 生成的目标类
**/
public class HelloWorld8 {
public HelloWorld8() {
super();
}
public void forTest() {
for(int i = 0; i < 10; i++) {
System.out.print(i);
}
}
}
ini
/**
* ASM字节码研究
* for循环
*/
public class lab08_testDemo08 {
static void main() {
String relative_path = "fileDemo/HelloWorld8.class";
File file = new File(relative_path);
byte[] bytes = dump();
new File(file.getParent()).mkdirs();
try {
Files.write(file.toPath(), bytes);
} catch (Exception e) {
e.printStackTrace();
}
}
public static byte[] dump() {
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
// 生成类
cw.visit(
69,
ACC_PUBLIC + ACC_SUPER,
"fileDemo/HelloWorld8",
null,
"java/lang/Object",
null
);
{
MethodVisitor mv1 = cw.visitMethod(
ACC_PUBLIC,
"<init>",
"()V",
null,
null
);
mv1.visitCode();
mv1.visitVarInsn(ALOAD, 0);
mv1.visitMethodInsn(
INVOKESPECIAL,
"java/lang/Object",
"<init>",
"()V",
false
);
mv1.visitInsn(RETURN);
mv1.visitMaxs(0, 0);
mv1.visitEnd();
}
{
Label conLabel = new Label();
Label endLabel = new Label();
MethodVisitor mv = cw.visitMethod(
ACC_PUBLIC,
"forTest",
"()V",
null,
null
);
mv.visitCode();
// 准备一个0值
mv.visitInsn(ICONST_0);
mv.visitVarInsn(ISTORE, 1);
// 循环开始标记
mv.visitLabel(conLabel);
mv.visitVarInsn(ILOAD, 1);
mv.visitIntInsn(BIPUSH, 10);
// 如果i >= 10跳转
mv.visitJumpInsn(IF_ICMPGE, endLabel);
mv.visitFieldInsn(
GETSTATIC,
"java/lang/System",
"out",
"Ljava/io/PrintStream;"
);
mv.visitVarInsn(ILOAD, 1);
mv.visitMethodInsn(
INVOKEVIRTUAL,
"java/io/PrintStream",
"print",
"(I)V",
false
);
mv.visitIincInsn(1 , 1);
// 正常完成一次循环
mv.visitJumpInsn(GOTO, conLabel);
mv.visitLabel(endLabel);
mv.visitInsn(RETURN);
mv.visitMaxs(0, 0);
mv.visitEnd();
}
cw.visitEnd();
return cw.toByteArray();
}
}
我们可以发现几个全新的的问题,ClassWrite构造方法中的参数是什么?visitMaxs方法的作用?
- 对于ClassWrite中的参数其存在可选项,其中
COMPUTE_FRAMES是最智能的,ASM会帮你自动计算最大栈深,与局部变量表大小.一般选择这个参数即可,其余的参数都不可避免的要进行手动的计算.但在一些特殊的情况,这两个值的计算往往是很复杂的. visitMaxs方法对于每个Code表都是必不可少的,这个方法必须存在.即便你设置了COMPUTE_FRAMES,这个方法也必须调用,但ASM会自动将计算的值进行替换为正确的值,所以可以任意的给值.未开启的话必须计算正确,防止错误.
这里有一个很重要的点,不要陷入思维死结.以后面会提到Transformation做类比.如果说ClassReader是河流的源头,那么ClassWrite就是河流的结束.而一个ClassVisitor并不是一个类的抽象.就像一条大河它不可能一条分支都没有.而ClassVisitor就是如此,它是河流的分支.对于一个类,它可以被多个ClassVisitor访问.每个ClassVisitor只关心自己感兴趣的部分.但它们最终都要汇入主干直到结束.如果有支流没有汇入那就会是Transformation的另一种情况---指令删除.
下面再展示几种Generate的情况
Try-Catch
csharp
/**
* try-catch目标类
**/
public class HelloWorld9 {
public HelloWorld9() {
super();
}
public void forTry() {
try {
System.out.println("Before Sleep");
Thread.sleep(1000L);
System.out.println("After Sleep");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
java
/**
* ASM字节码研究
* try-catch
*/
public class lab09_testDemo09 {
static void main() {
String relative_path = "fileDemo/HelloWorld9.class";
File file = new File(relative_path);
byte[] bytes = dump();
new File(file.getParent()).mkdirs();
try {
Files.write(file.toPath(), bytes);
} catch (Exception e) {
e.printStackTrace();
}
}
public static byte[] dump() {
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
// 生成类
cw.visit(
69,
ACC_PUBLIC + ACC_SUPER,
"fileDemo/HelloWorld9",
null,
"java/lang/Object",
null
);
{
MethodVisitor mv1 = cw.visitMethod(
ACC_PUBLIC,
"<init>",
"()V",
null,
null
);
mv1.visitCode();
mv1.visitVarInsn(ALOAD, 0);
mv1.visitMethodInsn(
INVOKESPECIAL,
"java/lang/Object",
"<init>",
"()V",
false
);
mv1.visitInsn(RETURN);
mv1.visitMaxs(0, 0);
mv1.visitEnd();
}
{
Label startLabel = new Label();
Label endLabel = new Label();
Label handleLabel = new Label();
Label returnLabel = new Label();
MethodVisitor mv = cw.visitMethod(
ACC_PUBLIC,
"forTry",
"()V",
null,
null
);
mv.visitCode();
mv.visitTryCatchBlock(startLabel, endLabel, handleLabel, "java/lang/InterruptedException");
// try部分
mv.visitLabel(startLabel);
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("Before Sleep");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
mv.visitLdcInsn(new Long(1000L));
mv.visitMethodInsn(INVOKESTATIC, "java/lang/Thread", "sleep", "(J)V", false);
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("After Sleep");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
mv.visitLabel(endLabel);
// 未捕获异常,直接跳到结束处
mv.visitJumpInsn(GOTO, returnLabel);
// 捕获异常
mv.visitLabel(handleLabel);
mv.visitVarInsn(ASTORE, 1);
mv.visitVarInsn(ALOAD, 1);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/InterruptedException", "printStackTrace", "()V", false);
mv.visitLabel(returnLabel);
mv.visitInsn(RETURN);
mv.visitMaxs(0, 0);
mv.visitEnd();
}
cw.visitEnd();
return cw.toByteArray();
}
}
Switch
csharp
/**
* switch目标类
**/
public class HelloWorld7 {
public HelloWorld7() {
super();
}
public void choose(int var1) {
switch (var1) {
case 1 -> System.out.println("val = 1");
case 2 -> System.out.println("val = 2");
default -> System.out.println("val is unknown");
}
}
}
ini
/**
* ASM字节码研究
* switch语句跳转
*/
public class lab07_testDemo07 {
static void main() {
String relative_path = "fileDemo/HelloWorld7.class";
File file = new File(relative_path);
byte[] bytes = dump();
new File(file.getParent()).mkdirs();
try {
Files.write(file.toPath(), bytes);
} catch (Exception e) {
e.printStackTrace();
}
}
public static byte[] dump() {
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
// 生成类
cw.visit(
69,
ACC_PUBLIC + ACC_SUPER,
"fileDemo/HelloWorld7",
null,
"java/lang/Object",
null
);
{
MethodVisitor mv1 = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
mv1.visitCode();
mv1.visitVarInsn(ALOAD, 0);
mv1.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
mv1.visitInsn(RETURN);
mv1.visitMaxs(0, 0);
mv1.visitEnd();
}
{
Label label1 = new Label();
Label label2 = new Label();
Label defaultLabel = new Label();
Label returnLabel = new Label();
MethodVisitor mv = cw.visitMethod(
ACC_PUBLIC,
"choose",
"(I)V",
null,
null
);
mv.visitCode();
mv.visitVarInsn(ILOAD, 1);
mv.visitTableSwitchInsn(
1,
2,
defaultLabel,
new Label[]{label1, label2}
);
mv.visitLabel(label1);
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("val = 1");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
mv.visitJumpInsn(GOTO, returnLabel);
mv.visitLabel(label2);
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("val = 2");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
mv.visitJumpInsn(GOTO, returnLabel);
mv.visitLabel(defaultLabel);
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("val is unknown");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
mv.visitLabel(returnLabel);
mv.visitInsn(RETURN);
mv.visitMaxs(0, 0);
mv.visitEnd();
}
cw.visitEnd();
return cw.toByteArray();
}
}
If
csharp
/**
* if语句
**/
public class HelloWorld6 {
public HelloWorld6() {
super();
}
public void check(int var1) {
if (var1 == 0) {
System.out.println("value is 0");
} else {
System.out.println("value is not 0");
}
}
}
java
/**
* ASM字节码研究
* if语句跳转
*/
public class lab06_testDemo06 {
static void main() {
String relative_path = "fileDemo/HelloWorld6.class";
File file = new File(relative_path);
byte[] bytes = dump();
new File(file.getParent()).mkdirs();
try {
Files.write(file.toPath(), bytes);
} catch (Exception e) {
e.printStackTrace();
}
}
public static byte[] dump() {
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
// 生成类
cw.visit(
69,
ACC_PUBLIC + ACC_SUPER,
"fileDemo/HelloWorld6",
null,
"java/lang/Object",
null
);
{
MethodVisitor mv1 = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
mv1.visitCode();
mv1.visitVarInsn(ALOAD, 0);
mv1.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
mv1.visitInsn(RETURN);
mv1.visitMaxs(0, 0);
mv1.visitEnd();
}
{
Label elseLabel = new Label();
Label returnLabel = new Label();
// 生成方法
MethodVisitor mv = cw.visitMethod(
ACC_PUBLIC,
"check",
"(I)V",
null,
null
);
mv.visitCode();
mv.visitVarInsn(ILOAD, 1);
// 不等于0就进行跳转
mv.visitJumpInsn(IFNE, elseLabel);
mv.visitFieldInsn(
GETSTATIC,
"java/lang/System",
"out",
"Ljava/io/PrintStream;"
);
mv.visitLdcInsn("value is 0");
mv.visitMethodInsn(
INVOKEVIRTUAL,
"java/io/PrintStream",
"println",
"(Ljava/lang/String;)V",
false
);
// 执行完第一个分支
mv.visitJumpInsn(GOTO, returnLabel);
// 第二分支起始处
mv.visitLabel(elseLabel);
mv.visitFieldInsn(
GETSTATIC,
"java/lang/System",
"out",
"Ljava/io/PrintStream;"
);
mv.visitLdcInsn("value is not 0");
mv.visitMethodInsn(
INVOKEVIRTUAL,
"java/io/PrintStream",
"println",
"(Ljava/lang/String;)V",
false
);
// 第一分支执行完成跳转处
mv.visitLabel(returnLabel);
mv.visitInsn(RETURN);
mv.visitMaxs(0, 0);
mv.visitEnd();
}
cw.visitEnd();
return cw.toByteArray();
}
}
补充一个点,当没有显式的声明构造方法时,JVM会默认帮你添加一个无参的构造方法.但通过ASM的Generate生成的类必须显示的手动添加构造方法.JVM不会帮你生成.因为你用字节码的方式构造一个类,不会通过javac编译器,JVM也就无法得知你是否有构造方法.因为你直接生成了字节码,跳过了编译.
第二个就是在方法体中用{ }代码块的意义仅仅是限制命名,这样mv就可以一直使用,而不是mv1, mv2...此处可用可不用.
第三点,对于每个访问者,其本质上就是流.使用结束后务必记得使用visitEnd方法进行流的关闭.
Transformation
在进入Transformation前,我们应该先弄懂整个观察者模式的调用流程.其实只要掌握了调用栈,整个框架的层次,原理学起来都会事半功倍.
以下有几个类.
- lab类---主类,连接ClassVisitor, ClassReader, ClassWrite的桥梁
- LogVisitor---日志访问者增强类
- TimeVisitor---方法耗时访问者增强类
- Target---被增强目标类
ini
/**
* 对Target.class进行字节码增强,添加日志和时间记录功能
*/
public class lab {
static void main() throws IOException {
String relative_path = "fileDemo1/Target.class";
File file = new File(relative_path);
byte[] bytes = dump();
new File(file.getParent()).mkdirs();
try {
Files.write(file.toPath(), bytes);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 对Target.class进行字节码增强,添加日志和时间记录功能
* @return 增强后的字节码
* @throws IOException
*/
public static byte[] dump() throws IOException {
ClassReader cr = new ClassReader("ASM_Lab.Transformation.demo5.Target");
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
int api = Opcodes.ASM9;
// 关联LogVisitor和TimeVisitor
ClassVisitor lv = new LogVisitor(api, cw);
TimeVisitor tv = new TimeVisitor(api, lv);
int op = ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES;
cr.accept(tv, op);
return cw.toByteArray();
}
}
java
/**
* 日志访问者
*/
public class LogVisitor extends ClassVisitor {
protected LogVisitor(int api, ClassVisitor classVisitor) {
super(api, classVisitor);
}
@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
// 修改类名为 fileDemo1/Target
super.visit(version, access, "fileDemo1/Target", signature, superName, interfaces);
}
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);
if (mv != null && !"<init>".equals(name) && !"<clinit>".equals(name)) {
boolean isAbstractMethod = (access & ACC_ABSTRACT) != 0;
boolean isNativeMethod = (access & ACC_NATIVE) != 0;
if (!isAbstractMethod && !isNativeMethod) {
mv = new LogMethodVisitorAdapter(api, mv);
}
}
return mv;
}
public static class LogMethodVisitorAdapter extends MethodVisitor {
protected LogMethodVisitorAdapter(int api, MethodVisitor methodVisitor) {
super(api, methodVisitor);
}
@Override
public void visitCode() {
mv.visitFieldInsn(
GETSTATIC,
"java/lang/System",
"out",
"Ljava/io/PrintStream;"
);
mv.visitLdcInsn("store log");
mv.visitMethodInsn(
INVOKEVIRTUAL,
"java/io/PrintStream",
"println",
"(Ljava/lang/String;)V",
false
);
super.visitCode();
}
}
}
java
/**
* TimeVisitor-方法计时访问者增强类
**/
public class TimeVisitor extends ClassVisitor {
protected TimeVisitor(int api, ClassVisitor classVisitor) {
super(api, classVisitor);
}
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);
if (mv != null && !"<init>".equals(name) && !"<clinit>".equals(name)) {
boolean isAbstractMethod = (access & ACC_ABSTRACT) != 0;
boolean isNativeMethod = (access & ACC_NATIVE) != 0;
if (!isAbstractMethod && !isNativeMethod) {
mv = new TimeMethodVisitorAdapter(api, mv, 0);
}
}
return mv;
}
public static class TimeMethodVisitorAdapter extends MethodVisitor {
private final int startTimeVarIndex;
protected TimeMethodVisitorAdapter(int api, MethodVisitor methodVisitor, int startVarIndex) {
super(api, methodVisitor);
this.startTimeVarIndex = startVarIndex;
}
@Override
public void visitCode() {
// 记录开始时间
mv.visitMethodInsn(
INVOKESTATIC,
"java/time/LocalDateTime",
"now",
"()Ljava/time/LocalDateTime;",
false
);
mv.visitVarInsn(ASTORE, startTimeVarIndex);
super.visitCode();
}
@Override
public void visitInsn(int opcode) {
if (opcode == RETURN || opcode == ARETURN || opcode == IRETURN ||
opcode == LRETURN || opcode == FRETURN || opcode == DRETURN) {
// 打印耗时
mv.visitFieldInsn(
GETSTATIC,
"java/lang/System",
"out",
"Ljava/io/PrintStream;"
);
// 记录结束时间
mv.visitMethodInsn(
INVOKESTATIC,
"java/time/LocalDateTime",
"now",
"()Ljava/time/LocalDateTime;",
false
);
// 加载开始时间
mv.visitVarInsn(ALOAD, startTimeVarIndex);
// 计算时间差
mv.visitMethodInsn(
INVOKESTATIC,
"java/time/Duration",
"between",
"(Ljava/time/temporal/Temporal;Ljava/time/temporal/Temporal;)Ljava/time/Duration;",
false
);
// 转换为毫秒
mv.visitMethodInsn(
INVOKEVIRTUAL,
"java/time/Duration",
"toMillis",
"()J",
false
);
// 直接调用println方法,栈顺序为[PrintStream, long]
mv.visitMethodInsn(
INVOKEVIRTUAL,
"java/io/PrintStream",
"println",
"(J)V",
false
);
}
super.visitInsn(opcode);
}
}
}
csharp
/**
* Target增强目标类
**/
public class Target {
public void test() {
int a = 2;
while (a < 100) {
a *= 2;
}
System.out.println(a);
}
}
csharp
/**
* 增强后的Target类
**/
public class Target {
public Target() {
super();
}
public void test() {
LocalDateTime var2 = LocalDateTime.now();
System.out.println("store log");
int var1;
for(var1 = 2; var1 < 100; var1 *= 2) {
}
System.out.println(var1);
System.out.println(Duration.between(LocalDateTime.now(), var2).toMillis());
}
}
为了简洁(其实是懒),我们将统一使用缩写.
这段代码的逻辑很简单,所谓的添加日志也并不复杂仅仅只是打印了一句话.在lab类中我们主要关心其中的dump方法他返回一个字节数组,就是增强完的Target类.方法内部的逻辑就是Transaformation的常规流程.值得注意的就是当存在多个ClassVisitor时,我们通过构造方法进行关联,越往后靠就是越外层的ClassVisitor.最后设置ClassReader的读取属性,通过回调的accept方法开始整个Transformation流程.而链条则类似于责任链模式.
现在我们以accept方法为起点,探索一下观察者模式的调用流程.

这里我们要补充一个点,根据上方的源码我们我知道无论是ClassVisitor, 还是MethodVisitor他的内部都有一个对应的成员持有链中下一个访问者的引用.(指针放置处)
| 时间 | 动作 | 说明 |
|---|---|---|
| t1 | ClassReader调用tv.visitMethod() |
|
| t2 | tv调用lv.visitMethod() |
向下传递 |
| t3 | lv调用cw.visitMethod() |
到达链尾 |
| t4 | cw返回MethodVisitor M1 | 开始返回 |
| t5 | lv包装M1,返回M2 | |
| t6 | tv包装M2,返回M3 | |
| t7 | ClassReader获得M3,开始调用其方法 | |
| t8 | M3.visitCode()调用M2.visitCode() |
向下传递 |
| t9 | M2.visitCode()调用M1.visitCode() |
向下传递,到达链尾 |
| t10 | M1.visitCode()写入字节码,返回 |
ClassWrite写入字节码,开始依次返回 |
| t11 | 控制权依次返回:M1→M2→M3→ClassReader | |
| t12 | 下一条指令重复t8-t11流程 | |
| t13 | 依次按类似于上述的流程调用visitEnd()方法收尾 |
上述的表格已经极尽详细,所有的指令流都遵循上表的调用流程.
实际上,ASM中有两条并行但相关的链:
- ClassVisitor链(处理类结构)
- MethodVisitor链(处理方法指令)
而已示例代码里的类,他们的方向大抵都是调用链:tv -> lv -> cw,以及返回链:cw -> lv -> tv,无论是cv链还是mv链亦是如此.
js
// 此处ClassVisitor代表其对应的MethodVisitor
tv.visitCode()开始
├─ 执行 mv.visitMethodInsn() // 指令1
│ ├─ 进入 lv.visitMethodInsn()
│ │ ├─ 进入 cw.visitMethodInsn()
│ │ │ └─ 写入字节码
│ │ └─ 返回
│ └─ 返回
├─ 执行 mv.visitVarInsn() // 指令2
│ ├─ 进入 lv.visitVarInsn()
│ │ ├─ 进入 cw.visitVarInsn()
│ │ │ └─ 写入字节码
│ │ └─ 返回
│ └─ 返回
└─ 执行 super.visitCode()
└─ ...
js
执行栈深度图示:
第1步:开始执行tv.visitCode()
[栈深度1] TimeMethodVisitorAdapter.visitCode() 开始
第2步:执行 mv.visitMethodInsn(...)
[栈深度1] TimeMethodVisitorAdapter.visitCode() 调用 mv.visitMethodInsn(...)
[栈深度2] └→ LogMethodVisitorAdapter.visitMethodInsn(...) 被调用
[栈深度3] └→ ClassWriter.visitMethodInsn(...) 被调用
[栈深度4] └→ ClassWriter内部:将INVOKESTATIC指令写入字节数组 ✅
[栈深度3] ← 返回 LogMethodVisitorAdapter.visitMethodInsn(...)
[栈深度2] ← 返回 TimeMethodVisitorAdapter.visitCode() 继续执行
第3步:执行 mv.visitVarInsn(ASTORE, 1)
[栈深度1] TimeMethodVisitorAdapter.visitCode() 调用 mv.visitVarInsn(...)
[栈深度2] └→ LogMethodVisitorAdapter.visitVarInsn(...) 被调用
[栈深度3] └→ ClassWriter.visitVarInsn(...) 被调用
[栈深度4] └→ ClassWriter内部:将ASTORE指令写入字节数组 ✅
[栈深度3] ← 返回 LogMethodVisitorAdapter.visitVarInsn(...)
[栈深度2] ← 返回 TimeMethodVisitorAdapter.visitCode() 继续执行
第4步:执行 super.visitCode()
[栈深度1] TimeMethodVisitorAdapter.visitCode() 调用 super.visitCode()
[栈深度2] └→ MethodVisitor.visitCode() 被调用(父类)
[栈深度3] └→ mv.visitCode() 被调用(即LogMethodVisitorAdapter.visitCode())
[栈深度4] └→ LogMethodVisitorAdapter.visitCode() 开始执行...
...(类似的深度调用继续)
结合这两个近乎详细的指令流.理解整个双链的调用过程无疑探囊取物.
理解了大致的流程,现在我们再次回到Transformation,其实大致上就是读取->自定义->重新写入.无论是修改类的内容,添加新的逻辑,删除原有的逻辑.Transformation都能做到.而添加新的内容我们便不再赘述(如上参考代码).主要关注修改 ,与删除 还有状态机.