ASM 库提供供了两个用于生成和转换已编译类的 API,一个是核心 API,以基于事件的形式 来表示类,另一个是树 API,以基于对象的形式来表示类。
第一部分 核心(Core) API
第二章
ASM提供了三个基于ClassVisitor API的核心组件,用于生成和变化类
- ClassReader类分析以字节数组形式给出的已编译类,并针对其在accept方法参数中传送的ClassVisitor实例,调用相应的visitXxx方法,这个类可以看作一个事件产生器。
- ClassWriter类是ClassVisitor抽象类的一个子类,它直接以二进制形式生成编译后的类。它会生成一个字节数组形式的输出,其中包含了已编译类,可以用toByteArray方法来提取。这个类可以看作一个事件使用器。
- ClassVisitor类它将收到的所有方法调用都委托给另一个ClassVisitor类。这个类可以看作一个事件筛选器。
分析类:必须的组件 ClassReader
生成类:必须的组件ClassWriter
已编译类不包含Package和Import部分,因此,所有的类名必须是完全限定的。
自定义ClassVisitor 转换类
@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);
}
建议性优化:
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();
移除/增加类成员:
需要方法名字和方法描述符
//移除方法
@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);
}
//增加类成员
@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();
}
2.3 工具
开发生成器和适配器非常有用
2.3.1
Type类,可以用于方便的获取类型,返回字段,方法描述符等等
2.3.2
TraceClassVisitor类,适配器生成的类的文本表示形式,该类可以用来打印生成的类,调试利器
public static void test2(){
ClassWriter cw = new ClassWriter(0);
TraceClassVisitor cv = new TraceClassVisitor(cw,new PrintWriter(System.out));
cv.visit(V1_5, ACC_PUBLIC + ACC_ABSTRACT + ACC_INTERFACE,
"pkg/Comparable", null, "java/lang/Object",
new String[] { "pkg/Mesurable" });
cv.visitField(ACC_PUBLIC + ACC_FINAL + ACC_STATIC, "LESS", "I",
null, new Integer(-1)).visitEnd();
cv.visitField(ACC_PUBLIC + ACC_FINAL + ACC_STATIC, "EQUAL", "I",
null, new Integer(0)).visitEnd();
cv.visitField(ACC_PUBLIC + ACC_FINAL + ACC_STATIC, "GREATER", "I",
null, new Integer(1)).visitEnd();
cv.visitMethod(ACC_PUBLIC + ACC_ABSTRACT, "compareTo",
"(Ljava/lang/Object;)I", null, null).visitEnd();
cv.visitEnd();
}
2.3.3
CheckClassAdapter 验证其对方法的调用顺序是否恰当,参数是否有效,然后才会委托给下一个访问器。当发生错误时,会抛出 IllegalStateException 或 IllegalArgumentException。
2.3.4
ASMifer 得到已编译类的ASM代码
第三章:方法
生成和转换已编译方法
3.1 结构
3.1.1执行模型
栈帧,局部变量,操作数栈
double和long占据两个slot
3.1.2 字节代码指令
字节代码指令有一个标识该指令的操作码和固定数目的参数组成
字节代码指令可以分为两类:
一小组指令,设计用来在局部变量和操作数栈之间传送值;
其他一些指令仅用于操作数栈:它们从栈中弹出一些值,根据这些值计算一个结果,并将它压回栈中。
ILOAD,LLOAD,FLOAD,DLOAD,ALOAD指令读取一个局部变量,并将它的值压到操作数栈中。
ISTORE,LSTORE,FSTORE,DSTORE,ASTORE指令从操作数栈中弹出一个值,并将它存储在由其索引i 指定的局部变量中。
字节码的类别:
栈 POP弹出栈顶部的值,DUP压入栈值的一个副本,SWAP弹出两个值,并按逆序压入它们等。
**常量 ** 这些指令在操作数栈压入一个常量值:ACONST_NULL压入null,ICONST_0压入 int 值 0, FCONST_0 压入 0f,DCONST_0 压入 0d,BIPUSH b 压入字节值 b,SIPUSH s 压入 short 值 s,LDC cst 压 入任意 int、float、long、double、String 或 class1 常量 cst,等等。
算术与逻辑 这些指令从操作数栈弹出数值,合并它们,并将结果压入栈中。它们没有任何 参数。xADD、xSUB、xMUL、xDIV 和 xREM 对应于**+、-、、/和%运算,其中 x 为 I、 L、F 或 D 之一。类似地,还有其他对应于<<、>>、>>>、|、&和^运算的指令,用于 处理int和long*值。
类型变换 这些指令从栈中弹出一个值,将其转换为另一类型,并将结果压入栈中。它们对 应于 Java 中的类型转换表达式。I2F, F2D, L2D 等将数值由一种数值类型转换为另一种 类型。CHECKCAST t 将一个引用值转换为类型 t。
对象 这些指令用于创建对象、锁定它们、检测它们的类型,等等。例如,NEW type指令将 一个 type 类型的新对象压入栈中(其中 type 是一个内部名)。
字段 这些指令读或写一个字段的值。GETFIELD owner name desc 弹出一个对象引用,并 压和其 name 字段中的值。PUTFIELD owner name desc 弹出一个值和一个对象引用,并 将这个值存储在它的 name 字段中。在这两种情况下,该对象都必须是 owner 类型,它 的字段必须为 desc 类型。GETSTATIC 和 PUTSTATIC 是类似指令,但用于静态字段。
方法 这些指令调用一个方法或一个构造器。它们弹出值的个数等于其方法参数个数加 1 (用于目标对象),并压回方法调用的结果。INVOKEVIRTUAL owner name desc 调用在 类 owner 中定义的 name 方法,其方法述符为 desc。INVOKESTATIC 用于静态方法, INVOKESPECIAL 用于私有方法和构造器,INVOKEINTERFACE 用于接口中定义的方 法。最后,对于 Java 7 中的类,INVOKEDYNAMIC 用于新动态方法调用机制。
数组 这些指令用于读写数组中的值。xALOAD指令弹出一个索引和一个数组,并压入此索 引处数组元素的值。xASTORE 指令弹出一个值、一个索引和一个数组,并将这个值存 储在该数组的这一索引处。这里的 x 可以是 I、L、F、D 或 A,还可以是 B、C 或 S。
跳转 这些指令无条件地或者在某一条件为真时跳转到一条任意指令。它们用于编译if、 for、do、while、break 和 continue 指令。例如,IFEQ label 从栈中弹出一个 int 值,如果这个值为 0,则跳转到由这个 label 指定的指令处(否则,正常执行下一 条指令)。还有许多其他跳转指令,比如 IFNE 或 IFGE。最后,TABLESWITCH 和 LOOKUPSWITCH 对应于 switch Java 指令。
返回 最后,xRETURN和RETURN指令用于终止一个方法的执行,并将其结果返回给调用 者。RETURN 用于返回 void 的方法,xRETURN 用于其他方法。
3.1.4 异常处理器
3.1.5 帧
栈映射帧,用于加快Java虚拟机中类验证过程的速度。仅为哪些对应于跳转目标或者异常处理器的指令,或者跟在无条件跳转指令之后的指令包含帧。
为节省更多空间,对每一帧都进行压缩:仅存储它与前一帧的差别,而初始帧根本不用存储,可以轻松地有方法参数类型推导得出。
3.2 接口与组件
调用顺序的规定
visitAnnotationDefault? (visitAnnotation |visitParameterAnnotation |visitAttribute )* ( visitCode (visitTryCatchBlock |visitLabel |visitFrame |visitXxxInsn | visitLocalVariable |visitLineNumber )* visitMaxs )? visitEnd
visitCode 和 visitMaxs 方法可用于检测该方法的字节代码在一个事件序列中的 开始与结束。和类的情况一样,visitEnd 方法也必须在最后调用,用于检测一个方法在一个事件序列中的结束
ASM 供了三个基于 MethodVisitor API 的核心组件,用于生成和转换方法:
-
ClassReader 类分析已编译方法的内容,在其 accept 方法的参数中传送了 ClassVisitor , ClassReader 类 将 针 对 这 一 ClassVisitor 返 回 的 MethodVisitor 对象调用相应方法。
-
ClassWriter 的 visitMethod 方法返回 MethodVisitor 接口的一个实现,它直 接以二进制形式生成已编译方法。
-
MethodVisitor类将它接收到的所有方法调用委托给另一个MethodVisitor方法。 可以将它看作一个事件筛选器。
ClassWriter选项
1. 在使用newClassWriter(0)时,不会自动计算任何东西。必须自行计算帧、局部变 量与操作数栈的大 小。
2. 在使用newClassWriter(ClassWriter.COMPUTE_MAXS)时,将为你计算局部变量 与操作数栈部分的大小。还是必须调用 visitMaxs,但可以使用任何参数:它们将被 忽略并重新计算。使用这一选项时,仍然必须自行计算这些帧。 3. 在 new ClassWriter(ClassWriter.COMPUTE_FRAMES)时,一切都是自动计算。 不再需要调用 visitFrame,但仍然必须调用 visitMaxs(参数将被忽略并重新计算)。
为了自动计算帧,有事需要计算两个给定类的公共超类,默认情况下,ClassWriter类会在getCommonSuperClass方法中进行计算,它会将两个类加载到JVM中,并使用反射API。如果我们正在生成几个互相引用的类。那可能会导致问题,因为被引用的类可能尚未存在。在这种情况下,可以重写getCommonSupterClass方法来解决这一问题。
生成方法
对 visitMaxs 的调用必须在已经访问了所有这些指令后执行。它用于为这个方法的执行帧定义局部变量和操作数栈部分的大小。
转换方法
无状态转换:转换是局部的,不会依赖于在当前指令之前访问的指令
有状态转换
AnalyzerAdapter
LocalVariablesSorter:这个方法适配器将一个方法中使用的局部变量按照他们在这个方法中出现的顺序重新编号 AdviceAdatper:这个方法适配器是一个抽象类,可用于在一个方法的开头以及恰在任意 RETURN 或 ATHROW 指令之前插入代码。它的主要好处就是对于构造器也是有效的,在构造器中,不能将代码恰好插 入到构造器的开头,而是插在对超构造器的调用之后。
第四章 元数据
-
泛型
类型签名 方法签名 类签名
SignatureVisitor:这个抽象类用于访问类型签名,方法签名和类签名。
SignatureReader 组件分析 一个签名,并针对一个给定的签名访问器调用适当的访问方法;
SignatureWriter 组件基于 它接收到的方法调用生成一个签名。
-
注释
如果保留策略是 RetentionPolicy.RUNTIME,则可以通过 反射 API 访问它。它还可以供编译器使用。
AnnotationVisitor生成和转换注释
TraceAnnotationVisitor
CheckAnnotationAdapter
-
调试
第五章 向后兼容
基本规则:
规则 1:要为 ASM X 编写一个 ClassVisitor 子类,就以这个版本号为参数,调用ClassVisitor 构造器,在这个版本的 ClassVisitor 类中,绝对不要重写或调用弃用的方法(或者将在之后版本引入的方法)。
规则 2:不要使用访问器的继承,而要使用委托(即访问器链)。一种好的做法是让你的访问器类在默认情况为 final 的,以确保这一特性。
等等
第二部分 树API
第六章 类
用于生成和转换已编译Java类的ASM树是基于ClassNode类的
用树API生成类的过程就是:创建一个ClassNode对象,并初始化它的字段。使用树API生成类时,需要多花费大约30%的时间,占用的内存也多余使用Core API。但可以按任意顺序生成元素类。
树 (Tree)API 通常用于那些不能由核心(Core) API 一次实现的转换。
第七章 方法
第八章 方法分析
第九章 元数据
- 不支持泛型
- 注解
- 调试
第十章 向后兼容
最后的分析:
90%的转换时间用于类分析和写入。
“复制常量池”优化可速 15-20%。
基于树的转换要比基于访问器的慢大约 25%。
COMPUTE_MAXS选项不会耗时太多。
COMPUTE_FRAMES选项耗时很多⇒进行增量帧更新。 分析包的成本非常高!