Java ASM详解:注解
在 Java 语言中,注解(Annotation)是很重要的一部分。它的存在让许多代码变得简洁。与注解执行器(Annotation Processor)结合之后,它能发挥出意想不到的功能。这篇文章就将讲述注解是什么写入在字节码中的。
注解类型的定义在前文(类的结构二)中已经写过了,如果不了解注解类型的写入可以先查看那一篇文章。
一.写入注解信息
写入注解信息需要用到 AnnotationVisitor 这个类,它含有的方法支持我们对注解添加信息
1.写入常量
写入常量需要使用 visit 方法,它的第一个参数代表注解属性的名称,第二个参数代表值。可以写入的常量类型有:8种基本类型、字符串和 Type 对象(不包括方法描述符)。
2.写入枚举常量
写入枚举常量需要使用 visitEnum 方法,第一个参数仍然是注解属性的名称,第二个是枚举对象的类型描述符,第三个是枚举对象的名称。
3.写入其他注解类型
写入其他注解类型对象需要使用 visitAnnotation 方法,第一个参数是注解属性名称,第二个是要写入的注解类型的类型描述符。这个方法返回一个新的 AnnotationVisitor ,使用这个新的 visitor 可以填充要写入的注解类型对象的信息。
假如说我们要写入下面的注解:
我们需要写下面这些代码:
4.写入数组
除了上述类型之外,注解类型还允许在注解中定义一维数组。写入数组需要用到 visitArray
方法,参数为注解属性名称,返回一个新的 AnnotationVisitor 用于填充这个数组。写入数组信息时,所有的注解属性名称都要写为 null,并且不允许再调用 visitArray,因为注解类型不允许二维及多维数组的存在。
下面是一个例子:
写入需要下面的代码:
5.写入带有@Repeatable注解的注解类型
通常情况下,一个注解位置每个注解类型只能声明一次,但是带有 @Repeatable 的注解类型可以多次声明。假设有下面的注解类型:
如果在注释位置上只有一个 Test 注解,那么写入时只需要写 Test;但是如果一个位置上有多个 Test 注解,则应该使用 TestContainer 进行等效代替并写入,像下面这样:
二.注解类型的可见性
根据不同注解类型的作用,在定义注解类型时我们通常都会设置它的可见性,也就是使用 @Retention 进行设置。

可见性影响了反射时我们能不能访问到这个注解,如果不写则默认为 false。
三.注解的写入位置
注解不是哪里都能写入的,它有一套非常详细的使用方法,下面我们将分类讲解。
1.类和类成员定义时的注解
当类与类的成员(字段、方法、记录元素)被定义时,它可以附加注解,例如:
这种注解在写入时需要调用各个 visitor 的 visitAnnotation 方法。其中第一个参数是注解类型的类型描述符,第二个是注解类型的可见性。
对于附加在类 TestAnnotation 上的注解 Retention,我们需要这样写入:
如果一个成员在被定义时被添加上了 @Deprecated 注解,那么在定义这个成员时也要加上 ACC_DEPRECATED 访问标志。
对于 value 方法,我们需要这样写入:
2.类型的注解
在类型上,我们也能加入注解,这些注解 @Target 中必须含有 TYPE_USE。要知道类型的注解怎么插入,我们需要先了解两个概念:类型路径(Type Path)和类型引用(Type
Reference)。
a.类型路径
为了指定注解在类型中的位置,JVM 引入了类型路径。假设我们有下面一个泛型需要进行注解:
可以看到,注解 ABCDE 分别注解了一个复杂类型中的不同元素。如果我们从类型的最外层开始对类型的参数进行遍历,我们就能最终指定插入的位置。遍历的每一步(step)都含有两个属性:
类型路径类别(Type Path Kind),它决定了这一步该走向哪种元素,可以使用的值如下表:

类型参数索引(Type Argument Index),代表在同一级的几个相同类别的元素中要选择哪个。例如在 Map<@A Test, Object> 中,泛型参数共有两个,要指定其中一个需要用到这个索引。请注意,只有泛型参数才需要指定索引,其它的类别不需要。
假设我们有下面的类型需要注解:
那么我们访问到注解 A 的路径就像这样:
对于数组来说,注解的位置影响了它的路径长度,下面是按照 A 注解类型路径长度逐渐增大排序的注解示例:
下面是一个组合的例子:
在 ASM 库中,类型路径使用 TypePath 包装,创建一个 TypePath 对象需要使用 fromString 方法,它的参数是一个字符串,这个字符串里面存储了可以复原 TypePath 的所有信息。对于每一步,都有一个对应关系,这些步按顺序连接起来就能复原 TypePath。

上面的例子可以用 0;*.0;[[ 代替。
b.类型引用
类型路径决定了注解在一个类型内的位置,而类型引用指定了这个被注解类型的位置。
类型引用本质上是一个 int,其中第25~32位是引用的类型,1~24位是引用的参数。ASM 库提供了 TypeReference 类来简化创建这些数字的代码。
先来说说无参的类型引用类型,这些类型可以使用 newTypeReference 创建 TypeReference 对象,之后通过 getValue 方法获得 int 形式的类型引用,如下表:

接下来说一下有参的类型引用类型。
需要类型参数的类型引用。它们需要使用 newTypeParameterReference 获得 TypeReference 对象,第二个参数是类型参数的序号。

需要类型参数边界的类型引用。类型参数边界即 <T extends ...> 这种类型参数后面的限定,可以不止一个。它们需要使用 newTypeParameterBoundReference 获得TypeReference 对象,第二个参数是类型参数的序号,第三个参数是规定边界限定的序号。

需要超类序号的类型引用。超类序号是定义类时指定的继承类和实现类的序号,继承类的序号是-1,实现类的序号按照定义顺序从0计数。使用 newSuperTypeReference 创建 TypeReference 对象,第二个参数就是超类序号。类型固定为 CLASS_EXTENDS,可用在 ClassVisitor::visitTypeAnnotation 和 RecordComponentVisitor::visitTypeAnnotation 方法中。
需要方法形式参数序号的类型引用。使用 newFormalParameterReference 创建对象,类型固定为 METHOD_FORMAL_PARAMETER,可用在 MethodVisitor::visitTypeAnnotation 中。
需要方法异常列表序号的类型引用。使用 newExceptionReference 创建 TypeReference,类型固定为 THROWS,可用在 MethodVisitor::visitTypeAnnotation 中。
需要 try-catch 块序号的类型引用。用 newTryCatchReference 创建,类型是 EXCEPTION_PARAMETER,使用 MethodVisitor::visitTryCatchAnnotation 写入字节码。
需要实际参数序号的类型引用。它们需要使用 newTypeArgumentReference 创建,都需要使用 MethodVisitor::visitInsnAnnotation 写入。

下面是一个例子:
下面将写入这个 test 方法:
3.方法形式参数上的注解
等下,我们刚刚不是说过形式参数上怎么插入注解了吗?事实上,形式参数可以使用两种注解:一种是标记为 TYPE_USE 的类型注解,另一种是标记为 PARAMETER 的形式参数注解。如果一个注解同时拥有这两个标志,就都要写入字节码。
写入形式参数注解需要两步:写入注解形式参数数量、写入形式参数注解。
写入注解形式参数数量需要使用 visitAnnotableParameterCount 方法。假设我们有一个方法:
它的形式参数是两个,因为接收器 this 不是形式参数。写入如下:
第二个参数代表了注解的可见性。
写入形式参数注解需要使用 visitParameterAnnotation,参数类似定义注解的使用方法。
请注意:如果形式参数列表中同时存在运行时可见和不可见的注解,那么先写 visitAnnotableParameterCount,可见性为 true,在后面写出所有运行时可见的注解;之后再一行 visitAnnotableParameterCount,可见性为 false,在后面写出所有运行时不可见的注解。visitAnnotableParameterCount 对于每个可见性只出现一次。
4.注解类型中的 default 默认值
在类的结构二中,我们说到了有默认值的注解属性怎么写入。它使用的是 visitAnnotationDefault 方法。
它返回的 AnnotationVisitor 需要写入一个 name 为 null 的属性,这个属性写入什么值和怎么写入取决于你要决定的默认值。

到这里有关于注解的相关知识都已经说完了,下篇专栏可能是 ASM Tree API 部分。