文章目录
前言
本章解释了如何使用核心API生成和转换已编译的Java类。下文介绍了已编译类,然后介绍了相应的ASM接口、组件和生成和转换它们的工具,并提供了许多简单的示例。
已编译类的结构
概述
已编译类的整体结构非常简单并且保留了源代码中的结构信息和几乎所有符号,这些内容包括:
- 该类的描述修饰符、名称、超类、接口和注解信息。
- 在这个类中声明的每个字段信息,包括字段的修饰符、名称、类型和注解。
- 在这个类中声明的每个方法和构造函数,包括方法的修饰符、名称、返回值和参数类型以及注解。它还以Java字节码指令序列的形式包含该方法的编译代码。
但是,源代码和已编译类之间存在一些差异:
- 已编译类只描述一个类,而源文件可以包含多个类。
- 已编译类不包含注释,但可以包含可用于将附加信息关联到这些元素的类、字段、方法和代码属性。但自从在Java 5中引入注解后,属性变得几乎毫无用处。
- 已编译类不包含包和导入部分,因此所有类型名称都必须是完全限定的。
另一个非常重要的结构差异是,编译类包含一个常量池部分。这个池是一个数组,包含类中出现的所有数值、字符串和类型常量。这些常量只在常量池部分中定义一次,在类文件的所有其它部分中通过它们的索引引用。下表总结了已编译类的整体结构:
--------------------------------------
修饰符、名称、父类、实现的接口
--------------------------------------
常量池:数字常量、字符串常量和类型常量
--------------------------------------
源文件名称(可选)
--------------------------------------
外层类的引用(如果当前类是内部类)
--------------------------------------
注解
--------------------------------------
属性
--------------------------------------
内部类|名称
--------------------------------------
字段 |修饰符、名称和类型
--------------------------------------
|注解
--------------------------------------
|属性
--------------------------------------
方法 |修饰符、名称、返回值和参数类型
--------------------------------------
|注解
|属性
|编译后的代码
--------------------------------------
另一个重要的区别是Java类型在已编译类和源文件中的表示方式不同。下一节解释它们在已编译类中的表示。
内部名称
在许多情况下,类型被限制为类或接口类型。例如,类的超类、类实现的接口或方法抛出的异常不能是基本类型或数组类型,而必须是类或接口类型。这些类型使用内部名称表示。类的内部名称就是这个类的完全限定名,其中点被斜杠代替。例如,String
的内部名称为java/lang/String
。
类型描述符
内部名称仅用于在已编译类中表示Java类型中的类或接口类型。在所有其他情况下,Java类型是用类型描述符在已编译类中表示的:
源文件中的Java类型 | 类型描述符 |
---|---|
boolean | Z |
char | C |
byte | B |
short | S |
int | I |
float | F |
long | J |
double | D |
Object | Ljava/lang/Object; |
int[] | [I |
Object[][] | [[Ljava/lang/Object; |
基本类型的描述符是单个字符。类类型的描述符是这个类的内部名称,前面是L
,后面是分号。最后,数组类型的描述符是一个方括号,后跟数组元素类型的描述符。
方法描述符
方法描述符是一个类型描述符列表,它在单个字符串中描述方法的参数类型和返回值类型。方法描述符以左括号开头,后面是每个形式参数的类型描述符,然后是右括号,后面是返回值的类型描述符。
源文件中的方法声明 | 方法描述符 |
---|---|
void m(int i,float f) | (IF)V |
int m(Object o) | (Ljava/lang/Object;)I |
int[] m(int i,String s) | (ILjava/lang/String;)[I |
Object m(int[] i) | ([I)Ljava/lang/Object; |
接口和组件
概述
用于生成和转换已编译类的ASM API是基于ClassVisitor
抽象类的。这个类中的每个方法都对应于同名的类文件结构部分。对于已编译类结构中的各个部分:
- 简单部分:使用无返回值的
visitXxx()
方法进行访问 - 复杂部分:使用返回辅助
visitor
类的visitAnnotation
、visitField
和visitMethod
方法进行访问,它们分别返回一个AnnotationVisitor
、一个FieldVisitor
和一个MethodVisitor
。
public abstract class ClassVisitor {
public ClassVisitor(int api);
public ClassVisitor(int api, ClassVisitor cv);
public void visit(int version, int access, String name,String signature, String superName, String[] interfaces);
public void visitSource(String source, String debug);
public void visitOuterClass(String owner, String name, String desc);
AnnotationVisitor visitAnnotation(String desc, boolean visible);
public void visitAttribute(Attribute attr);
public void visitInnerClass(String name, String outerName,
String innerName, int access);
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);
void visitEnd();
}
这些辅助类递归地使用相同的原则,例如:
public abstract class FieldVisitor {
public FieldVisitor(int api);
public FieldVisitor(int api, FieldVisitor fv);
public AnnotationVisitor visitAnnotation(String desc, boolean visible);
public void visitAttribute(Attribute attr);
public void visitEnd();
}
ClassVisitor
类的方法必须按照下列顺序调用:
visit
visitSource?
visitOuterClass?
( visitAnnotation | visitAttribute )*
( visitInnerClass | visitField | visitMethod )*
visitEnd
这意味着必须先调用visit
,然后最多调用一次visitSource
,然后最多调用一次visitOuterClass
,紧随其后的是以任意顺序调用任意数量的visitAnnotation
和visitAttribute
,然后以任意顺序调用任意数量的visitInnerClass
、visitField
和visitMethod
,最后以单个调用visitEnd
结束。
ASM提供了三个基于ClassVisitor
的核心组件来生成和转换类:
ClassReader
类解析作为字节数组给出的已编译类,并调用作为参数传递给其accept
方法的ClassVisitor
实例上相应的visitXxx
方法。它可以被看作是一个事件制造者。ClassWriter
类是ClassVisitor
抽象类的子类,它直接以二进制形式构建已编译的类。它生成一个包含已编译类的字节数组作为输出,可以使用toByteArray
方法检索该字节数组。它可以被视为事件消费者。ClassVisitor
类将它接收到的所有方法调用委托给另一个ClassVisitor
实例。它可以看作是一个事件过滤器。
下一节将通过具体的示例展示如何使用这些组件来生成和转换类。
解析类
解析现有类所需的唯一组件是ClassReader
组件。让我们举一个例子来说明这一点。假设我们想以类似javap
工具的方式打印一个类的内容。第一步是编写ClassVisitor
类的一个子类,它打印所访问类的信息。以下是一个可能的、过度简化的实现:
public class ClassPrinter extends ClassVisitor {
public ClassPrinter() {
super(ASM4);
}
public void visit(int version, int access, String name,String signature, String superName, String[] interfaces) {
System.out.println(name + " extends " + superName + " {");
}
public void visitSource(String source, String debug) {
}
public void visitOuterClass(String owner, String name, String desc) {
}
public AnnotationVisitor visitAnnotation(String desc,boolean visible) {
return null;
}
public void visitAttribute(Attribute attr) {
}
public void visitInnerClass(String name, String outerName,String innerName, int access) {
}
public FieldVisitor visitField(int access, String name, String desc,String signature, Object value) {
System.out.println(" " + desc + " " + name);
return null;
}
public MethodVisitor visitMethod(int access, String name,String desc, String signature, String[] exceptions) {
System.out.println(" " + name + desc);
return null;
}
public void visitEnd() {
System.out.println("}");
}
}
第二步是将这个ClassPrinter
与ClassReader
组件结合起来,这样ClassReader
产生的事件就可以被我们的ClassPrinter
消费:
ClassPrinter cp = new ClassPrinter();
ClassReader cr = new ClassReader("java.lang.Runnable");
cr.accept(cp, 0);
第二行创建一个ClassReader
来解析Runnable
类。最后一行调用的accept
方法解析Runnable
类字节码,并调用cp
上相应的ClassVisitor
方法。结果输出如下:
java/lang/Runnable extends java/lang/Object {
run()V
}
请注意,有几种方法可以构造ClassReader
实例。必须读取的类可以按名称指定,如上所述,也可以按值指定,如字节数组或InputStream
。读取类内容的输入流可以使用ClassLoader
的getresourcesstream
方法获得:
cl.getResourceAsStream(classname.replace(’.’, ’/’) + ".class");
生成类
生成类所需的唯一组件是ClassWriter
组件。让我们举一个例子来说明这一点。考虑下面的接口:
package pkg;
public interface Comparable extends Mesurable {
int LESS = -1;
int EQUAL = 0;
int GREATER = 1;
int compareTo(Object o);
}
它可以通过对ClassVisitor
的六个方法调用来生成:
ClassWriter cw = new ClassWriter(0);
cw.visit(V1_5, ACC_PUBLIC + ACC_ABSTRACT + ACC_INTERFACE,"pkg/Comparable", null, "java/lang/Object",new String[] { "pkg/Mesurable" });
cw.visitField(ACC_PUBLIC + ACC_FINAL + ACC_STATIC, "LESS", "I",null, new Integer(-1)).visitEnd();
cw.visitField(ACC_PUBLIC + ACC_FINAL + ACC_STATIC, "EQUAL", "I",null, new Integer(0)).visitEnd();
cw.visitField(ACC_PUBLIC + ACC_FINAL + ACC_STATIC, "GREATER", "I",null, new Integer(1)).visitEnd();
cw.visitMethod(ACC_PUBLIC + ACC_ABSTRACT, "compareTo","(Ljava/lang/Object;)I", null, null).visitEnd();
cw.visitEnd();
byte[] b = cw.toByteArray();
对visit
方法的调用定义了类头。V1_5
参数是一个常量,和其他ASM常量一样,是在ASM Opcodes
接口中定义的。它指定了类的版本,Java 1.5。ACC_XXX
常数是对应于Java修饰符的标志。这里我们指定这个类是一个接口,而且它是公共的和抽象的。下一个参数指定了类名。下一个参数对应于泛型。在我们的例子中,它是null
,因为接口没有使用类型变量进行参数化。第五个参数是超类,内部形式。最后一个参数是要扩展的接口的数组。
接下来的三个对visitField
的调用类似,用于定义三个接口字段。第一个参数是一组标志,对应于Java修饰符。这里我们将字段指定为public
、final
和static
。第二个参数是字段的名称,就像源代码中显示的那样。第三个参数是字段的类型,采用类型描述符的形式。这里的字段是int
类型的字段,其描述符是I
。第四个参数对应于泛型。在我们的例子中,它是null
,因为字段类型没有使用泛型。最后一个参数是字段的常量值:这个参数只能用于真正的常量字段,即最终的静态字段。对于其他字段,它必须为null
。由于这里没有注解,我们立即调用返回的FieldVisitor
的visitEnd
方法,即不调用它的visitAnnotation
或visitAttribute
方法。
visitMethod
调用用于定义compareTo
方法。这里的第一个参数也是一组与Java修饰符对应的标志。第二个参数是方法名,如源代码中所示。第三个参数是方法的描述符。第四个参数对应泛型。最后一个参数是方法可以抛出的异常的数组。visitMethod
方法返回一个MethodVisitor
,它可以用来定义方法的注解和属性,最重要的是定义方法的代码。在这里,由于没有注解,并且由于该方法是抽象的,我们立即调用返回的MethodVisitor
的visitEnd
方法。
最后,调用visitEnd
来通知cw
类已经完成,并调用toByteArray
来将其作为字节数组输出。
使用生成的类
前面的字节数组可以存储在Comparable.class文件中以供将来使用。也可以使用ClassLoader
动态加载它。一种方法是定义一个ClassLoader
子类,其defineClass
方法为public
:
class MyClassLoader extends ClassLoader {
public Class defineClass(String name, byte[] b) {
return defineClass(name, b, 0, b.length);
}
}
然后生成的类可以直接用:
Class c = myClassLoader.defineClass("pkg.Comparable", b);
加载生成的类的另一种可能更简洁的方法是定义ClassLoader
子类,该子类的findClass
方法被重写,以便动态生成所请求的类:
class StubClassLoader extends ClassLoader {
@Override
protected Class findClass(String name)throws ClassNotFoundException {
if (name.endsWith("_Stub")) {
ClassWriter cw = new ClassWriter(0);
...
byte[] b = cw.toByteArray();
return defineClass(name, b, 0, b.length);
}
return super.findClass(name);
}
}
转换类
当这些组件一起使用时,事情开始变得非常有趣。首先是把ClassReader
产生的事件指向ClassWriter
。结果是ClassReader
解析的类会被ClassWriter
重构:
byte[] b1 = ...;
ClassWriter cw = new ClassWriter(0);
ClassReader cr = new ClassReader(b1);
cr.accept(cw, 0);
byte[] b2 = cw.toByteArray(); // b2 represents the same class as b1
下一步是在ClassReader
和ClassWriter
之间引入一个ClassVisitor
:
byte[] b1 = ...;
ClassWriter cw = new ClassWriter(0);
// cv forwards all events to cw
ClassVisitor cv = new ClassVisitor(ASM4, cw) { };
ClassReader cr = new ClassReader(b1);
cr.accept(cv, 0);
byte[] b2 = cw.toByteArray(); // b2 represents the same class as b1
然而,结果并没有改变,因为ClassVisitor
没有过滤任何东西。通过重写ClassVisitor
中的一些方法,便能够改变一个类:
public class ChangeVersionAdapter extends ClassVisitor {
public ChangeVersionAdapter(ClassVisitor cv) {
super(ASM4, cv);
}
@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);
}
}
通过修改访问方法的其他参数,您可以实现其他转换,而不仅仅是更改类版本。
优化
前述变换仅在原始类中更改了四个字节。然而,使用上述代码,b1
被完全解析,并且相应的事件被用来从头开始构建 b2
,这并不是非常高效的。更有效的方法是,将 b1
中未直接转换的部分复制到 b2
中,而无需解析这些部分或生成相应的事件。ASM 自动为方法执行此优化:
- 如果
ClassReader
组件检测到由传递给其accept
方法的ClassVisitor
返回的MethodVisitor
来自于ClassWriter
,这意味着此方法的内容不会被转换。 - 在这种情况下,
ClassReader
组件不会解析此方法的内容,也不会生成相应的事件,只是将该方法的字节数组表示复制到ClassWriter
中。
此优化是由 ClassReader
和 ClassWriter
组件执行的,如果它们相互引用,则可以像这样设置:
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();
这种优化使得上述代码比之前的快了两倍,因为 ChangeVersionAdapter
不对任何方法进行转换。对于通常的类转换,即转换一些或全部方法的情况,速度提升较小,但仍然明显:通常在 10% 到 20% 的范围内。不过,这种优化需要将原始类中定义的所有常量复制到转换后的类中。对于添加字段、方法或指令的转换,这并不是问题,但对于删除或重命名许多类元素的转换,会导致类文件更大,与未优化的情况相比。因此,建议仅对增量转换使用此优化。
使用转换后的类
转换后的类 b2
可以存储在磁盘上或使用 ClassLoader
加载,就像前面一节中描述的那样。但是,在 ClassLoader
内部进行的类转换只能转换由该类加载器加载的类。如果要转换所有类,则必须将转换放在java.lang.instrument
包中定义的 ClassFileTransformer
内:
public static void premain(String agentArgs, Instrumentation inst) {
inst.addTransformer(new ClassFileTransformer() {
public byte[] transform(ClassLoader l, String name, Class c,ProtectionDomain d, byte[] b)throws IllegalClassFormatException {
ClassReader cr = new ClassReader(b);
ClassWriter cw = new ClassWriter(cr, 0);
ClassVisitor cv = new ChangeVersionAdapter(cw);
cr.accept(cv, 0);
return cw.toByteArray();
}
});
}
删除类成员
前面一节中用于转换类版本的方法当然可以应用于 ClassVisitor
类的其他方法。例如,通过更改 visitField
和 visitMethod
方法中的访问或名称参数,您可以更改字段或方法的修饰符或名称。此外,您还可以选择不转发具有修改参数的方法调用。其效果是删除相应的类元素。
例如,以下类适配器删除了关于外部和内部类的信息,以及编译类的源文件的名称。这是通过在适当的 visit
方法中不转发任何内容来完成的:
public class RemoveDebugAdapter extends ClassVisitor {
public RemoveDebugAdapter(ClassVisitor cv) {
super(ASM4, cv);
}
@Override
public void visitSource(String source, String debug) {
}
@Override
public void visitOuterClass(String owner, String name, String desc) {
}
@Override
public void visitInnerClass(String name, String outerName,String innerName, int access) {
}
}
这种策略对字段和方法不起作用,因为 visitField
和 visitMethod
方法必须返回结果。要删除字段或方法,您必须不转发方法调用,并向调用者返回 null
。
例如,以下类适配器通过其名称和描述符(仅名称不足以标识方法,因为类可以包含具有相同名称但具有不同参数的多个方法)指定删除一个方法:
public class RemoveMethodAdapter extends ClassVisitor {
private String mName;
private String mDesc;
public RemoveMethodAdapter(ClassVisitor cv, String mName, String mDesc) {
super(ASM4, cv);
this.mName = mName;
this.mDesc = mDesc;
}
@Override
public MethodVisitor visitMethod(int access, String name,String desc, String signature,String[] exceptions) {
if (name.equals(mName) && desc.equals(mDesc)) {
// do not delegate to next visitor -> this removes the method
return null;
}
return cv.visitMethod(access, name, desc, signature, exceptions);
}
}
增加类成员
与其只转发少量调用,不如多转发一些调用,这样可以添加更多的类元素。新的调用可以插入到原始方法调用之间的多个位置,但必须遵循调用 visitXxx
方法的调用顺序。
举个例子,如果你想给一个类添加一个字段,你需要在原始方法调用之间插入一个新的 visitField
调用。你必须把这个新的调用放在你的类适配器的 visit
方法中。比如,你不能把这个调用放在 visit
方法中,因为这样可能导致先调用 visitField
,然后调用 visitSource
、visitOuterClass
、visitAnnotation
或 visitAttribute
,这是不允许的。同样地,你也不能把这个调用放在 visitSource
、visitOuterClass
、visitAnnotation
或 visitAttribute
方法中。唯一能放的地方是 visitInnerClass
、visitField
、visitMethod
或 visitEnd
方法中。
如果你把这个新的调用放在 visitEnd
方法中,那么这个字段将总是被添加(除非你添加了显式的条件),因为这个方法总会被调用。如果你把它放在 visitField
或 visitMethod
中,将会添加多个字段:每个原始类中的字段或方法一个。这两种方案都有意义;取决于你的需求。例如,你可以添加一个单独的计数器字段来统计对象的调用次数,或者为每个方法分别添加一个计数器来统计每个方法的调用次数。
为了说明上面的讨论,这里是一个类适配器,它向一个类中添加一个字段,除非这个字段已经存在:
public class AddFieldAdapter extends ClassVisitor {
private int fAcc;
private String fName;
private String fDesc;
private boolean isFieldPresent;
public AddFieldAdapter(ClassVisitor cv, int fAcc, String fName,String fDesc) {
super(ASM4, cv);
this.fAcc = fAcc;
this.fName = fName;
this.fDesc = fDesc;
}
@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();
}
}
该字段是在 visitEnd
方法中添加的。visitField
方法没有被覆盖来修改现有字段或移除字段,而是仅用于检测我们要添加的字段是否已经存在。请注意,在调用 fv.visitEnd()
之前,在 visitEnd
方法中进行了 fv != null
的测试:这是因为,正如我们在前一节中看到的,类访问者在 visitField
中可以返回 null
。
转换链
到目前为止,我们已经看到了由 ClassReader
、类适配器和 ClassWriter
组成的简单转换链。当然,可以使用更复杂的链,将多个类适配器链接在一起。将多个适配器链接在一起可以组合多个独立的类转换,以进行复杂的转换。还要注意,转换链不一定是线性的。您可以编写一个 ClassVisitor
,将它收到的所有方法调用同时转发给多个 ClassVisitor
:
public class MultiClassAdapter extends ClassVisitor {
protected ClassVisitor[] cvs;
public MultiClassAdapter(ClassVisitor[] cvs) {
super(ASM4);
this.cvs = cvs;
}
@Override public void visit(int version, int access, String name,String signature, String superName, String[] interfaces) {
for (ClassVisitor cv : cvs) {
cv.visit(version, access, name, signature, superName, interfaces);
}
}
...
}
对称地,多个类适配器可以委托给同一个 ClassVisitor
(这需要一些预防措施,以确保例如 visit
和 visitEnd
方法在此 ClassVisitor
上确切地被调用一次)。
工具类
除了 ClassVisitor
类和相关的 ClassReader
和 ClassWriter
组件外,ASM 还提供了 org.objectweb.asm.util
包中的几个工具,在开发类生成器或适配器时可能会有用,但在运行时不需要。ASM 还提供了一个用于在运行时操作内部名称、类型描述符和方法描述符的实用程序类。下面将介绍所有这些工具。
Type
在前面的部分中,您已经看到了 ASM API 将 Java 类型暴露为它们存储在已编译类中的形式,即作为内部名称或类型描述符。将它们暴露为它们在源代码中出现的形式是可能的,以使代码更易读。但这将需要在 ClassReader
和 ClassWriter
中两种表示之间进行系统转换,这将降低性能。这就是为什么 ASM 不会将内部名称和类型描述符透明地转换为它们等效的源代码形式。但是,在必要时,ASM 提供了 Type
类来手动执行这种转换。
Type
对象表示一个 Java 类型,可以从类型描述符或 Class
对象构造。Type
类还包含表示基本类型的静态变量。例如,Type.INT_TYPE
是表示 int
类型的 Type
对象。
getInternalName
方法返回一个 Type
的内部名称。例如,Type.getType(String.class).getInternalName()
返回 String
类的内部名称,即 java/lang/String
。此方法仅用于类或接口类型。
getDescriptor
方法返回一个 Type
的描述符。例如,您可以在代码中使用 Type.getType(String.class).getDescriptor()
替代 Ljava/lang/String;
。或者,您可以使用 Type.INT_TYPE.getDescriptor()
替代 I
。
Type
对象还可以表示方法类型。这种对象可以从方法描述符或 Method
对象构造。然后,getDescriptor
方法返回与此类型对应的方法描述符。此外,getArgumentTypes
和 getReturnType
方法可用于获取方法的参数类型和返回类型的 Type
对象。例如,Type.getArgumentTypes("(I)V")
返回一个包含单个元素 Type.INT_TYPE
的数组。类似地,调用 Type.getReturnType("(I)V")
返回 Type.VOID_TYPE
对象。
TraceClassVisitor
为了检查生成或转换的类是否符合您的预期,由 ClassWriter
返回的字节数组对于人类来说并不是很有用,因为它是不可读的。文本表示法将更容易使用。这就是 TraceClassVisitor
类的作用。这个类,正如其名称所示,扩展了 ClassVisitor
类,并构建了所访问的类的文本表示形式。因此,您可以使用 TraceClassVisitor
来生成类,以便获得实际生成内容的可读跟踪。或者,更好的是,您可以同时使用两者。实际上,TraceClassVisitor
可以除了其默认行为外,还将所有调用其方法的调用委派给另一个访问者,例如一个 ClassWriter
:
ClassWriter cw = new ClassWriter(0);
TraceClassVisitor cv = new TraceClassVisitor(cw, printWriter);
cv.visit(...);
...
cv.visitEnd();
byte b[] = cw.toByteArray();
这段代码创建了一个 TraceClassVisitor
,它将收到的所有调用委托给 cw
,并将这些调用的文本表示打印到 printWriter
。
请注意,您可以在生成或转换链的任何点上使用 TraceClassVisitor
,而不仅仅是在 ClassWriter
前面,以便查看链中的这一点发生了什么。还要注意,此适配器生成的类的文本表示形式可以使用 String.equals()
方便地进行比较。
CheckClassAdapter
ClassWriter
类不检查其方法是否按照适当的顺序和使用有效的参数进行调用。因此,有可能生成无效的类,这些类将被 Java 虚拟机验证器拒绝。
为了尽快检测到其中一些错误,可以使用CheckClassAdapter
类。与 TraceClassVisitor
类似,此类扩展了 ClassVisitor
类,并将其所有方法的调用委派给另一个 ClassVisitor
,例如 TraceClassVisitor
或 ClassWriter
。然而,与其打印所访问类的文本表示形式不同,此类在委派给下一个访问者之前会检查其方法是否按照适当的顺序和使用有效的参数进行调用。如果发生错误,将抛出 IllegalStateException
或 IllegalArgumentException
。
为了检查一个类,打印该类的文本表示形式,并最终创建一个字节数组表示形式,您可以使用类似以下的方式:
ClassWriter cw = new ClassWriter(0);
TraceClassVisitor tcv = new TraceClassVisitor(cw, printWriter);
CheckClassAdapter cv = new CheckClassAdapter(tcv);
cv.visit(...);
...
cv.visitEnd();
byte b[] = cw.toByteArray();
请注意,如果您以不同的顺序链接这些类访问者,它们执行的操作也会以不同的顺序进行。例如,使用以下代码,检查将在跟踪之后进行:
ClassWriter cw = new ClassWriter(0);
CheckClassAdapter cca = new CheckClassAdapter(cw);
TraceClassVisitor cv = new TraceClassVisitor(cca, printWriter);
与 TraceClassVisitor
类似,您可以在生成或转换链的任何点上使用 CheckClassAdapter
,而不仅仅是在ClassWriter
前面,以便在链中的此点检查类。
ASMifier
这个类提供了 TraceClassVisitor
工具的另一种后端(默认情况下使用 Textifier
后端,产生上面显示的输出类型)。这个后端使得 TraceClassVisitor
类的每个方法打印出调用它的 Java 代码。例如,调用 visitEnd()
方法会打印出 cv.visitEnd()
;。结果是,当一个带有 ASMifier
后端的 TraceClassVisitor
访问器访问一个类时,它会打印出使用 ASM 生成此类的源代码。如果您使用此访问器访问一个已经存在的类,这将非常有用。例如,如果您不知道如何使用 ASM 生成某个已编译类,请编写相应的源代码,使用 javac 编译它,并使用 ASMifier
访问已编译的类。您将获得生成此已编译类的 ASM 代码!
ASMifier
类可以在命令行中使用:
java -classpath asm.jar:asm-util.jar \
org.objectweb.asm.util.ASMifier \
java.lang.Runnable
生成缩进后的代码如下:
package asm.java.lang;
import org.objectweb.asm.*;
public class RunnableDump implements Opcodes {
public static byte[] dump() throws Exception {
ClassWriter cw = new ClassWriter(0);
FieldVisitor fv;
MethodVisitor mv;
AnnotationVisitor av0;
cw.visit(V1_5, ACC_PUBLIC + ACC_ABSTRACT + ACC_INTERFACE,"java/lang/Runnable", null, "java/lang/Object", null);
{
mv = cw.visitMethod(ACC_PUBLIC + ACC_ABSTRACT, "run", "()V",
null, null);
mv.visitEnd();
}
cw.visitEnd();
return cw.toByteArray();
}
}