文档教程后端【免费下载链接】CodeGuide:books: 本代码库是作者小傅哥多年从事一线互联网 Java 开发的学习历程技术汇总旨在为大家提供一个清晰详细的学习教程侧重点更倾向编写Java核心内容。如果本仓库能为您提供帮助请给予支持(关注、点赞、分享)项目地址https://gitcode.com/gh_mirrors/code/CodeGuide点击查看免费下载导读本文是 ASM 字节码框架关于**方法Method**这一核心主题的技术指南讲解如何用核心 ASM API 生成与转换已编译的方法。你将掌握MethodVisitor接口的完整方法签名与调用顺序约束、ClassReader/ClassWriter/MethodVisitor三大核心组件的协作机制、COMPUTE_MAXS与COMPUTE_FRAMES的计算选项以及无状态转换方法耗时计时器和有状态转换指令序列模式匹配两种典型的字节码改写方案。文中所有关键结论均结合本仓库的 ASM 实战案例如 JavaAgentASM 插桩采集方法耗时、字节码增强给方法加 TryCatch交叉验证。一、MethodVisitor方法级访问事件模型生成和转换已编译方法的 ASM API 建立在抽象类MethodVisitor之上见 3.2 接口和组件。它由ClassVisitor.visitMethod方法返回其作用与ClassVisitor之于类完全对称——每一个方法都对应一个独立的事件序列。除注释与调试信息相关的方法在下一章讲解外MethodVisitor为每个字节码指令类别定义了一个访问方法划分依据是指令的参数个数与类型。注意这里的类别划分并不等同于 3.1.2 节对指令的功能分类而是面向 API 使用形态的划分例如无参数的visitInsn(int opcode)、带一个变量索引的visitVarInsn、带字段三元组的visitFieldInsn等。MethodVisitor的完整抽象方法如下public 访问修饰符已省略与 3.2 接口和组件 中列出的签名一致abstract class MethodVisitor { // public accessors ommited MethodVisitor(int api); MethodVisitor(int api, MethodVisitor mv); AnnotationVisitor visitAnnotationDefault(); AnnotationVisitor visitAnnotation(String desc, boolean visible); AnnotationVisitor visitParameterAnnotation(int parameter, String desc, boolean visible); void visitAttribute(Attribute attr); void visitCode(); void visitFrame(int type, int nLocal, Object[] local, int nStack, Object[] stack); void visitInsn(int opcode); void visitIntInsn(int opcode, int operand); void visitVarInsn(int opcode, int var); void visitTypeInsn(int opcode, String desc); void visitFieldInsn(int opc, String owner, String name, String desc); void visitMethodInsn(int opc, String owner, String name, String desc); void visitInvokeDynamicInsn(String name, String desc, Handle bsm, Object... bsmArgs); void visitJumpInsn(int opcode, Label label); void visitLabel(Label label); void visitLdcInsn(Object cst); void visitIincInsn(int var, int increment); void visitTableSwitchInsn(int min, int max, Label dflt, Label[] labels); void visitLookupSwitchInsn(Label dflt, int[] keys, Label[] labels); void visitMultiANewArrayInsn(String desc, int dims); void visitTryCatchBlock(Label start, Label end, Label handler, String type); void visitLocalVariable(String name, String desc, String signature, Label start, Label end, int index); void visitLineNumber(int line, Label start); void visitMaxs(int maxStack, int maxLocals); void visitEnd(); }1. 方法的调用顺序约束与ClassVisitor一样MethodVisitor的方法调用必须遵循严格顺序MethodVisitor接口的 Javadoc 中还有更多约束visitAnnotationDefault? ( visitAnnotation | visitParameterAnnotation | visitAttribute )* ( visitCode ( visitTryCatchBlock | visitLabel | visitFrame | visitXxxInsn | visitLocalVariable | visitLineNumber )* visitMaxs )? visitEnd由此可以得出三个关键结论注释与属性优先对于非抽象方法如果存在注释和属性必须首先访问它们然后才是方法的字节代码代码必须顺序访问方法的字节代码位于对visitCode的调用有且仅有一个与对visitMaxs的调用有且仅有一个之间按指令顺序逐一访问visitEnd 收尾与类的情况一样visitEnd必须在最后调用用于标记方法事件序列的结束。于是visitCode和visitMaxs这对方法恰好标记了方法字节代码在一个事件序列中的起止边界——这在后续编写方法适配器时是两条极其重要的锚点。2. ClassVisitor 与 MethodVisitor 的合并使用将ClassVisitor和MethodVisitor合并即可生成完整的类。下面展示了生成两个方法 m1、m2 的典型流程ClassVisitor cv ...; cv.visit(...); MethodVisitor mv1 cv.visitMethod(..., m1, ...); mv1.visitCode(); mv1.visitInsn(...); ... mv1.visitMaxs(...); mv1.visitEnd(); MethodVisitor mv2 cv.visitMethod(..., m2, ...); mv2.visitCode(); mv2.visitInsn(...); ... mv2.visitMaxs(...); mv2.visitEnd(); cv.visitEnd();一个容易忽略但很有用的特性是并不需要完成一个方法之后才能开始访问另一个方法。MethodVisitor实例之间完全独立可以按任意顺序交叉使用只要还没调用cv.visitEnd()ClassVisitor cv ...; cv.visit(...); MethodVisitor mv1 cv.visitMethod(..., m1, ...); mv1.visitCode(); mv1.visitInsn(...); ... MethodVisitor mv2 cv.visitMethod(..., m2, ...); mv2.visitCode(); mv2.visitInsn(...); ... mv1.visitMaxs(...); mv1.visitEnd(); ... mv2.visitMaxs(...); mv2.visitEnd(); cv.visitEnd();二、方法级三大核心组件ASM 基于MethodVisitorAPI 提供了三个核心组件构成生产者—消费者—过滤器的完整事件管线组件角色行为ClassReader事件产生器分析已编译方法的内容在accept方法的参数中传送ClassVisitor针对该ClassVisitor返回的MethodVisitor对象调用相应方法ClassWriter事件消费者visitMethod方法返回MethodVisitor接口的一个实现直接以二进制形式生成已编译方法MethodVisitor事件筛选器将接收到的所有方法调用委托给另一个MethodVisitor可看作一个事件过滤器这与类级别的三大组件ClassReader/ClassWriter/ClassVisitor一一对应具体可对照 2.2 接口和组件 中的类级事件模型来理解。在本仓库的 JavaAgentASM 插桩案例 中可以清楚地看到这条管线的落地形态private byte[] getBytes(ClassLoader loader, String className, byte[] classfileBuffer) { ClassReader cr new ClassReader(classfileBuffer); ClassWriter cw new ClassWriter(cr, ClassWriter.COMPUTE_MAXS); ClassVisitor cv new ProfilingClassAdapter(cw, className); cr.accept(cv, ClassReader.EXPAND_FRAMES); return cw.toByteArray(); }ClassReader读取类字节码 → 自定义ClassVisitor过滤/改写 →ClassWriter重新输出字节码三者通过事件流串接正是本节三个组件的典型协作方式。三、ClassWriter 选项MAXS 与 FRAMES 的自动计算在 3.1.5 节已经看到为方法计算栈映射帧并不容易必须计算所有帧、找出与跳转目标对应的帧、跳在无条件跳转之后的帧最后还要压缩剩余帧。类似地计算局部变量与操作数栈部分的大小相对容易一些但依然算不上简单。幸好 ASM 能替我们完成这一计算。创建ClassWriter时可以指定自动计算哪些内容构造方式自动计算范围需要自行完成new ClassWriter(0)什么都不自动计算必须自行计算帧、局部变量与操作数栈的大小new ClassWriter(ClassWriter.COMPUTE_MAXS)自动计算局部变量与操作数栈大小仍必须调用visitMaxs但参数会被忽略并重新计算帧仍需自行计算new ClassWriter(ClassWriter.COMPUTE_FRAMES)一切自动计算不再需要调用visitFrame但仍必须调用visitMaxs参数被忽略并重新计算性能代价需要权衡COMPUTE_MAXS选项使ClassWriter的速度降低约 10%而COMPUTE_FRAMES则使其降低约一半。这必须与自行计算所耗费的时间进行比较——在特定情况下往往存在比 ASM 通用算法更简单、更快的计算方法但 ASM 的算法必须能处理所有情况。关于帧压缩如果选择自行计算帧可以让ClassWriter执行压缩步骤——只需要用visitFrame(F_NEW, nLocals, locals, nStack, stack)访问未压缩帧即可其中nLocals和nStack是局部变量的个数和操作数栈的大小locals和stack是包含相应类型的数组。关于 getCommonSuperClass为自动计算帧有时需要计算两个给定类的公共超类。默认情况下ClassWriter在getCommonSuperClass方法中完成这一计算——它会将两个类加载到 JVM 中并使用反射 API。如果正在生成几个相互引用的类这可能导致问题因为被引用的类可能尚不存在。此时可以重写getCommonSuperClass方法来解决。仓库中的 TryCatch 增强案例 展示了一个手工编写帧的实例——在 catch 块入口处显式访问F_SAME1帧操作数栈上有一个Exception对象// 标志catch块开始位置 mv.visitLabel(target); mv.visitFrame(Opcodes.F_SAME1, 0, null, 1, new Object[]{java/lang/Exception});这正是自行计算帧由 ClassWriter 执行压缩思想的体现。四、生成方法从 getF 到 checkAndSetF1. 生成 getter最简单的映射如果mv是一个MethodVisitor那么getF方法读取this.f并返回的字节代码可以用下面的方法调用生成mv.visitCode(); mv.visitVarInsn(ALOAD, 0); mv.visitFieldInsn(GETFIELD, pkg/Bean, f, I); mv.visitInsn(IRETURN); mv.visitMaxs(1, 1); mv.visitEnd();第一个调用visitCode启动字节代码生成过程随后三个调用生成该方法的三条指令——可以看出字节代码与 ASM API 之间的映射非常简单直接visitMaxs必须在访问完所有指令之后调用用于定义执行帧中局部变量和操作数栈部分的大小这里两部分各为 1 个槽分别对应this与this.f最后一次调用visitEnd结束方法的生成。2. 生成 checkAndSetF标记与帧的介入setF方法和构造器的字节代码可以用类似方式生成。一个更有意义的示例是checkAndSetF方法f 非负才允许写入否则抛IllegalArgumentExceptionmv.visitCode(); mv.visitVarInsn(ILOAD, 1); Label label new Label(); mv.visitJumpInsn(IFLT, label); mv.visitVarInsn(ALOAD, 0); mv.visitVarInsn(ILOAD, 1); mv.visitFieldInsn(PUTFIELD, pkg/Bean, f, I); Label end new Label(); mv.visitJumpInsn(GOTO, end); mv.visitLabel(label); mv.visitFrame(F_SAME, 0, null, 0, null); mv.visitTypeInsn(NEW, java/lang/IllegalArgumentException); mv.visitInsn(DUP); mv.visitMethodInsn(INVOKESPECIAL, java/lang/IllegalArgumentException, init, ()V); mv.visitInsn(ATHROW); mv.visitLabel(end); mv.visitFrame(F_SAME, 0, null, 0, null); mv.visitInsn(RETURN); mv.visitMaxs(2, 2); mv.visitEnd();在visitCode与visitEnd之间可以看到恰好映射到 3.1.5 节末尾所示字节代码的方法调用每条指令、标记或帧分别有一个调用唯一例外是label和end两个Label对象的声明与构造。注意 Label 的语义Label对象规定了跟在visitLabel之后的指令。例如上面的end规定的是RETURN指令而不是随后马上访问的帧因为帧不是指令。用几条标记指定同一指令是完全合法的但一个标记只能恰好指定一条指令反过来一条指令上的标记必须用visitLabel恰好访问一次。最后一条约束是标记不能共享每个方法都必须拥有自己的标记。五、转换方法方法适配器链方法可以像类一样被转换——使用一个方法适配器将它收到的方法调用转发出去并做修改改变参数 → 改变具体指令不转发某个收到的调用 → 删除一条指令在接收到的调用之间插入调用 → 增加新指令。MethodVisitor类本身提供了一种方法适配器的基本实现它只是转发接收到的所有方法调用不做任何其他事情。1. 最简单的适配器RemoveNopAdapter考虑一个删除方法中 NOP 指令的适配器NOP 不做任何事情删除它没有任何问题public class RemoveNopAdapter extends MethodVisitor { public RemoveNopAdapter(MethodVisitor mv) { super(ASM4, mv); } Override public void visitInsn(int opcode) { if (opcode ! NOP) { mv.visitInsn(opcode); } } }这个适配器可以在一个类适配器内部使用public class RemoveNopClassAdapter extends ClassVisitor { public RemoveNopClassAdapter(ClassVisitor cv) { super(ASM4, cv); } Override public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { MethodVisitor mv; mv cv.visitMethod(access, name, desc, signature, exceptions); if (mv ! null) { mv new RemoveNopAdapter(mv); } return mv; } }换言之类适配器只是构造一个方法适配器封装链中下一个类访问器返回的方法访问器并返回这个适配器其效果是构造了一个类似于类适配器链的方法适配器链。2. 方法适配器链并非必须与类适配器链同构这种相似性不是强制的完全可能构造与类适配器链不相似的方法适配器链每种方法甚至可以有不同的方法适配器链。例如类适配器可以选择仅删除普通方法中的 NOP而不处理构造器mv cv.visitMethod(access, name, desc, signature, exceptions); if (mv ! null !name.equals(init)) { mv new RemoveNopAdapter(mv); } ...在这种情况下构造器的适配器链更短。反过来构造器的适配器链也可以更长在visitMethod内部创建几个链接在一起的适配器甚至方法适配器链的拓扑结构都可以不同于类适配器——类适配器是线性的而方法适配器链可以有分支例如一个方法拆成两个同名变体方法public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { MethodVisitor mv1, mv2; mv1 cv.visitMethod(access, name, desc, signature, exceptions); mv2 cv.visitMethod(access, _ name, desc, signature, exceptions); return new MultiMethodAdapter(mv1, mv2); }六、无状态转换实战AddTimerAdapter 方法计时器下面实现一个比RemoveNopAdapter更有意义的适配器为程序中的每个类添加静态计时器字段并把每个方法的执行时间累加到该字段上。以类 C 为例转换前public class C { public void m() throws Exception { Thread.sleep(100); } }转换后public class C { public static long timer; public void m() throws Exception { timer - System.currentTimeMillis(); Thread.sleep(100); timer System.currentTimeMillis(); } }1. 通过 TraceClassVisitor 对比前后差异为了弄清需要插入哪些指令可以编译这两个版本用TraceClassVisitor默认Textifier后端或ASMifier后端对比输出得到的差异如下GETSTATIC C.timer : J INVOKESTATIC java/lang/System.currentTimeMillis()J LSUB PUTSTATIC C.timer : J LDC 100 INVOKESTATIC java/lang/Thread.sleep(J)V GETSTATIC C.timer : J INVOKESTATIC java/lang/System.currentTimeMillis()J LADD PUTSTATIC C.timer : J RETURN MAXSTACK 4 MAXLOCALS 1结论清晰必须在方法开头增加四条指令在每个返回指令之前增加另外四条指令同时更新操作数栈的最大尺寸。2. 重写 visitCode 添加开头指令方法的代码开头部分用visitCode访问因此通过重写方法适配器的这一方法即可添加前四条指令public void visitCode() { mv.visitCode(); mv.visitFieldInsn(GETSTATIC, owner, timer, J); mv.visitMethodInsn(INVOKESTATIC, java/lang/System, currentTimeMillis, ()J); mv.visitInsn(LSUB); mv.visitFieldInsn(PUTSTATIC, owner, timer, J); }其中owner必须被设定为所转换类的名字。3. 重写 visitInsn 在返回指令前收尾需要在任意 RETURN 之前、以及任何 xRETURN 或 ATHROW 之前添加收尾的四条指令——这些终止指令都没有参数因此在visitInsn中访问public void visitInsn(int opcode) { if ((opcode IRETURN opcode RETURN) || opcode ATHROW) { mv.visitFieldInsn(GETSTATIC, owner, timer, J); mv.visitMethodInsn(INVOKESTATIC, java/lang/System, currentTimeMillis, ()J); mv.visitInsn(LADD); mv.visitFieldInsn(PUTSTATIC, owner, timer, J); } mv.visitInsn(opcode); }4. 更新操作数栈最大大小插入的指令压入两个 long 值需要操作数栈中的四个槽。方法开头操作数栈为空所以开头添加的四条指令需要一个大小为 4 的栈插入代码不会改变栈状态弹出的值数目与压入相同因此若原代码需要大小为 s 的栈转换后的方法所需栈最大大小为max(4, s)。但返回指令之前插入的代码更麻烦我们不知道恰在执行这些指令前操作数栈的大小只知道它 ≤ s因此只能按最糟情景处理——在返回指令前添加的代码可能要求操作数栈达到 s4public void visitMaxs(int maxStack, int maxLocals) { mv.visitMaxs(maxStack 4, maxLocals); }幸好并不一定要给出最优操作数栈大小。给出任何大于等于最优值的值都是合法的尽管可能浪费线程执行栈上的内存。当然也可以完全不操心最大栈大小而依赖COMPUTE_MAXS选项——它会计算最优值而非最糟情景值。对于这种简单转换人工更新 maxStack 并不需要太多精力。那么栈映射帧需要更新吗答案是否定的因为1插入的代码没有改变操作数栈2插入代码中没有跳转指令3原代码的控制流图没有被修改。这意味着原帧没有发生变化压缩后的原帧也无需改动。5. 完整实现AddTimerAdapter将所有元素放入相关联的ClassVisitor与MethodVisitor子类中public class AddTimerAdapter extends ClassVisitor { private String owner; private boolean isInterface; public AddTimerAdapter(ClassVisitor cv) { super(ASM4, cv); } Override public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { cv.visit(version, access, name, signature, superName, interfaces); owner name; isInterface (access ACC_INTERFACE) ! 0; } Override public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { MethodVisitor mv cv.visitMethod(access, name, desc, signature, exceptions); if (!isInterface mv ! null !name.equals(init)) { mv new AddTimerMethodAdapter(mv); } return mv; } Override public void visitEnd() { if (!isInterface) { FieldVisitor fv cv.visitField(ACC_PUBLIC ACC_STATIC, timer, J, null, null); if (fv ! null) { fv.visitEnd(); } } cv.visitEnd(); } class AddTimerMethodAdapter extends MethodVisitor { public AddTimerMethodAdapter(MethodVisitor mv) { super(ASM4, mv); } Override public void visitCode() { mv.visitCode(); mv.visitFieldInsn(GETSTATIC, owner, timer, J); mv.visitMethodInsn(INVOKESTATIC, java/lang/System, currentTimeMillis, ()J); mv.visitInsn(LSUB); mv.visitFieldInsn(PUTSTATIC, owner, timer, J); } Override public void visitInsn(int opcode) { if ((opcode IRETURN opcode RETURN) || opcode ATHROW) { mv.visitFieldInsn(GETSTATIC, owner, timer, J); mv.visitMethodInsn(INVOKESTATIC, java/lang/System, currentTimeMillis, ()J); mv.visitInsn(LADD); mv.visitFieldInsn(PUTSTATIC, owner, timer, J); } mv.visitInsn(opcode); } Override public void visitMaxs(int maxStack, int maxLocals) { mv.visitMaxs(maxStack 4, maxLocals); } } }这个类适配器负责实例化方法适配器构造器除外、在visitEnd中添加计时器字段并把被转换类名存储在方法适配器可访问的owner字段中。6. 仓库源码中的同款思路上述在visitCode中注入开始逻辑、在返回/异常指令前注入收尾逻辑正是字节码监控的标准范式。本仓库的 JavaAgentASM 插桩采集方法耗时案例 完整实现了该思路在方法入口注入INVOKESTATIC System.nanoTime()LSTORE记录开始时间在onMethodExit中注入LLOAD时间戳、LDC方法 ID、ALOAD参数数组与返回值最后INVOKESTATIC调用预设的静态监控方法ProfilingAspect.point(...)mv.visitMethodInsn(INVOKESTATIC, java/lang/System, nanoTime, ()J, false); startTimeIdentifier newLocal(Type.LONG_TYPE); mv.visitVarInsn(LSTORE, startTimeIdentifier);mv.visitVarInsn(LLOAD, startTimeIdentifier); // 加载开始时间 mv.visitLdcInsn(methodId); // 加载方法ID mv.visitMethodInsn(INVOKESTATIC, Type.getInternalName(ProfilingAspect.class), point, (JI[Ljava/lang/Object;Ljava/lang/Object;)V, false);另一个 TryCatch 增强案例 则展示了visitMaxs的另一种玩法在visitMaxs中注入 catch 块标签、F_SAME1帧与ATHROW从而给方法包上 TryCatch——可见visitMaxs不只是声明大小还可以承担收尾插桩的职责。七、有状态转换指令序列的模式匹配上一节的转换是局部的不依赖当前指令之前访问过的指令——开头添加的代码总是相同且总会被添加每个 RETURN 之前的代码也是如此。这种转换称为无状态转换实现简单但只有最简单的转换具有这一性质。更复杂的转换需要记忆在当前指令之前已访问指令的状态。例如删除所有ICONST_0 IADD序列加入 0 没有实际效果的转换在访问一条IADD指令时只有当上一条被访问的指令是ICONST_0时才必须删除它。这要求在方法适配器中存储状态因此称为有状态转换。1. 延迟决策PatternMethodAdapter在访问ICONST_0时只有当下一条指令是IADD时才必须删除它——但下一条指令还是未知的。解决方法是把是否删除的决定推迟到下一条指令如果下一指令是IADD则删除两条指令否则发出ICONST_0和当前指令。为实现这类删除或替代指令序列的转换可以引入一个MethodVisitor子类它的各visitXxxInsn方法统一调用一个公共的visitInsn()方法该方法负责结算尚未决定的指令public abstract class PatternMethodAdapter extends MethodVisitor { protected final static int SEEN_NOTHING 0; protected int state; public PatternMethodAdapter(int api, MethodVisitor mv) { super(api, mv); } Override public void visitInsn(int opcode) { visitInsn(); mv.visitInsn(opcode); } Override public void visitIntInsn(int opcode, int operand) { visitInsn(); mv.visitIntInsn(opcode, operand); } ... protected abstract void visitInsn(); }2. 删除 ICONST_0 IADDRemoveAddZeroAdapter上述转换可实现如下public class RemoveAddZeroAdapter extends PatternMethodAdapter { private static int SEEN_ICONST_0 1; public RemoveAddZeroAdapter(MethodVisitor mv) { super(ASM4, mv); } Override public void visitInsn(int opcode) { if (state SEEN_ICONST_0) { if (opcode IADD) { state SEEN_NOTHING; return; // 删除序列 } } visitInsn(); if (opcode ICONST_0) { state SEEN_ICONST_0; return; // 延迟决定 } mv.visitInsn(opcode); } Override protected void visitInsn() { if (state SEEN_ICONST_0) { mv.visitInsn(ICONST_0); } state SEEN_NOTHING; } }visitInsn(int)方法首先判断是否已检测到目标序列——若是重新初始化state并立即返回效果就是删除该序列否则调用公共的visitInsn()方法若ICONST_0是最后一条被访问的指令则先发出它若当前指令是ICONST_0则记住这个事实并返回延迟决定其余情况将当前指令转发到下一访问器。3. 标记与帧模式匹配中的指令前几节已经看到对标记和帧的访问恰在它们的相关指令之前进行——尽管它们本身不是指令但与指令同时受到访问。这对指令序列检测有影响而且这种影响实际上是一种优势如果被删除的指令之一是跳转指令的目标若某指令可能跳转到ICONST_0意味着有一个指定这一指令的标记删除两条指令后该标记将指向跟在被删除 IADD 之后的指令——这正是希望的但如果某指令可能跳转到IADD则不能删除这个序列无法确保跳转前栈中已压入 0。幸好这种情况下ICONST_0与IADD之间必然有一个标记可以轻松检测到。对栈映射帧的推理完全一样如果访问介于两条指令之间的栈映射帧就不能删除它们。处理这两种情况的办法是把标记和帧也当作模式匹配算法中的指令public abstract class PatternMethodAdapter extends MethodVisitor { ... Override public void visitFrame(int type, int nLocal, Object[] local, int nStack, Object[] stack) { visitInsn(); mv.visitFrame(type, nLocal, local, nStack, stack); } Override public void visitLabel(Label label) { visitInsn(); mv.visitLabel(label); } Override public void visitMaxs(int maxStack, int maxLocals) { visitInsn(); mv.visitMaxs(maxStack, maxLocals); } }注意visitMaxs也会调用公共的visitInsn()方法——它处理的情景是方法的末尾恰好是要检测序列的一个前缀。另外编译后的方法中可能包含源文件行号信息如异常栈轨迹所用用visitLineNumber方法访问也与指令同时被调用。但在指令序列中间给出行号不影响转换或删除指令的可能性因此在模式匹配算法中可以完全忽略它们。4. 更复杂的例子删除字段自我赋值模式匹配可以推广到更复杂的指令序列。例如删除字段自我赋值操作通常是键入错误如f f;字节码为ALOAD 0 ALOAD 0 GETFIELD f PUTFIELD f。实现前最好先设计状态机识别该序列S0未识别→ S1看到ALOAD 0S1 → S2再看到ALOAD 0S2 → S2连续第三个ALOAD 0时发出前一个ALOAD 0并停留处理连续 3 个以上 ALOAD 0 的情形S2 → S3看到GETFIELD f记录字段信息S3 → S0看到PUTFIELD f字段名匹配删除整个序列。找到状态机后相应方法适配器的编写就简单了class RemoveGetFieldPutFieldAdapter extends PatternMethodAdapter { private final static int SEEN_ALOAD_0 1; private final static int SEEN_ALOAD_0ALOAD_0 2; private final static int SEEN_ALOAD_0ALOAD_0GETFIELD 3; private String fieldOwner; private String fieldName; private String fieldDesc; public RemoveGetFieldPutFieldAdapter(MethodVisitor mv) { super(mv); } Override public void visitVarInsn(int opcode, int var) { switch (state) { case SEEN_NOTHING: // S0 - S1 if (opcode ALOAD var 0) { state SEEN_ALOAD_0; return; } break; case SEEN_ALOAD_0: // S1 - S2 if (opcode ALOAD var 0) { state SEEN_ALOAD_0ALOAD_0; return; } case SEEN_ALOAD_0ALOAD_0: // S2 - S2 if (opcode ALOAD var 0) { mv.visitVarInsn(ALOAD, 0); return; } break; } visitInsn(); mv.visitVarInsn(opcode, var); } Override public void visitFieldInsn(int opcode, String owner, String name, String desc) { switch (state) { case SEEN_ALOAD_0ALOAD_0: // S2 - S3 if (opcode GETFIELD) { state SEEN_ALOAD_0ALOAD_0GETFIELD; fieldOwner owner; fieldName name; fieldDesc desc; return; } break; case SEEN_ALOAD_0ALOAD_0GETFIELD: // S3 - S0 if (opcode PUTFIELD name.equals(fieldName)) { state SEEN_NOTHING; return; } break; } visitInsn(); mv.visitFieldInsn(opcode, owner, name, desc); } Override protected void visitInsn() { switch (state) { case SEEN_ALOAD_0: // S1 - S0 mv.visitVarInsn(ALOAD, 0); break; case SEEN_ALOAD_0ALOAD_0: // S2 - S0 mv.visitVarInsn(ALOAD, 0); mv.visitVarInsn(ALOAD, 0); break; case SEEN_ALOAD_0ALOAD_0GETFIELD: // S3 - S0 mv.visitVarInsn(ALOAD, 0); mv.visitVarInsn(ALOAD, 0); mv.visitFieldInsn(GETFIELD, fieldOwner, fieldName, fieldDesc); break; } state SEEN_NOTHING; } }每种状态转移都标有一个条件当前指令的值和一个操作必须发出的指令序列。注意从 S2 到自身的转换发现三个或三个以上连续ALOAD 0时停留在已访问两个ALOAD 0的状态中并发出第三个ALOAD 0。与 3.2.4 节AddTimerAdapter同理有状态转换同样不需要转换栈映射帧原帧在转换后仍然有效甚至不需要转换局部变量和操作数栈大小。另外要注意有状态转换并不限于检测和转换指令序列——许多其他类型的转换如 3.3 工具 中基于AnalyzerAdapter计算逐指令栈帧、基于LocalVariablesSorter重新编号局部变量、基于AdviceAdapter在方法入口与 RETURN/ATHROW 前插桩的适配器也都是有状态的。八、总结围绕方法这一粒度ASM 的事件模型可以概括为三条主线接口即协议MethodVisitor的每个visitXxxInsn对应一类字节码指令调用必须遵守visitCode开始→ 指令/标记/帧有序访问→visitMaxs结束→visitEnd的固定顺序组件即管线ClassReader产生方法事件、MethodVisitor适配器筛选/改写事件、ClassWriter消费事件并输出字节码配合COMPUTE_MAXS/COMPUTE_FRAMES选项可以把帧与栈大小的计算交给框架转换即三种操作改参数改指令、不转发删指令、插入调用增指令——无状态转换如计时器插桩依赖visitCode与visitInsn锚点有状态转换如指令序列删除依赖状态机与延迟决策。掌握了这些再去阅读 JavaAgentASM 插桩案例、TryCatch 增强案例以及本仓库的 ASM 入门导读就能把方法级字节码编程的能力真正落到全链路监控、非入侵插桩等实战场景中。方法级转换只是入口后续还可以继续深入方法级工具适配器3.3 工具与元数据注释、泛型、调试信息的处理形成完整的方法操控能力。赞分享文档教程后端【免费下载链接】CodeGuide:books: 本代码库是作者小傅哥多年从事一线互联网 Java 开发的学习历程技术汇总旨在为大家提供一个清晰详细的学习教程侧重点更倾向编写Java核心内容。如果本仓库能为您提供帮助请给予支持(关注、点赞、分享)项目地址https://gitcode.com/gh_mirrors/code/CodeGuide点击查看免费下载相关推荐ASM 树 API 方法级组件合成MethodNode 与 MethodVisitor 的链接模式与字节码转换实战ASM 树 API 方法级组件合成MethodNode 与 MethodVisitor 的链接模式与字节码转换实战 树 API 的 MethodNode 把方文档教程后端深入 ASM 树 API 方法模型MethodNode、InsnList 与指令级代码生成/转换实战深入 ASM 树 API 方法模型MethodNode、InsnList 与指令级代码生成/转换实战 本篇技术指南聚焦 ASM 字节码框架的 树 APITr文档教程后端ASM 树 API 方法生成与转换实战从 MethodNode 指令树到全局跳转优化ASM 树 API 方法生成与转换实战从 MethodNode 指令树到全局跳转优化 本章聚焦 ASM 树 API 在 已编译方法 维度的生成与转换将方法内文档教程后端创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考