拓冰建站拓冰建站
首页 / 资讯中心 / 正文

CodeGuide 仓库 ASM 文档精读:深入 Java 类文件元数据(泛型签名、注解与调试信息)的生成与转换

文档教程后端【免费下载链接】CodeGuide:books: 本代码库是作者小傅哥多年从事一线互联网 Java 开发的学习历程技术汇总旨在为大家提供一个清晰详细的学习教程侧重点更倾向编写Java核心内容。如果本仓库能为您提供帮助请给予支持(关注、点赞、分享)项目地址https://gitcode.com/gh_mirrors/code/CodeGuide点击查看免费下载本文是 CodeGuide 仓库中 ASM 文档系列 第 4 章元数据的系统精读。类文件元数据——泛型签名、注解、调试信息——虽不参与字节码指令的运行时执行却支撑着反射、编译器分离编译、调试器与注解框架等核心能力。读完本文你将掌握用 ASM 核心 API 生成与转换这三类元数据的方法理解SignatureVisitor、AnnotationVisitor的访问者模型并能结合仓库中的字节码实战案例把元数据操纵能力用于 APM 监控、框架参数化等真实场景。在 Java 类文件.class中除了指令本身还存放着大量元数据类/方法/字段的泛型信息、各种注解、以及用于调试的源码行号与局部变量映射。ASM 为它们提供了完整的生成与转换 API。本章对应仓库文档 4.0元数据.md将逐节展开每一节首先介绍一种元数据类型及其类文件存储结构然后给出相应的 ASM 接口、组件与工具并配以说明性示例。一、元数据概述不执行指令却决定框架与工具的能力边界与字节码指令不同元数据不是由字节代码指令在运行时使用而是服务于三类消费方反射 API当注解或泛型信息的保留策略允许如RetentionPolicy.RUNTIME时可通过反射访问编译器泛型签名、注解可供编译器在分离编译separate compilation时使用调试器与异常栈行号与局部变量映射直接服务于javap、IDE 断点调试和异常堆栈追踪。因此元数据操纵是几乎所有 Java 框架Spring、MyBatis、依赖注入、AOP与字节码工具APM、混沌工程的底层支撑。如仓库实战文档 ASM字节码编程 所述ASM 可以直接产生二进制 class 文件也可以在类被加载入 Java 虚拟机之前动态改变类行为元数据正是其中可被读取、修改、删除、新增的一部分。二、泛型签名4.1递归语法与 SignatureVisitor 组件4.1.1 结构签名Signature与描述符Descriptor的关系由于类型和方法描述符的定义远早于 Java 5 引入泛型出于后向兼容原因泛型信息没有存入描述符而是存储在独立的签名Signature构造中类型签名、方法签名、类签名。在涉及泛型时这些签名与描述符一并存入类、字段和方法的声明中。值得强调的是泛型不影响方法字节码——编译器用它们做静态类型检查但会在必要时重新引入类型转换就像方法未被泛型化一样进行编译。类型签名的语法完整规则见《Java 虚拟机规范》因泛型的递归本质而非常复杂——一个泛型可将另一泛型作为参数例如ListListETypeSignature: Z | C | B | S | I | F | J | D | FieldTypeSignature FieldTypeSignature: ClassTypeSignature | [ TypeSignature | TypeVar ClassTypeSignature: L Id ( / Id )* TypeArgs? ( . Id TypeArgs? )* ; TypeArgs: TypeArg TypeArg: * | ( | - )? FieldTypeSignature TypeVar: T Id ;规则要点类型签名要么是基元类型描述符要么是字段类型签名字段类型签名定义为类类型签名、数组类型签名或类型变量类类型签名即类类型描述符在主类名或内部类名之后可携带尖括号中的类型参数内部类名以点前缀区分。常见的 Java 泛型类型与对应类型签名的对照如下Java 类型相应的类型签名ListELjava/util/ListTE;;List?Ljava/util/List*;List? extends NumberLjava/util/ListLjava/lang/Number;;List? super IntegerLjava/util/List-Ljava/lang/Integer;;ListListString[]Ljava/util/List[Ljava/util/ListLjava/lang/String;;;HashMapK, V.HashIteratorKLjava/util/HashMapTK;TV;.HashIteratorTK;;方法签名扩展了方法描述符描述方法参数的类型签名与返回类型签名此外还包含所抛出异常的签名前缀^并可在尖括号中包含可选的形式类型参数MethodTypeSignature: TypeParams? ( TypeSignature* ) ( TypeSignature | V ) Exception* Exception: ^ClassTypeSignature | ^TypeVar TypeParams: TypeParam TypeParam: Id : FieldTypeSignature? ( : FieldTypeSignature )*例如泛型静态方法static T Class? extends T m (int n)对应的方法签名为T:Ljava/lang/Object;(I)Ljava/lang/ClassTT;;类签名注意与类类型签名区分定义为超类的类型签名、所实现接口的类型签名以及可选的形式类型参数ClassSignature: TypeParams? ClassTypeSignature ClassTypeSignature*例如CE extends ListE的类签名为E:Ljava/lang/Object;Ljava/util/ListTE;;。4.1.2 接口与组件SignatureVisitor 与 SignatureReader/Writer与描述符一样ASM API 以签名在编译类中的存储形式直接暴露签名出现在ClassVisitor的visit、visitField、visitMethod方法中分别作为可选的类签名、类型签名或方法签名参数。同时org.objectweb.asm.signature包提供了基于SignatureVisitor抽象类的工具用于生成和转换签名public abstract class SignatureVisitor { public final static char EXTENDS ; public final static char SUPER -; public final static char INSTANCEOF ; public SignatureVisitor(int api); public void visitFormalTypeParameter(String name); public SignatureVisitor visitClassBound(); public SignatureVisitor visitInterfaceBound(); public SignatureVisitor visitSuperclass(); public SignatureVisitor visitInterface(); public SignatureVisitor visitParameterType(); public SignatureVisitor visitReturnType(); public SignatureVisitor visitExceptionType(); public void visitBaseType(char descriptor); public void visitTypeVariable(String name); public SignatureVisitor visitArrayType(); public void visitClassType(String name); public void visitInnerClassType(String name); public void visitTypeArgument(); public SignatureVisitor visitTypeArgument(char wildcard); public void visitEnd(); }访问类型签名时粗体标注的方法必须按以下顺序调用它直接反映前面的语法规则。注意其中两个方法返回SignatureVisitor——这正是类型签名递归定义的体现visitBaseType | visitArrayType | visitTypeVariable | ( visitClassType visitTypeArgument* ( visitInnerClassType visitTypeArgument* )* visitEnd ) )访问方法签名的方法顺序为( visitFormalTypeParameter visitClassBound? visitInterfaceBound* )* visitParameterType* visitReturnType visitExceptionType*访问类签名的方法顺序为( visitFormalTypeParameter visitClassBound? visitInterfaceBound* )* visitSuperClass visitInterface*关键约束这些返回SignatureVisitor的方法必须顺序使用——在完全访问一个嵌套签名之前不得调用父访问器的任何方法。这与ClassVisitor返回的MethodVisitor可以并行处理不同因为嵌套签名的访问是严格递归的。另外返回的SignatureVisitor不允许为null。与类访问模型相同ASM 基于该 API 提供了两个组件SignatureReader分析一个签名针对给定的签名访问器调用适当的访问方法事件产生器SignatureWriter基于接收的方法调用生成一个签名事件使用器其转发方法总是返回this。签名转换实战重命名类名的签名适配器。下面这个适配器利用除visitClassType和visitInnerClassType之外全部原样转发的策略对签名中出现的类名做重命名public class RenameSignatureAdapter extends SignatureVisitor { private SignatureVisitor sv; private MapString, String renaming; private String oldName; public RenameSignatureAdapter(SignatureVisitor sv, MapString, String renaming) { super(ASM4); this.sv sv; this.renaming renaming; } public void visitFormalTypeParameter(String name) { sv.visitFormalTypeParameter(name); } public SignatureVisitor visitClassBound() { sv.visitClassBound(); return this; } public SignatureVisitor visitInterfaceBound() { sv.visitInterfaceBound(); return this; } // ... public void visitClassType(String name) { oldName name; String newName renaming.get(oldName); sv.visitClassType(newName null ? name : newName); } public void visitInnerClassType(String name) { oldName oldName . name; String newName renaming.get(oldName); sv.visitInnerClassType(newName null ? name : newName); } public void visitTypeArgument() { sv.visitTypeArgument(); } public SignatureVisitor visitTypeArgument(char wildcard) { sv.visitTypeArgument(wildcard); return this; } public void visitEnd() { sv.visitEnd(); } }注意内部类的处理技巧visitClassType记录外层类名visitInnerClassType将外层名与内部名拼接如java/util/HashMap.HashIterator再查表替换。以下代码将Ljava/util/HashMapTK;TV;.HashIteratorTK;;重命名为LATK;TV;.BTK;;String s Ljava/util/HashMapTK;TV;.HashIteratorTK;;; MapString, String renaming new HashMapString, String(); renaming.put(java/util/HashMap, A); renaming.put(java/util/HashMap.HashIterator, B); SignatureWriter sw new SignatureWriter(); SignatureVisitor sa new RenameSignatureAdapter(sw, renaming); SignatureReader sr new SignatureReader(s); sr.acceptType(sa); sw.toString();4.1.3 工具用 TraceClassVisitor / ASMifier 反查签名2.3 工具 中介绍的TraceClassVisitor和ASMifier类以内部形式打印类文件中的签名。反查技巧编写一个带有目标泛型的 Java 类 → 用javac编译 → 用上述命令行工具查看即可得到与给定泛型对应的精确签名文本。这比手推语法规则高效得多也是理解复杂签名如多层嵌套泛型的捷径。三、注解4.2AnnotationVisitor 与增删注解适配器类、字段、方法和方法参数注解如Deprecated、Override只要保留策略不是RetentionPolicy.SOURCE就会存储在编译后的类中。与泛型类似注解信息不在运行时供字节码指令使用但当保留策略为RetentionPolicy.RUNTIME时可通过反射 API 访问也可供编译器使用。4.2.1 结构所有注解的统一内部形态源码中的注解形态各异Deprecated、Retention(RetentionPolicy.CLASS)、Task(descrefactor, id1)。但在类文件内部所有注解形式统一为一种注解类型 一组名称/值对取值仅限于基元、String或Class值枚举值注解值上述值的数组由于注解可以包含其他注解甚至注解数组注解结构可能非常复杂这正是AnnotationVisitor递归模型存在的原因。4.2.2 接口与组件AnnotationVisitor 抽象类public abstract class AnnotationVisitor { public AnnotationVisitor(int api); public AnnotationVisitor(int api, AnnotationVisitor av); public void visit(String name, Object value); public void visitEnum(String name, String desc, String value); public AnnotationVisitor visitAnnotation(String name, String desc); public AnnotationVisitor visitArray(String name); public void visitEnd(); }方法调用顺序visitEnd除外可按任意顺序( visit | visitEnum | visitAnnotation | visitArray )* visitEnd各方法语义visit(name, value)访问基元、String和Class值Class用Type对象表示visitEnum(name, desc, value)访问枚举值desc为枚举类型的描述符visitAnnotation(name, desc)返回嵌套注解的访问器用于注解中包含注解visitArray(name)返回数组元素访问器。注意数组元素未命名因此返回的访问器会忽略name参数可传null。与ClassVisitor返回的MethodVisitor不同返回的嵌套AnnotationVisitor必须顺序使用在完全访问一个嵌套注解前不能调用父访问器的任何方法。实战一删除注解 —— 返回 null 即可与字段/方法移除同理见 2.2.5 移除类成员在visitAnnotation中返回null即可删除对应注解public class RemoveAnnotationAdapter extends ClassVisitor { private String annDesc; public RemoveAnnotationAdapter(ClassVisitor cv, String annDesc) { super(ASM4, cv); this.annDesc annDesc; } Override public AnnotationVisitor visitAnnotation(String desc, boolean vis) { if (desc.equals(annDesc)) { return null; } return cv.visitAnnotation(desc, vis); } }实战二添加类注解 —— 必须尊重调用顺序添加类注解比删除更难因为必须遵守ClassVisitor的调用顺序约束visit→visitSource?→visitOuterClass?→(visitAnnotation | visitAttribute)*→(visitInnerClass | visitField | visitMethod)*→visitEnd。所有可能跟在注解访问之后的方法都必须被重写以便在所有注解都已访问完毕的恰当时机补插新的注解方法注解的添加因visitCode的存在而更容易public class AddAnnotationAdapter extends ClassVisitor { private String annotationDesc; private boolean isAnnotationPresent; public AddAnnotationAdapter(ClassVisitor cv, String annotationDesc) { super(ASM4, cv); this.annotationDesc annotationDesc; } Override public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { cv.visit(v, access, name, signature, superName, interfaces); } Override public AnnotationVisitor visitAnnotation(String desc, boolean visible) { if (visible desc.equals(annotationDesc)) { isAnnotationPresent true; } return cv.visitAnnotation(desc, visible); } Override public void visitInnerClass(String name, String outerName, String innerName, int access) { addAnnotation(); cv.visitInnerClass(name, outerName, innerName, access); } Override public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) { addAnnotation(); return cv.visitField(access, name, desc, signature, value); } Override public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { addAnnotation(); return cv.visitMethod(access, name, desc, signature, exceptions); } Override public void visitEnd() { addAnnotation(); cv.visitEnd(); } private void addAnnotation() { if (!isAnnotationPresent) { AnnotationVisitor av cv.visitAnnotation(annotationDesc, true); if (av ! null) { av.visitEnd(); } isAnnotationPresent true; } } }实现要点通过isAnnotationPresent标志去重避免重复添加addAnnotation()插在visitInnerClass/visitField/visitMethod/visitEnd的入口处保证新增注解总是位于所有已有注解之后符合调用顺序若原类版本低于 1.5适配器应将其更新至该版本——因为 JVM 会忽略版本低于 1.5 的类中的注解。实战三以注解实现转换参数化 —— 最常见的应用注解在类和方法适配器中最重要的应用场景是用注解参数化转换行为仅转换带Persistent注解的字段、仅对带Log注解的方法添加日志代码等。这一切之所以容易实现是因为注解总是最先被访问类注解先于字段和方法方法/参数注解先于代码。只需在检测到所需注解时置位标志如上例的isAnnotationPresent供后续转换逻辑使用即可。这与仓库实战中字节码增强监控耗时基于字节码插桩采集数据见 ASM 实战系列的拦截-标志-改写模式一脉相承。4.2.3 工具用 ASMifier 生成注解创建代码2.3 工具 中的TraceClassVisitor、CheckClassAdapter、ASMifier均支持注解也可用TraceAnnotationVisitor或CheckAnnotationAdapter在单个注解级别工作。运行java -classpath asm.jar:asm-util.jar \ org.objectweb.asm.util.ASMifier \ java.lang.Deprecated输出如下经微小重构展示如何用ACC_ANNOTATION标志创建注解类以及如何创建两个类注解——一个无值、一个带枚举值package asm.java.lang; import org.objectweb.asm.*; public class DeprecatedDump implements Opcodes { public static byte[] dump() throws Exception { ClassWriter cw new ClassWriter(0); AnnotationVisitor av; cw.visit(V1_5, ACC_PUBLIC ACC_ANNOTATION ACC_ABSTRACT ACC_INTERFACE, java/lang/Deprecated, null, java/lang/Object, new String[]{java/lang/annotation/Annotation}); { av cw.visitAnnotation(Ljava/lang/annotation/Documented;, true); av.visitEnd(); } { av cw.visitAnnotation(Ljava/lang/annotation/Retention;, true); av.visitEnum(value, Ljava/lang/annotation/RetentionPolicy;, RUNTIME); av.visitEnd(); } cw.visitEnd(); return cw.toByteArray(); } }方法注解和参数注解可用MethodVisitor中的visitAnnotation与visitParameterAnnotation方法以类似方式创建。如果你对注解的树 API 感兴趣可进一步阅读 9.2 注释AnnotationNode扩展了AnnotationVisitor提供desc与values字段并可在visitEnd中通过accept(cv.visitAnnotation(...))完成匿名内部类式的注解转换。四、调试信息4.3行号、局部变量与 SKIP_DEBUG 选项以javac -g编译的类包含三类可选调试信息源文件名源代码行号与字节码指令之间的映射源码局部变量名与字节码局部变量槽之间的映射。这些信息在调试器和异常栈轨迹中使用。调试信息是可选的——移除它们不影响类功能这正是 2.2.5 移除类成员 中RemoveDebugAdapter删除visitSource/visitOuterClass/visitInnerClass事件后类仍然具有全部功能的原因。4.3.1 结构行号映射与局部变量元组行号映射存储为(line number, label)对列表位于方法的已编译代码部分。若l1、l2、l3按序出现则对(n1, l1)、(n2, l2)、(n3, l3)的含义是l1与l2之间的指令来自第n1行l2与l3之间的指令来自n2l3之后的指令来自n3。注意一个行号可出现在多个对中——因为同一源码行的表达式其字节码指令可能不连续例如for (init; cond; incr) statement;通常编译为init statement incr cond的顺序。局部变量映射存储为(name, type descriptor, type signature, start, end, index)多元组列表在start与end两个标记之间槽index中的局部变量对应源码中名为name、类型由描述符/签名描述的局部变量。编译器可能复用同一局部变量槽存放不同作用域的变量如下例中槽 1 依次存放i、j、il1: ... // 这里的槽 1 包含局部变量i l2: ... // 这里的槽 1 包含局部变量j l3: ... // 这里的槽 1 再次包含局部变量i end:对应多元组为(i, I, null, l1, l2, 1) (j, I, null, l2, l3, 1) (i, I, null, l3, end, 1)4.3.2 接口与组件visitSource / visitLineNumber / visitLocalVariable调试信息通过ClassVisitor和MethodVisitor的三个方法访问源文件名 →ClassVisitor.visitSource(String source, String debug)行号映射 →MethodVisitor.visitLineNumber(int line, Label start)每次访问一对局部变量映射 →MethodVisitor.visitLocalVariable(String name, String desc, String signature, Label start, Label end, int index)每次访问一个多元组。visitLineNumber必须在作为参数传入的标记已被访问之后调用实践中紧跟该标记立即调用从而可随时得知当前指令的源码行号public class MyAdapter extends MethodVisitor { int currentLine; public MyAdapter(MethodVisitor mv) { super(ASM4, mv); } Override public void visitLineNumber(int line, Label start) { mv.visitLineNumber(line, start); currentLine line; } ... }类似地visitLocalVariable也必须在传入标记被访问后调用。与上节结构对应的方法调用序列为visitLineNumber(n1, l1); visitLineNumber(n2, l2); visitLineNumber(n3, l3); visitLocalVariable(i, I, null, l1, l2, 1); visitLocalVariable(j, I, null, l2, l3, 1); visitLocalVariable(i, I, null, l3, end, 1);忽略调试信息SKIP_DEBUG 选项为访问行号和局部变量名ClassReader可能需要引入人为Label对象——即跳转指令并不需要、仅为表示调试信息而创建的标记。这可能在指令删除类转换中造成误判指令序列中部的一个Label被当作跳转目标从而禁止该序列被删除。规避方式是在ClassReader.accept方法中使用SKIP_DEBUG选项读取器将不再访问调试信息、也不创建人为标记。当然调试信息会从类中删除仅在不会影响应用功能时使用。ClassReader还提供其他常用选项SKIP_CODE跳过对已编译代码的访问只需类结构时很有用SKIP_FRAMES跳过栈映射帧EXPAND_FRAMES解压缩栈映射帧。这些选项与 3.3 工具 中的分析工具配合可以显著提升类分析效率。4.3.3 工具TraceClassVisitor / CheckClassAdapter / ASMifier与泛型、注解场景一致可用TraceClassVisitor、CheckClassAdapter、ASMifier查看调试信息的访问方式例如在 2.2.3 生成类 的ClassWriter生成流程中若需保留调试能力可在方法访问器上补充visitLineNumber与visitLocalVariable调用或在转换链中保持这些事件不丢失。五、与仓库实战的结合从文档到可运行的字节码增强本章的元数据 API 并非孤立理论它与仓库中 ASM 实战系列一脉相承泛型签名在用ClassWriter.visitMethod生成带泛型的方法时签名参数signature即按本章语法传入用ASMifier反查签名可免去手工推导。注解仓库实战 ASM字节码编程 展示了ClassWriter生成HelloWorld的完整流程visit定义对象头、visitMethod添加方法、visitFieldInsn/visitLdcInsn/visitMethodInsn/visitInsn生成指令、visitMaxs设置操作数栈深度与局部变量大小、visitEnd收尾。若在此基础上叠加visitAnnotation与visitParameterAnnotation即可为生成的类和方法注入注解实现框架级参数化。调试信息在 JavaAgentASM 字节码插桩采集方法信息 这类非入侵监控场景中保持visitLineNumber/visitLocalVariable事件的透传可确保增强后的类仍具备完整调试体验而SKIP_DEBUG则适用于无需调试信息、追求转换链精简的场景。六、小结本章围绕类文件元数据给出了一条完整的学习主线元数据类型存储位置核心 ASM API关键组件泛型签名类/字段/方法声明中的 signatureSignatureVisitorSignatureReader/SignatureWriter注解类、字段、方法、参数AnnotationVisitor返回null删除、递归嵌套访问调试信息源文件名、行号映射、局部变量元组visitSource/visitLineNumber/visitLocalVariableSKIP_DEBUG等ClassReader选项三者共享相同的访问者Visitor设计哲学事件产生器Reader→ 事件筛选器Adapter→ 事件使用器Writer且都要求嵌套访问器顺序使用、完整访问。掌握这套模式后配合TraceClassVisitor、CheckClassAdapter、ASMifier等工具即可在字节码层面自由地生成、转换、删除和检测元数据为 APM 监控、框架注解、动态代理等高级能力打下坚实基础。后续章节将继续深入方法的代码结构3.0 方法与后向兼容规则5.0 后向兼容完整体系可参阅 1 引言。赞分享文档教程后端【免费下载链接】CodeGuide:books: 本代码库是作者小傅哥多年从事一线互联网 Java 开发的学习历程技术汇总旨在为大家提供一个清晰详细的学习教程侧重点更倾向编写Java核心内容。如果本仓库能为您提供帮助请给予支持(关注、点赞、分享)项目地址https://gitcode.com/gh_mirrors/code/CodeGuide点击查看免费下载相关推荐深入 ASM 元数据编程AnnotationVisitor 生成与转换类文件注释全解析深入 ASM 元数据编程AnnotationVisitor 生成与转换类文件注释全解析 导读 本文围绕小傅哥《CodeGuide 编程指南》仓库中 ASM 文文档教程后端终极指南Blazor调试中的TypeInfo与AssemblyInfo元数据解析技巧终极指南Blazor调试中的TypeInfo与AssemblyInfo元数据解析技巧 Blazor作为.NET生态中革命性的Web开发框架其调试机制依赖于对前端Web框架Stocks!替代方案探索IEX Cloud关闭后的数据源迁移策略Stocks!替代方案探索IEX Cloud关闭后的数据源迁移策略 当IEX Cloud服务宣布关闭后许多依赖其获取股票行情的应用程序面临数据中断风险。St创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门