ASM 泛型签名全解析:从 Signature 语法规则到 SignatureVisitor 字节码读写实战
【免费下载链接】CodeGuide:books: 本代码库是作者小傅哥多年从事一线互联网 Java 开发的学习历程技术汇总,旨在为大家提供一个清晰详细的学习教程,侧重点更倾向编写Java核心内容。如果本仓库能为您提供帮助,请给予支持(关注、点赞、分享)!项目地址: https://gitcode.com/gh_mirrors/code/CodeGuide
泛型信息在 class 文件中并不存在于类型/方法描述符里,而是以独立的"签名(Signature)"形式附加在类、字段和方法声明上。本文基于 CodeGuide 仓库中《ASM 文档》的 4.1 泛型 一章,系统讲解类型签名、方法签名、类签名的语法规则,深入剖析org.objectweb.asm.signature包中SignatureVisitor、SignatureReader、SignatureWriter三个核心组件,并结合仓库源码给出一个可运行的签名重命名适配器,帮助你掌握用 ASM 读写、转换泛型签名的完整技能。
泛型信息为什么不能放进描述符
诸如List<E>之类的泛型类,以及使用它们的类,包含了有关它们所声明或使用的泛型的信息。这一信息不是由字节码指令在运行时使用的,但可以通过反射 API 访问,还可以供编译器在分离编译(separate compilation)时使用。
为什么不能把泛型直接塞进类型描述符或方法描述符?关键原因在于后向兼容:类型和方法描述符的定义远早于 Java 5 对泛型的引入,一旦修改描述符格式,所有旧版本的 class 文件、JVM 和既有工具链都将被破坏。因此,泛型信息被保存在称为**签名(signature)**的类似构造中,与描述符并列存在。
从 class 文件结构上看(见 2.1 结构),已编译类中:
- 内部名(internal name)表示类/接口类型,如
String→java/lang/String; - 类型描述符表示字段类型,基元类型用单字符(
Z、C、B、S、I、F、J、D),类类型为L+ 内部名 +;,数组为[+ 元素描述符; - 方法描述符表示参数类型与返回类型,如
(I)V。
在涉及泛型时,除了描述符之外,签名还会存储在类、字段和方法声明中。需要特别强调的是:泛型不会影响方法的字节码——编译器用泛型执行静态类型检查,但会在必要时重新插入类型转换指令,就像这些方法未被泛型化一样进行编译。也就是说,泛型签名对 JVM 指令执行毫无影响,它是一份"元信息",供反射 API 和编译期工具读取。
签名语法:递归定义下的三类签名
与类型和方法描述符不同,类型签名的语法非常复杂,这源于泛型的递归本质——一个泛型可以将另一个泛型作为参数,例如List<List<E>>。其语法规则如下(完整规则见《Java 虚拟机规范》):
TypeSignature: 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 ;逐条解读这几条规则:
- 第一条规则:类型签名(TypeSignature)或者是一个基元类型描述符(
Z C B S I F J D),或者是一个字段类型签名(FieldTypeSignature)。基元类型没有泛型可言,所以直接复用描述符中的单字符表示。 - 第二条规则:字段类型签名可以是类类型签名(ClassTypeSignature)、数组类型签名(
[加类型签名)或类型变量(TypeVar)。注意数组可以递归嵌套,这也是签名可以变得很深的原因之一。 - 第三条规则:类类型签名就是类类型描述符的扩展——在主类名之后或内部类名之后(以
.为前缀)的尖括号中可能带有类型参数(TypeArgs)。L Id ( / Id )*部分与普通类描述符一致,后面可选的TypeArgs?与( . Id TypeArgs? )*则承载泛型信息。 - 第四条与第五条规则:类型参数(TypeArg)可以是通配符
*,也可以是带+(extends 上界)或-(super 下界)前缀的字段类型签名;类型变量(TypeVar)则是T加标识符加;。
一个类型参数本身可以是一个完整的字段类型签名,带有自己的类型参数——因此签名可能非常复杂。下面这张表将 Java 源代码中的类型与其对应的类型签名一一对应,是理解签名语法最直观的参考:
| Java 类型 | 相应的类型签名 |
|---|---|
List<E> | Ljava/util/List<TE;>; |
List<?> | Ljava/util/List<*>; |
List<? extends Number> | Ljava/util/List<+Ljava/lang/Number;>; |
List<? super Integer> | Ljava/util/List<-Ljava/lang/Integer;>; |
List<List<String>[]> | Ljava/util/List<[Ljava/util/List<Ljava/lang/String;>;>; |
HashMap<K, V>.HashIterator<K> | Ljava/util/HashMap<TK;TV;>.HashIterator<TK;>; |
从表中可以总结出几个重要规律:
- 类型变量
E写作TE;,T前缀表示"这是一个类型变量"; - 无界通配符
?写作*; - 有界通配符
? extends T写作+T(协变),? super T写作-T(逆变); - 数组类型直接在元素签名前加
[,因此List<String>[]写作[Ljava/util/List<Ljava/lang/String;>;; - 泛型内部类用
.分隔外层与内层,如HashMap<K, V>.HashIterator<K>中,<TK;TV;>跟随外层类名,.HashIterator<TK;>跟随内层类名。
方法签名:在描述符之上叠加异常与形式类型参数
方法签名扩展了方法描述符,就像类型签名扩展了类型描述符一样。它描述了方法参数的类型签名及其返回类型的签名,与方法描述符不同的是,它还额外包含了两类信息:
- 该方法所抛出的异常签名,前面带有
^前缀; - 可选的形式类型参数,放在尖括号之间。
MethodTypeSignature: TypeParams? ( TypeSignature* ) ( TypeSignature | V ) Exception* Exception: ^ClassTypeSignature | ^TypeVar TypeParams: < TypeParam+ > TypeParam: Id : FieldTypeSignature? ( : FieldTypeSignature )*形式类型参数TypeParam的规则值得留意:Id : FieldTypeSignature? ( : FieldTypeSignature )*中,第一个冒号后是类边界(可空),后续冒号后是接口边界(可多个)——这与 Java 语法T extends A & B & C的声明方式一一对应。
例如以下泛型静态方法,以类型变量T为参数:
static <T> Class<? extends T> m (int n)它对应的方法签名是:
<T:Ljava/lang/Object;>(I)Ljava/lang/Class<+TT;>;逐段拆解这个签名:
<T:Ljava/lang/Object;>:形式类型参数声明,T的类边界为Ljava/lang/Object;(Java 中未显式声明边界时默认是Object);(I):参数类型签名,一个int,与描述符中的I完全一致;Ljava/lang/Class<+TT;>;:返回类型签名,即Class<? extends T>,其中+TT;表示? extends T(+为通配符上界,TT;为类型变量T)。
类签名:超类 + 接口 + 形式类型参数
最后是类签名(ClassSignature),不要将它与类类型签名相混淆。类签名被定义为其超类的类型签名,后面跟有所实现接口的类型签名,以及可选的形式类型参数:
ClassSignature: TypeParams? ClassTypeSignature ClassTypeSignature*例如,一个声明为C<E> extends List<E>的类,其类签名为:
<E:Ljava/lang/Object;>Ljava/util/List<TE;>;这里<E:Ljava/lang/Object;>是形式类型参数声明,Ljava/util/List<TE;>;是超类List<E>的类型签名(实现接口时则依次列出各接口的类型签名)。
接口与组件:SignatureVisitor 访问者模型
和描述符的情况一样,出于相同的效果原因(见 2.3 工具 中对Type类的说明——ASM 为了避免两种表示之间的系统转换损失性能,直接以编译类中的存储形式公开类型),ASM API 公开签名的形式与它们在编译类中的存储形式相同。签名主要出现在ClassVisitor类的visit、visitField和visitMethod方法中,分别作为可选的类签名、类型签名或方法签名参数(见 2.2 接口和组件):
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces); 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);当类、字段或方法未使用泛型时,这三个signature参数均为null;一旦使用了泛型,就必须提供对应的签名字符串。
幸好,ASM 在org.objectweb.asm.signature包中提供了一些基于SignatureVisitor抽象类的工具,用于生成和转换签名。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(); }三个静态常量EXTENDS、SUPER、INSTANCEOF对应+、-、=三个通配符标记:+表示? extends(上界),-表示? super(下界),=表示?(确切类型参数,常用于Class<...>等场景)。
这个抽象类同时用于访问类型签名、方法签名和类签名。用于类型签名的方法(上文中以粗体区分的visitBaseType、visitArrayType、visitTypeVariable、visitClassType等)必须按以下顺序调用,它正好反映了前面给出的语法规则——注意其中有两个方法返回了SignatureVisitor,这正是类型签名递归定义的直接体现:
visitBaseType | visitArrayType | visitTypeVariable | ( visitClassType visitTypeArgument* ( visitInnerClassType visitTypeArgument* )* visitEnd ) )用于访问方法签名的方法调用顺序为:
( visitFormalTypeParameter visitClassBound? visitInterfaceBound* )* visitParameterType* visitReturnType visitExceptionType*用于访问类签名的方法调用顺序为:
( visitFormalTypeParameter visitClassBound? visitInterfaceBound* )* visitSuperClass visitInterface*这些方法大多返回一个SignatureVisitor:它是准备用来访问嵌套类型签名的。有一个关键的使用约束与ClassVisitor不同:SignatureVisitor返回的嵌套SignatureVisitor不得为 null,而且必须顺序使用——在完全访问完一个嵌套签名之前,不得访问父访问器的任何方法。
SignatureReader 与 SignatureWriter
和类的情况一样,ASM 基于SignatureVisitor这个 API 提供了两个组件:
SignatureReader:分析一个签名字符串,并针对一个给定的签名访问器调用适当的访问方法(相当于事件产生器);SignatureWriter:基于它接收到的方法调用生成一个签名字符串(相当于事件使用器)。
利用与类和方法相同的原理,这两个类可用于生成和转换签名。例如,假定我们希望对出现在某些签名中的类名进行重命名,这一效果可以用下面的签名适配器完成。除visitClassType和visitInnerClassType方法之外,它将自己接收到的所有其他方法调用都不加修改地转发(这里假设sv方法总是返回this,SignatureWriter就属于这种情况):
public class RenameSignatureAdapter extends SignatureVisitor { private SignatureVisitor sv; private Map<String, String> renaming; private String oldName; public RenameSignatureAdapter(SignatureVisitor sv, Map<String, 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(); } }这个适配器的实现要点:
- 转发类:
visitFormalTypeParameter、visitTypeArgument、visitEnd等方法原样转发; - 返回
this:visitClassBound、visitInterfaceBound、visitTypeArgument(char)等需要返回嵌套访问器的方法,直接返回this,使后续调用继续流向同一个适配器(即"顺序使用"约束的落地写法); - 重命名逻辑:
visitClassType记录当前的oldName并查表替换;visitInnerClassType则用outer.inner拼接出完整的内部类名再查表,从而正确处理HashMap<K,V>.HashIterator<K>这类嵌套内部类签名。
将该适配器与SignatureWriter、SignatureReader串联起来,即可完成一次完整的签名转换。以下代码的结果为"LA<TK;TV;>.B<TK;>;":
String s = "Ljava/util/HashMap<TK;TV;>.HashIterator<TK;>;"; Map<String, String> renaming = new HashMap<String, 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();整个数据流清晰可见:
SignatureReader解析签名字符串s,通过acceptType(sa)把解析事件派发给适配器;RenameSignatureAdapter在visitClassType/visitInnerClassType中完成HashMap→A、HashMap.HashIterator→B的重命名,其余事件原样透传;- 事件最终落到
SignatureWriter上,由其重新拼装出新的签名字符串。
注意这里的acceptType是针对类型签名的分析方法——SignatureReader还提供accept方法用于分析类签名/方法签名,对应入口是visit与visitField/visitMethod中传递的签名参数。
实践:如何用工具反查一个泛型对应的签名
在 2.3 工具 一节给出的TraceClassVisitor和ASMifier类,可以以内部形式打印类文件中包含的签名。利用它们,可以通过以下方式找出与一个给定泛型相对应的签名:
- 编写一个使用了目标泛型的 Java 类;
- 用
javac编译它,得到.class文件; - 用 ASM 命令行工具或自定义的
TraceClassVisitor打印器查看编译产物中记录的真实签名。
例如,使用ASMifier的命令行方式(见 2.3 工具 中的命令行示例):
java -classpath asm.jar:asm-util.jar org.objectweb.asm.util.ASMifier java.lang.RunnableASMifier会打印出用 ASM API 生成该类的完整 Java 源代码(ClassWriter、visitMethod等调用),其中visit方法的signature参数、visitField/visitMethod的signature参数即为所需的签名原文。同理,把TraceClassVisitor挂在ClassReader与ClassWriter之间,也可以在输出轨迹中直接看到// signature注释行标注的签名内容。
树 API 视角:泛型为什么没有对应的 Node
如果你转向 ASM 的树 API(org.objectweb.asm.tree包),会发现一个值得注意的设计取舍:树 API 没有提供对泛型的任何支持。事实上,它用签名表示泛型,这一点与核心 API 中一样,但却没有提供与SignatureVisitor对应的SignatureNode类——尽管这也是可能的(至少使用几个 Node 类来区分类型、方法和类签名会很方便)。这一点在仓库的 9.1 泛型 中有明确说明。
因此,当你在树 API 中操作ClassNode、FieldNode、MethodNode时,泛型信息仍然以signature字符串字段的形式原样保留;如果需要对其中的类名、类型变量进行程序化修改,仍需借助SignatureReader/SignatureWriter解析和重建签名,这与核心 API 的处理方式完全一致。
小结
- 泛型与字节码解耦:泛型签名不影响方法字节码,编译器完成静态类型检查后会在必要时插入类型转换指令;签名只是 class 文件中的一份元数据,供反射 API 与编译器分离编译使用;
- 三种签名、一套语法:类型签名(
TE;、*、+、-、数组[、内部类.)、方法签名(参数/返回类型签名 +^异常 + 尖括号形式类型参数)、类签名(形式类型参数 + 超类 + 接口),全部由递归文法定义; - 一套访问者模型:
SignatureVisitor是签名读写与转换的统一接口,SignatureReader解析产生事件,SignatureWriter消费事件生成签名,适配器在中间完成过滤与改写;嵌套访问器不得为 null、必须顺序使用; - 工具链齐备:
TraceClassVisitor、ASMifier可以把编译产物中的签名原样打印出来,是反查、调试签名语法的利器。
如需继续深入,可依次阅读仓库内的相关章节:2.1 结构(描述符与内部名基础)、2.2 接口和组件(ClassVisitor签名参数)、2.3 工具(TraceClassVisitor/ASMifier用法)、4.2 注释(另一类元数据),以及 9.1 泛型(树 API 下的处理方式)。
【免费下载链接】CodeGuide:books: 本代码库是作者小傅哥多年从事一线互联网 Java 开发的学习历程技术汇总,旨在为大家提供一个清晰详细的学习教程,侧重点更倾向编写Java核心内容。如果本仓库能为您提供帮助,请给予支持(关注、点赞、分享)!项目地址: https://gitcode.com/gh_mirrors/code/CodeGuide
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考