从字节码的视角中,一个 Java 类由很多组件凝聚而成,而这之中便包括超类、接口、属性、域和方法等等。当我们在使用 ASM 进行操控时,可以将它们视为一个个与之对应的事件。因此 ASM 提供了一个 类访问者 ClassVisitor,以通过它来访问当前类的各个组件,当解析器 ClassReader 依次遇到上述的各个组件时,ClassVisitor 上对应的 visitor 事件处理器方法均会被一一调用。
与类相似,方法也是由多个组件凝聚而成的,其对应着方法属性、注解及编译后的代码(Class 字节码)。ASM 的 MethodVisitor 提供了一种 hook(钩子)机制,以便能够访问方法中的每一个操作码,这样我们便能够对字节码文件进行细粒度地修改。
下面,我们便来一一分析下它们。
1、类访问者 ClassVisitor
通常我们在使用 ASM 的访问者模式有一个模板代码,如下所示:
InputStream is = new FileInputStream(classFile);
// 1
ClassReader classReader = new ClassReader(is);
// 2
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
// 3
ClassVisitor classVisitor = new TraceClassAdapter(Opcodes.ASM5, classWriter);
// 4
classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES);
首先,在注释1处,我们 将目标文件转换为流的形式,并将它融入类读取器 ClassReader 之中。然后,在注释2处,我们 构建了一个类写入器 ClassWriter,其参数 COMPUTE_MAXS 的作用是将自动计算本地变量表最大值和操作数栈最大值的任务托付给了ASM。接着,在注释3处,新建了一个自定义的类访问器,这个自定义的 ClassVisitor
的作用是为了在每一个方法的开始和结尾处插入相应的记时代码,以便统计出每一个方法的耗时。最后,在注释4处,类读取器 ClassReader 实例这个被访问者调用了自身的 accept 方法接收了一个 classVisitor 实例,需要注意的是,第二个参数指定了 EXPAND_FRAMES
,旨在说明在读取 class 的时候需要同时展开栈映射帧(StackMap Frame),如果我们需要使用自定义的 MethodVisitor 去修改方法中的指令时必须要指定这个参数,。
上面,我们说到了栈映射帧(StackMap Frame),它到底是什么呢?
栈映射帧 StackMap Frame
它是 Java 6 以后引入的一种验证机制,用于 检验 Java 字节码的正确性。它的工作方式是 记录每一个关键步骤完成后其方法中操作数栈的理论状态,然后,在实际运行的时候,ASM 会将其实际状态和理论状态对比,如果状态不一致则表明出现了错误。
但栈映射帧的实现并不简单,因此通过调用 classReader 实例的 accept 方法我们便可以让 ASM 自动去计算栈映射帧,尽管这 会增加 50% 的额外运算。此外,可能会有小概率的情况遇到 栈映射帧验证失败 的情况,例如:VerifyError: Inconsistent stackmap frames at branch target
这个错误。
最常见的原因可能就是由于 字节码写错造成的,此时,我们应该去检查对应的字节码实现代码。此外,也可能是 JDK 版本的支持问题或是 ASM 自身的缺陷,但是,这种情况几乎不会发生。
2、类读取(解析)者 ClassVisitor
现在,让我们再回到上述注释4处的代码,在这里,我们调用了 classReader 的 accept 方法接收了一个访问者 classVisitor,下面,我们来看看其内部的实现,代码如下所示(源码实现较长,这里我们只需关注注释处的代码即可:
/**
- Makes the given visitor visit the Java class of this {@link ClassReader}
- . This class is the one specified in the constructor (see
- {@link #ClassReader(byte[]) ClassReader}).
- @param classVisitor
-
the visitor that must visit this class.
- @param flags
-
option flags that can be used to modify the default behavior
-
of this class. See {@link #SKIP_DEBUG}, {@link #EXPAND_FRAMES}
-
, {@link #SKIP_FRAMES}, {@link #SKIP_CODE}.
*/
public void accept(final ClassVisitor classVisitor, final int flags) {
accept(classVisitor, new Attribute[0], flags);
}
在 accept 方法中又继续调用了 classReader 的另一个 accept 重载方法,如下所示:
public void accept(final ClassVisitor classVisitor,
final Attribute[] attrs, final int flags) {
int u = header; // current offset in the class file
char[] c = new char[maxStringLength]; // buffer used to read strings
Context context = new Context();
context.attrs = attrs;
context.flags = flags;
context.buffer = c;
// 1、读取类的描述信息,例如 access、name 等等
int access = readUnsignedShort(u);
String name = readClass(u + 2, c);
String superClass = readClass(u + 4, c);
String[] interfaces = new String[readUnsignedShort(u + 6)];
u += 8;
for (int i = 0; i < interfaces.length; ++i) {
interfaces[i] = readClass(u, c);
u += 2;
}
// 2、读取类的属性信息,例如签名 signature、sourceFile 等等。
String signature = null;
String sourceFile = null;
String sourceDebug = null;
String enclosingOwner = null;
String enclosingName = null;
String enclosingDesc = null;
int anns = 0;
int ianns = 0;
int tanns = 0;
int itanns = 0;
int innerClasses = 0;
Attribute attributes = null;
u = getAttributes();
for (int i = readUnsignedShort(u); i > 0; --i) {
String attrName = readUTF8(u + 2, c);
// tests are sorted in decreasing frequency order
// (based on frequencies observed on typical classes)
if (“SourceFile”.equals(attrName)) {
sourceFile = readUTF8(u + 8, c);
} else if (“InnerClasses”.equals(attrName)) {
innerClasses = u + 8;
} else if (“EnclosingMethod”.equals(attrName)) {
enclosingOwner = readClass(u + 8, c);
int item = readUnsignedShort(u + 10);
if (item != 0) {
enclosingName = readUTF8(items[item], c);
enclosingDesc = readUTF8(items[item] + 2, c);
}
} else if (SIGNATURES && “Signature”.equals(attrName)) {
signature = readUTF8(u + 8, c);
} else if (ANNOTATIONS
&& “RuntimeVisibleAnnotations”.equals(attrName)) {
anns = u + 8;
} else if (ANNOTATIONS
&& “RuntimeVisibleTypeAnnotations”.equals(attrName)) {
tanns = u + 8;
} else if (“Deprecated”.equals(attrName)) {
access |= Opcodes.ACC_DEPRECATED;
}
else if (“Synthetic”.equals(attrName)) {
access |= Opcodes.ACC_SYNTHETIC
| ClassWriter.ACC_SYNTHETIC_ATTRIBUTE;
} else if (“SourceDebugExtension”