资讯动态

深入 ASM 核心组件:ClassVisitor、ClassReader 与 ClassWriter 的类分析与转换实战

发布时间:2026/9/23 15:15:34 来源:尧图企业网站定制
深入 ASM 核心组件ClassVisitor、ClassReader 与 ClassWriter 的类分析与转换实战【免费下载链接】CodeGuide:books: 本代码库是作者小傅哥多年从事一线互联网 Java 开发的学习历程技术汇总旨在为大家提供一个清晰详细的学习教程侧重点更倾向编写Java核心内容。如果本仓库能为您提供帮助请给予支持(关注、点赞、分享)项目地址: https://gitcode.com/gh_mirrors/code/CodeGuide本篇技术指南围绕 ASM 字节码框架最核心的ClassVisitorAPI 展开完整讲解如何基于ClassReader事件产生器、ClassWriter事件使用器与ClassVisitor事件筛选器三大组件实现已编译类的分析、生成与转换三大操作。读者学完后将掌握类文件事件模型的调用顺序约束、字段/方法级增删改的实现套路以及结合ClassLoader与java.lang.instrument将转换后的类落地运行的能力。全文以仓库中《ASM 4 使用手册》章节 2.2 接口和组件 为主体骨架并引入仓库内 ASM 字节码编程实践系列 的真实源码示例作为佐证。一、从已编译类的结构到事件模型在进入ClassVisitor之前先回顾已编译类的基本事实详见 2.1 结构一个.class文件包含修饰符/名字/超类/接口信息、常量池、源文件名可选、封装类引用、注释、属性、内部类、字段与方法等部分所有类型名字必须是完全限定的且不使用package和import语法。ASM 的核心设计思想是把访问一个类文件抽象为一系列方法调用事件类文件中的每个结构部分都对应一个同名方法访问者按固定顺序依次调用它们即可重建或改写这个类。这一事件模型的载体就是ClassVisitor抽象类。二、ClassVisitor事件模型的载体用于生成和转换已编译类的 ASM API 基于ClassVisitor抽象类构建。该类中的每个方法都对应于一个同名的类文件结构部分简单的部分只需一次方法调用即可访问调用返回void参数描述该部分的内容而内容可以达到任意长度、任意复杂度的部分则用一个初始方法调用返回一个辅助的访问者类——visitAnnotation、visitField和visitMethod就属于这种情况它们分别返回AnnotationVisitor、FieldVisitor和MethodVisitor。ClassVisitor的核心方法签名如下摘自 2.2 接口和组件public abstract class ClassVisitor { public ClassVisitor(int api); public ClassVisitor(int api, ClassVisitor cv); public void visit(int version, int access, String name, String signature, String superName, String[] interfaces); public void visitSource(String source, String debug); public void visitOuterClass(String owner, String name, String desc); AnnotationVisitor visitAnnotation(String desc, boolean visible); public void visitAttribute(Attribute attr); public void visitInnerClass(String name, String outerName, String innerName, int access); public FieldVisitor visitField(int access, String name, String desc, String signature, Object value); public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions); void visitEnd(); }辅助访问者类递归适用同样的原则。例如FieldVisitor抽象类中的每个方法对应于同名的类文件子结构visitAnnotation返回一个辅助的AnnotationVisitor与ClassVisitor中的情形完全一致public abstract class FieldVisitor { public FieldVisitor(int api); public FieldVisitor(int api, FieldVisitor fv); public AnnotationVisitor visitAnnotation(String desc, boolean visible); public void visitAttribute(Attribute attr); public void visitEnd(); }调用顺序约束关键ClassVisitor的方法必须按以下顺序调用在该类的 Javadoc 中规定visit visitSource? visitOuterClass? ( visitAnnotation | visitAttribute )* ( visitInnerClass | visitField | visitMethod )* visitEnd含义是必须首先调用visit然后是最多一次visitSource接下来是最多一次visitOuterClass之后可按任意顺序进行任意多次visitAnnotation和visitAttribute再按任意顺序进行任意多次visitInnerClass、visitField和visitMethod最后以一次visitEnd结束。这个顺序约束是所有类转换正确性的根基增加/删除成员时新调用必须插入到合法位置否则会生成被 JVM 验证器拒绝的无效类。三、三大核心组件产生者、使用者与筛选者ASM 提供了三个基于ClassVisitorAPI 的核心组件用于生成和转换类组件角色职责ClassReader事件产生器分析以字节数组形式给出的已编译类并针对其accept方法参数中传送的ClassVisitor实例调用相应的visitXxx方法ClassWriter事件使用器ClassVisitor抽象类的子类直接以二进制形式生成编译后的类输出可通过toByteArray方法提取为字节数组ClassVisitor事件筛选器将它收到的所有方法调用委托给另一个ClassVisitor类可在委托前修改或拦截事件后续所有操作——分析、生成、转换——都是这三个角色围绕事件流的组合游戏。四、分析类ClassReader 单兵作战分析一个已经存在的类时唯一必需的组件是ClassReader。以打印一个类的内容方式类似于 javap 工具为例第一步是编写ClassVisitor的一个子类打印所访问类的相关信息public class ClassPrinter extends ClassVisitor { public ClassPrinter() { super(ASM4); } public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { System.out.println(name extends superName {); } public void visitSource(String source, String debug) { } public void visitOuterClass(String owner, String name, String desc) { } public AnnotationVisitor visitAnnotation(String desc, boolean visible) { return null; } public void visitAttribute(Attribute attr) { } public void visitInnerClass(String name, String outerName, String innerName, int access) { } public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) { System.out.println( desc name); return null; } public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { System.out.println( name desc); return null; } public void visitEnd() { System.out.println(}); } }第二步将ClassPrinter与ClassReader合并让ClassReader产生的事件由ClassPrinter消费ClassPrinter cp new ClassPrinter(); ClassReader cr new ClassReader(java.lang.Runnable); cr.accept(cp, 0);第二行创建了一个ClassReader来分析Runnable类accept方法分析Runnable类的字节码并对cp调用相应的ClassVisitor方法。输出结果为java/lang/Runnable extends java/lang/Object { run()V }注意构建ClassReader实例的方式有若干种要读取的类可以像上面一样用名字指定也可以用字节数组或InputStream的值来指定。利用ClassLoader的getResourceAsStream方法可以获得一个读取类内容的输入流cl.getResourceAsStream(classname.replace(., /) .class);五、生成类ClassWriter 的事件消费生成一个类时唯一必需的组件是ClassWriter。以生成以下接口为例package pkg; public interface Comparable extends Mesurable { int LESS -1; int EQUAL 0; int GREATER 1; int compareTo(Object o); }可以对ClassVisitor进行六次方法调用来生成它ClassWriter cw new ClassWriter(0); cw.visit(V1_5, ACC_PUBLIC ACC_ABSTRACT ACC_INTERFACE, pkg/Comparable, null, java/lang/Object, new String[]{pkg/Mesurable}); cw.visitField(ACC_PUBLIC ACC_FINAL ACC_STATIC, LESS, I, null, new Integer(-1)).visitEnd(); cw.visitField(ACC_PUBLIC ACC_FINAL ACC_STATIC, EQUAL, I, null, new Integer(0)).visitEnd(); cw.visitField(ACC_PUBLIC ACC_FINAL ACC_STATIC, GREATER, I, null, new Integer(1)).visitEnd(); cw.visitMethod(ACC_PUBLIC ACC_ABSTRACT, compareTo, (Ljava/lang/Object;)I, null, null).visitEnd(); cw.visitEnd(); byte[] b cw.toByteArray();逐一拆解这六次调用的语义visit定义类标头V1_5是定义在ASM Opcodes接口中的常量指明类的版本为 Java 1.5ACC_XXX常量是与 Java 修饰符对应的标志此处规定该类是接口且为public、abstract不能被实例化类名采用内部形式见 2.1 结构 的 2.1.2 节已编译类不包含package与import部分因此所有类名必须完全限定signature参数对应泛型此例为null接口未由类型变量参数化第五个参数是内部形式的超类接口类隐式继承自Object最后一个参数是被扩展的接口数组由其内部名指定。三次visitField定义接口字段第一个参数是对应 Java 修饰符的标志这里是public、final、static第二个参数是字段名第三个参数是采用类型描述符形式的字段类型int 字段的描述符为I第四个参数对应泛型此例为null最后一个参数是字段的常量值——该参数必须仅用于真正的常量字段final static字段其他字段必须为null。由于没有注释所以立即调用返回的FieldVisitor的visitEnd方法。visitMethod定义compareTo方法第一个参数是修饰符标志第二个参数是方法名第三个参数是方法描述符(Ljava/lang/Object;)I第四个参数对应泛型为null最后一个参数是方法可抛出的异常数组由其内部名指明此例为null。visitMethod返回的MethodVisitor可用于定义方法的注释、属性最重要的是方法的代码。这里方法为抽象且无注释所以立即调用其visitEnd。最后调用visitEnd通知cw类已结束再通过toByteArray()以字节数组形式提取结果。使用生成的类两种 ClassLoader 路线生成的字节数组可以存储在.class文件中供以后使用也可以用ClassLoader动态加载。一种方法是定义一个ClassLoader子类将其defineClass方法设为公有class MyClassLoader extends ClassLoader { public Class defineClass(String name, byte[] b) { return defineClass(name, b, 0, b.length); } }然后直接调用所生成的类Class c myClassLoader.defineClass(pkg.Comparable, b);另一种更清晰的方法是重写findClass方法在运行过程中生成所请求的类class StubClassLoader extends ClassLoader { Override protected Class findClass(String name) throws ClassNotFoundException { if (name.endsWith(_Stub)) { ClassWriter cw new ClassWriter(0); ... byte[] b cw.toByteArray(); return defineClass(name, b, 0, b.length); } return super.findClass(name); } }仓库实践文章 ASM 字节码编程 | 如果你只写 CRUD那这种技术你永远碰不到 中给出了完整的可运行示例通过ClassWriter生成一个包含main方法的HelloWorld类方法体内依次使用visitFieldInsn(GETSTATIC, java/lang/System, out, Ljava/io/PrintStream;)获取静态属性、visitLdcInsn(Hello World)加载常量、visitMethodInsn(INVOKEVIRTUAL, ...)调用println并以visitMaxs(2, 1)设置操作数栈深度与局部变量大小随后用自定义ClassLoader加载并反射调用sum(int, int)方法完成两数之和计算。这套生成 → 自定义 ClassLoader 加载 → 反射调用的链路正是ClassWriter事件消费能力的典型落地。六、转换类三组件联动的真正价值ClassReader与ClassWriter单独使用时事件要么人工产生由ClassWriter直接消费要么由ClassReader产生后人工消费。当这两个组件一同使用时事情才真正开始变得有意义。第一步是把ClassReader产生的事件转给ClassWriter结果是类写入器重建了类读取器分析的类byte[] b1 ...; ClassWriter cw new ClassWriter(0); ClassReader cr new ClassReader(b1); cr.accept(cw, 0); byte[] b2 cw.toByteArray(); // b2 和 b1 表示同一个类这本身没有实际意义复制字节数组有更简单的方法但下一步——在类读取器和类写入器之间引入一个ClassVisitor——才是关键byte[] b1 ...; ClassWriter cw new ClassWriter(0); // cv 将所有事件转发给 cw ClassVisitor cv new ClassVisitor(ASM4, cw) { }; ClassReader cr new ClassReader(b1); cr.accept(cv, 0); byte[] b2 cw.toByteArray(); // b2 与 b1 表示同一个类结果没有改变因为此时的ClassVisitor事件筛选器没有筛选任何东西。但为了转换一个类只需重写一些方法、筛选一些事件就足够了。例如下面的ClassVisitor子类仅重写visit方法将类版本号改为 Java 1.5public class ChangeVersionAdapter extends ClassVisitor { public ChangeVersionAdapter(ClassVisitor cv) { super(ASM4, cv); } Override public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { cv.visit(V1_5, access, name, signature, superName, interfaces); } }除visit调用被修改外所有调用都被原样转发给构造器传入的cv。通过修改visit的其他参数还可以实现更多转换例如向实现接口列表中添加一个接口。但要注意改变类的名字比想象中复杂得多——类的名字可以出现在已编译类的许多不同地方真正实现重命名必须修改类中出现的所有名字。仓库实践文章中的方法耗时监控正是这一模式的真实用例ClassReader cr new ClassReader(MyMethod.class.getName())读取原有类ClassVisitor cv new ProfilingClassAdapter(cw, ...)增强字节码cr.accept(cv, ClassReader.EXPAND_FRAMES)驱动事件流转最终在onMethodEnter/onMethodExit回调中织入System.nanoTime()计时与StringBuilder拼接的耗时输出代码详见 JavaAgentASM 字节码插桩采集方法名称以及入参和出参结果并记录方法耗时。优化方法级字节复制上述转换只修改了原类的四个字节但整个b1均被分析并从头构建b2效率不高。如果将b1中不被转换的部分直接复制到b2中不分析、不生成事件效率会高得多。ASM 自动为方法执行这一优化在ClassReader.accept方法参数中传送ClassVisitor后如果ClassReader检测到这个ClassVisitor返回的MethodVisitor来自一个ClassWriter说明该方法的内容将不会被转换应用程序甚至看不到其内容这种情况下ClassReader不分析方法内容、不生成相应事件而是直接把ClassWriter中表示该方法的字节数组复制过去。要让ClassReader和ClassWriter互相持有对方引用以启用该优化需如下构造byte[] b1 ... ClassReader cr new ClassReader(b1); ClassWriter cw new ClassWriter(cr, 0); ChangeVersionAdapter ca new ChangeVersionAdapter(cw); cr.accept(ca, 0); byte[] b2 cw.toByteArray();执行该优化后由于ChangeVersionAdapter没有转换任何方法代码速度可以达到优化前的两倍。对于转换部分或全部方法的常见转换提速幅度会小一些但仍相当可观约 10% 到 20%。不过该优化需要把原类中定义的所有常量都复制到转换后的类中对增加字段、方法或指令的转换不成问题但对移除或重命名许多类成员的转换会导致类文件大于未优化时的情况。因此建议仅对增加性转换应用这一优化。仓库实践文章中的耗时监控案例正是这种增加性转换ClassWriter cw new ClassWriter(cr, ClassWriter.COMPUTE_MAXS)直接以ClassReader构造ClassWriter利用优化通道复制未被修改的方法字节码同时以COMPUTE_MAXS标志让 ASM 自动计算操作数栈与局部变量大小。使用转换后的类ClassFileTransformer 全局织入转换后的类b2可以存储在磁盘上或用ClassLoader加载。但ClassLoader中的类转换只能转换由该加载器加载的类。若要转换所有类必须把转换放进ClassFileTransformer内部定义于java.lang.instrument包public static void premain(String agentArgs, Instrumentation inst) { inst.addTransformer(new ClassFileTransformer() { public byte[] transform(ClassLoader l, String name, Class c, ProtectionDomain d, byte[] b) throws IllegalClassFormatException { ClassReader cr new ClassReader(b); ClassWriter cw new ClassWriter(cr, 0); ClassVisitor cv new ChangeVersionAdapter(cw); cr.accept(cv, 0); return cw.toByteArray(); } }); }仓库的 JavaAgent 实践文章 PreMain 与 ProfilingTransformer 示例 完整实现了这一模式通过MANIFEST.MF中的Premain-Class: org.itstack.sqm.asm.PreMain与Can-Redefine-Classes: true声明入口premain方法注册ProfilingTransformertransform方法内以ClassReader读取classfileBuffer、以ClassWriter重建再通过自定义ClassVisitor/MethodVisitor织入采集类名、方法名、入参出参与耗时的字节码——这就是 Java Agent ASM 非侵入式监控的标准范式。七、移除类成员不转发即是删除上一节用于转换类版本的方法自然也可用于ClassVisitor的其他方法。例如通过改变visitField和visitMethod的access或name参数可以改变字段或方法的修饰符或名字而选择根本不转发该调用其效果就是相应的类元素被移除。例如下面的类适配器移除外部类、内部类信息及源文件名所得到的类仍然具有全部功能因为删除的这些元素仅用于调试目的实现方式就是在适当的访问方法中不转发任何内容public class RemoveDebugAdapter extends ClassVisitor { public RemoveDebugAdapter(ClassVisitor cv) { super(ASM4, cv); } Override public void visitSource(String source, String debug) { } Override public void visitOuterClass(String owner, String name, String desc) { } Override public void visitInnerClass(String name, String outerName, String innerName, int access) { } }但这一策略对字段和方法无效因为visitField和visitMethod必须返回一个结果。要移除字段或方法需不转发方法调用并向调用者返回null。例如下面的类适配器按方法名及描述符移除方法仅使用名字不足以标识一个方法因为一个类中可能包含多个同名但参数不同的方法public class RemoveMethodAdapter extends ClassVisitor { private String mName; private String mDesc; public RemoveMethodAdapter( ClassVisitor cv, String mName, String mDesc) { super(ASM4, cv); this.mName mName; this.mDesc mDesc; } Override public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { if (name.equals(mName) desc.equals(mDesc)) { // 不要委托至下一个访问器 - 这样将移除该方法 return null; } return cv.visitMethod(access, name, desc, signature, exceptions); } }仓库实践文章中ProfilingClassAdapter.visitMethod对queryUserInfo之外的方法返回null正是利用返回 null 即移除的机制将不关心的方法从事件流中剥离。八、增加类成员多转发即是新增少转发收到的调用可以移除成员反过来多转发调用发出的调用数多于收到的调用就可以增加类成员。新的调用可以插入在原方法调用之间的若干位置只要遵守visitXxx的调用顺序约束见第二节。例如向一个类中添加字段必须在原方法调用之间添加一次对visitField的新调用且这个新调用必须放在类适配器的特定访问方法中不能放在visit中——这可能导致visitField之后跟有visitSource、visitOuterClass、visitAnnotation或visitAttribute是无效的同样不能放在visitSource、visitOuterClass、visitAnnotation或visitAttribute中仅有的合法位置是visitInnerClass、visitField、visitMethod或visitEnd。放在visitEnd中字段总会被添加除非增加显式条件因为这个方法总会被调用放在visitField或visitMethod中则会为原类中的每个字段和方法各添加一个字段。两种方案都可能发挥作用取决于需求例如可以仅添加一个计数器字段用于统计对一个对象的调用次数也可以为每个方法添加一个计数器分别统计。注意事项事实上唯一真正正确的解决方案是在visitEnd中做增加调用以添加新成员——一个类中不得包含重复成员要确保新成员不与已有成员重复唯一方法是先将新成员与所有已有成员对比而这只有在visitEnd中访问完所有成员后才能完成。这种做法相当受限在实践中使用程序员不大可能使用的生成名如_counter$或_4B7F_i就足以避免重复成员不必非把它们加到visitEnd中。另外第一章曾讨论过树 API 没有这一限制可以在任意时刻向使用该 API 的转换中添加新成员。下面的类适配器演示了完整实现——向类中添加一个字段除非该字段已存在public class AddFieldAdapter extends ClassVisitor { private int fAcc; private String fName; private String fDesc; private boolean isFieldPresent; public AddFieldAdapter(ClassVisitor cv, int fAcc, String fName, String fDesc) { super(ASM4, cv); this.fAcc fAcc; this.fName fName; this.fDesc fDesc; } Override public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) { if (name.equals(fName)) { isFieldPresent true; } return cv.visitField(access, name, desc, signature, value); } Override public void visitEnd() { if (!isFieldPresent) { FieldVisitor fv cv.visitField(fAcc, fName, fDesc, null, null); if (fv ! null) { fv.visitEnd(); } } cv.visitEnd(); } }该字段在visitEnd中追加visitField未被重写为修改或删除字段只检测目标字段是否已存在。注意fv ! null的检测类访问器可以在visitField中返回null上一节已看到因此必须判空。九、转换链多适配器的组合与扇出前面看到的是由ClassReader、单个类适配器和ClassWriter组成的简单转换链。当然可以使用更复杂的转换链将几个类适配器链接在一起组合出多个独立的类转换以完成复杂转换。转换链不一定是线性的。可以编写一个ClassVisitor将接收到的所有方法调用同时转发给几个ClassVisitor扇出public class MultiClassAdapter extends ClassVisitor { protected ClassVisitor[] cvs; public MultiClassAdapter(ClassVisitor[] cvs) { super(ASM4); this.cvs cvs; } Override public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { for (ClassVisitor cv : cvs) { cv.visit(version, access, name, signature, superName, interfaces); } } ... }反过来几个类适配器也可以委托至同一个ClassVisitor扇入——这需要采取预防措施确保比如visit和visitEnd针对该ClassVisitor恰好仅被调用一次。只要守住这条纪律链式、树状乃至扇出/扇入的复杂转换网络都是可行的。十、开发辅助工具与验证手段在动手编写类生成器或适配器时org.objectweb.asm.util包还提供了几个运行时不需要、但调试极为有用的工具详见 2.3 工具Type在内部名/类型描述符与源码形式之间手动转换。例如Type.getType(String.class).getInternalName()得到java/lang/StringType.INT_TYPE.getDescriptor()得到IgetArgumentTypes与getReturnType可拆分方法描述符。TraceClassVisitor生成所访问类的文本表示可同时委托给另一个访问器如ClassWriter便于确认生成结果是否符合预期。CheckClassAdapter在委托前验证方法的调用顺序与参数有效性出错时抛出IllegalStateException或IllegalArgumentException帮助尽早发现 JVM 验证器会拒绝的无效类。ASMifier为TraceClassVisitor提供另一种后端将访问事件打印为生成它的 ASM 代码——用 ASMifier 访问一个已编译类即可得到用 ASM 生成该类所需的源代码。这些工具配合前文的ClassVisitor调用顺序约束构成了开发字节码转换功能的完整调试闭环。总结ClassVisitor事件模型是理解 ASM 的钥匙ClassReader把已编译类解析为事件流ClassVisitor适配器在委托链中筛选、修改、增删事件ClassWriter将事件流重建为字节数组。三条核心纪律贯穿始终严格遵守visit→ 各访问方法 →visitEnd的调用顺序删除成员时返回null而不转发增加成员时把新调用放在visitInnerClass/visitField/visitMethod/visitEnd等合法位置。掌握这些之后无论是生成新类、改写版本、移除调试信息还是结合ClassFileTransformer做全局非侵入式监控都能在 2.2 接口和组件、3.2 接口和组件MethodVisitor 以及仓库的 ASM 实战文章 中继续深入。【免费下载链接】CodeGuide:books: 本代码库是作者小傅哥多年从事一线互联网 Java 开发的学习历程技术汇总旨在为大家提供一个清晰详细的学习教程侧重点更倾向编写Java核心内容。如果本仓库能为您提供帮助请给予支持(关注、点赞、分享)项目地址: https://gitcode.com/gh_mirrors/code/CodeGuide创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价