元数据
泛型
泛型在运行时并不会被字节码指令使用,但可以被反射API拿到,可以被编译器使用。
由于向前兼容的原因,泛型并没有存储在类型或者方法的描述符中而是在类似的结构类型、方法、类的签名中。当一个泛型类型被使用的时候这些签名就会和类型的描述符相关联(泛型类型并不会影响字节码中的函数:编译器只是使用它们做静态的类型检查,编译class的时候直接忽略这些信息,必要的时候进行类型转换)。
和方法描述符不同,泛型可以递归定义(List<List<String>>)因此比较复杂。
List<? extends Number> 对应的签名如:Ljava/util/List<+Ljava/lang/Number;>;
方法签名就是把方法中类型替换成类型签名,和方法描述符不同,签名中仍可包含抛出的异常和使用<>的类型说明。例如<T:Ljava/lang/Object;>(I)Ljava/lang/Class<+TT;>;
代表static <T> Class<? extends T> m (int n)
再者,类的签名中泛型描述需要避免和父类签名混淆 eg C<E> extends List<E>is <E:Ljava/lang/Object;>Ljava/util/List<TE;>;.
相关api
可以使用SignalVisitor来访问或者修改泛型数据
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();
}
访问泛型方法签名的顺序如下:
( visitFormalTypeParameter visitClassBound? visitInterfaceBound* )*
visitParameterType* visitReturnType visitExceptionType*
访问泛型类签名的顺序:
( visitFormalTypeParameter visitClassBound? visitInterfaceBound* )*
visitSuperClass visitInterface*
和类的访问类似,api提供了SignatureReader和SignatureWriter。
TraceClassVisitor和ASMifier同样可以用于获取签名数据
注解
注解数据,如果不是声明为RetentionPolicy.SOURCE的话,都会存储在字节码中。虽然不被字节码指令使用但是可以使用反射来获取先关信息只要其声明为RetentionPolicy.RUNTIME,也可以被编译器使用。
虽然源代码中注解形式多样,eg @Deprecated @Retention(RetentionPolicy.CLASS) or @Task(desc="refactor") 但编译后注解只有一种形式,声明一个注解类型和一组键值对,其值限制在:基础数据类型 字符串 或者Class类型,enum类型, 注解类型,以上类型的数组。由于注解可以嵌套并且支持数组,因此也比较复杂
访问或者生成注解我们可以使用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();
}
由于ClassVisitor调用顺序有严格限制的关系,我们在向一个class添加注解的时候就比较麻烦,需要在其他visit方法调用前去check注解是否已经添加。而对于制定函数添加注解则比较简单。class annotation必须要在成员和函数访问之前访问。
TraceClassVisitor、CheckClassAdapter、ASMifier同样支持注解。
调试相关数据
使用javac -g编译的class文件包含源文件名称,字节码和源文件行号的mapping文件,源文件变量和字节码操作栈变量之间的mapping文件。这些可选的信息被用于调试及异常堆栈信息处理。
源文件名字被保存在class文件的一块专属区域,源文件行号和字节码指令的mapping被以(line number, label)的形式存放在函数的代码区。例如
(n1, l1)
(n2, l2)
(n3, l3)
说明l1l2之间的指令来自第n1行代码,l2l3来自n2等。同一个行号有可能出现在多个指令区间。
源文件的变量和操作栈变量的mapping是一系列诸如(name, type descriptor, type signature, start, end, index)
的数据,他们存储于方法的代码区。上述一行数据的含义是在start和end之间本地符号表index对应的变量对应于源代码中名字和类型分别于前三个值对应的变量。
源文件名字可以通过ClassVisitor的visitSource方法得到,源代码行号和字节码紫菱的mapping可以通过MethodVisitor的visitLineNumber来得到;符号表的映射关系可以通过MethodVisitor的visitLocalVariable方法获取。
为了获取相关的调试信息,ClassReader需要引入许多人造的label,这些label并不是执行代码所需要的。为避免这些debug信息带来的额外的负担,我们可以将SKIP_DEBUG传给ClassReader.accept函数,这样class reader就不会访问debug信息了。ClassReader还包含其他的可选参数:
- SKIP_CODE不访问方法实体,对于只需要获取class框架的场景非常适用。
- SKIP_FRAMES 跳过stack map frames
- EXPAND_FRAMES 不再压缩frames
兼容性问题
由于class新特性变更等原因,ASM不是一直向前兼容的。不过如果我们依照以下建议,从ASM4.0开始后续的兼容性问题将尽可能的被避免。
为了处理兼容性问题,4.0开始ClassVisitor、FieldVisitor、MethodVisitor都从接口转化为抽象类,并且在构造函数中传入ASM版本号。
新的 class文件格式可能会对已有的class分析和适配工具带来难以预料的兼容性问题。
对于生成class的代码,基本没有限制,ASM基本能保持兼容
对于ClassVisitor或者FieldVisitor、MethodVisitor你需要按照以下原则来:
- 使用XXXVisitor时使用显示ASM版本号的构造函数,并且不调用当前版本已经不建议使用的函数
- 所有继承体系上的类应该使用相同的ASM版本号。
- 不要使用继承来实现visitor,尽量使用代理替代。如果可以尽可能使你的visitor类为final的