综合编程

ASM中文手册-Part I. Core API-Classes篇

微信扫一扫,分享到朋友圈

ASM中文手册-Part I. Core API-Classes篇
0

本章解释如何生成和转换编译后的Java类使用核心ASM API。它从编译类的表示开始然后给出了相应的ASM 接口、组件和工具生成并转换它们,并提供许多示例。的内容方法、注释和泛型将在下一章中解释。

2.1. Structure

2.1.1. Overview

编译类的整体结构非常简单。实际上,与本机编译的应用程序不同,编译后的类保留了源代码中的结构信息和几乎所有符号。事实上,编译后的类包含:

  • 描述修饰符(如public或private)、名称、超类、接口和类的注释的部分。
  • 该类中声明的每个字段有一个节。每个部分描述字段的修饰符、名称、类型和注释。
  • 该类中声明的每个方法和构造函数有一个节。每个部分描述一个方法的修饰符、名称、返回值和参数类型以及注释。它还以Java字节码指令序列的形式包含方法的编译代码。

但是,源类和编译类之间有一些区别:

  • 编译后的类只描述一个类,而源文件可以包含多个类。例如,描述具有一个内部类的源文件被编译为两个类文件:一个用于主类,另一个用于内部类。然而,主类文件包含对其内部类的引用,而在方法中定义的内部类包含对其封闭方法的引用。
  • 当然,编译后的类不包含注释,但是可以包含类、字段、方法和代码属性,这些属性可用于将附加信息关联到这些元素。自从在Java 5中引入注释(可以用于相同的目的)以来,属性已经变得几乎毫无用处。
  • 编译后的类不包含包和导入部分,因此所有类型名必须完全限定。

另一个非常重要的结构差异是,编译后的类包含一个常量池部分。这个池是一个数组,包含类中出现的所有数值、字符串和类型常量。这些常量只在常量池部分定义一次,并由它们在类文件的所有其他部分中的索引引用。希望ASM隐藏了与常量池相关的所有细节,这样您就不用担心它了。图2.1总结了编译类的总体结构。确切的结构在Java虚拟机规范第4节中进行了描述。

图2.1。:编译类的整体结构(*表示0或更多)

另一个重要的区别是Java类型在编译类和源类中表示不同。下一节将解释它们在编译类中的表示。

2.1.2. Internal names

在许多情况下,类型被限制为类或接口类型。例如,类的超类、类实现的接口或方法抛出的异常不能是基本类型或数组类型,而必须是类或接口类型。这些类型在具有内部名称的已编译类中表示。类的内部名称只是该类的完全限定名,其中点被斜线替换。例如,字符串的内部名称是java/lang/String。

2.1.3. Type descriptors

内部名称仅用于约束为类或接口类型的类型。在所有其他情况下,例如字段类型,Java类型都用带有类型描述符的编译类表示(参见图2.2)。

图2.2。:某些Java类型的类型描述符

基本类型的描述符是单字符:Z表示布尔值,C表示char, B表示字节,S表示短字符,I表示int, F表示浮点数,J表示长字符,D表示双字符。类类型的描述符是该类的内部名称,前面是L,后面是分号。例如,String的类型描述符是Ljava/lang/String;。最后,数组类型的描述符是方括号,后面跟着数组元素类型的描述符。

2.1.4. Method descriptors

方法描述符是一个类型描述符列表,它在一个字符串中描述方法的参数类型和返回类型。方法描述符与左括号开始,其次是每个形式参数的类型描述符,紧随其后的是一个右括号,紧随其后的类型描述符返回类型,或者V如果方法返回void方法描述符(不包含方法的名称或参数名称)。

图2.3。:示例方法描述符

一旦了解了类型描述符的工作方式,理解方法描述符就很容易了。例如(I)I,我描述了一个方法,它接受一个int类型的参数,并返回一个int。

2.2. Interfaces and components

2.2.1. Presentation

用于生成和转换已编译类的ASM API基于ClassVisitor抽象类(参见图2.4)。该类中的每个方法都对应于同名的类文件结构部分(参见图2.1)。通过一个方法调用访问其简单的一部分,该方法调用的参数描述了其内容 ,并返回void。内容可以是使用返回辅助访问者类的初始方法调用访问任意长度和复杂度的对象。这是visitAnnotation、visitField和visitMethod方法的情况,它们分别返回一个AnnotationVisitor、一个FieldVisitor和一个MethodVisitor。

对于这些辅助类,递归地使用相同的原则。例如,FieldVisitor抽象类中的每个方法(参见图2.5)对应于同名的类文件子结构和visitAnnotation

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();
    
}

图2.4。: ClassVisitor class

返回辅助的AnnotationVisitor,如ClassVisitor中所示。这些辅助访问者的创建和使用将在下一章中进行说明:实际上,本章仅限于可以单独使用ClassVisitor类解决的简单问题。

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();
    
}

图2.5。: FieldVisitor class

ClassVisitor类的方法必须按照以下顺序调用,在该类的Javadoc中指定:

visit visitSource? visitOuterClass? ( visitAnnotation | visitAttribute )*
( visitInnerClass | visitField | visitMethod )*
visitEnd

这意味着 visit 必须被称为第一,然后是调用 visitSource ,接着是调用 visitOuterClass ,接着是以任意顺序调用 visitAnnotationvisitAttribute ,然后是任意顺序的调用 visitInnerClassvisitField 和  visitMethod ,最后调用 visitEnd

ASM提供了三个基于ClassVisitor API的核心组件来生成和转换类:

  • ClassReader类解析作为字节数组给出的已编译类,并对作为参数传递给其accept方法的ClassVisitor实例调用相应的visitXxx方法。它可以被看作是一个事件生成器。
  • ClassWriter类是ClassVisitor抽象类的子类,该类直接以二进制形式构建编译后的类。它生成一个包含已编译类的字节数组作为输出,可以使用toByteArray方法检索该类。它可以被视为事件消费者。
  • ClassVisitor类将它接收到的所有方法调用委托给另一个ClassVisitor实例。它可以看作是一个事件过滤器。

    下一节将通过具体的示例展示如何使用这些组件生成和转换类。

2.2.2. Parsing classes

解析现有类所需的唯一组件是ClassReader。让我们举个例子来说明这一点。假设我们想以与javap工具类似的方式打印类的内容。第一步是编写ClassVisitor类的子类,该类打印它访问的类的信息。这里有一个简化的实现:

import org.objectweb.asm.*;

public class ClassPrinter extends ClassVisitor {
    
    public ClassPrinter() {
        super(Opcodes.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("}");
    }
}

第二步是将这个类sprinter与一个ClassReader组件组合起来,以便ClassReader生成的事件被我们的ClassPrinter调用:

ClassPrinter cp = new ClassPrinter();
ClassReader cr = new ClassReader("java.lang.Runnable");
cr.accept(cp, 0);

第二行创建一个ClassReader来解析Runnable类。最后一行调用的accept方法解析可运行类字节码,并用ClassPrinter调用相应的ClassVisitor方法,结果如下:

java/lang/Runnable extends java/lang/Object {
	run()V
}

注意,有几种方法可以构造ClassReader实例。可以通过类的名称、字节数组或InputStream来。获取InputStream可以使用ClassLoader的getResourceAsStream方法获得,方法是:

cl.getResourceAsStream(classname.replace(’.’, ’/’) + ".class");

2.2.3. Generating classes

生成类所需的惟一组件是ClassWriter。让我们举个例子来说明这一点。请看以下接口:

package pkg;
public interface Comparable extends Mesurable {
    int LESS = -1;
    int EQUAL = 0;
    int GREATER = 1;
    int compareTo(Object o);
}

它可以通过ClassVisitor的6个方法调用生成:

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();

第一行创建一个ClassWriter实例,它将实际构建类的字节数组表示(构造函数参数将在下一章中解释)。

对visit方法的调用定义了类头(class header)。与ASM操作码接口中定义的所有其他ASM常量一样,V1_5参数是一个常量。它指定了类版本,Java 1.5。ACC_XXX常量是与Java修饰符对应的标志。这里我们指定类是一个接口,并且它是公共的和抽象的(因为它不能被实例化)。下一个参数以内部形式指定类名(参见2.1.2节)。请记住,编译后的类不包含包或导入部分,因此所有类名都必须是完全限定的。下一个参数对应于泛型(参见4.1节)。在我们的例子中,它是null,因为接口不是由类型变量参数化的。第五个参数是内部形式的超类(接口类隐式地继承自Object)。最后一个参数是扩展的接口数组,由它们的内部名称指定。

接下来对visitField方法的三次调用是类似的,用于定义三个接口字段。第一个参数是一组与Java修饰符对应的标志。这里我们指定字段为public、final和static。第二个参数是字段的名称,因为它出现在源代码中。第三个参数是字段的类型,类型描述符形式。这里的字段是int字段,它的描述符是I。第四个参数对应泛型。在我们的例子中,它是null,因为字段类型没有使用泛型。最后一个参数是字段的常量值: 该参数只能用于真正的常亮值字段,即final static字段。对于其他字段,它必须为空。由于这里没有注解,我们立即调用返回的FieldVisitor的visitEnd方法,即不调用它的visitAnnotation或visitAttribute方法。

visitMethod调用用于定义compareTo方法。同样,这里的第一个参数是一组与Java修饰符对应的标志。第二个参数是方法名,因为它出现在源代码中。第三个参数是方法的描述符。第四个参数对应泛型。在我们的例子中,它是null,因为该方法没有使用泛型。最后一个参数是方法可以抛出的异常数组,由它们的内部名称指定。这里它是null,因为方法没有声明任何异常。visitMethod方法返回一个MethodVisitor(参见图3.4),它可以用来定义方法的注释和属性,以及最重要的方法代码。这里,由于没有注释,而且方法是抽象的,所以我们立即调用返回MethodVisitor的visitEnd方法。

在最后调用cw对象的visitEnd方法通知类已经创建结束,并使用toByteArray转为字节数组。

Using generated classes

前面的字节数组可以存储在Comparable.class文件中,以备将来使用。也可以使用类加载器动态加载它。一种方法是定义一个类加载器子类,它的defineClass方法是公共的

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);

加载生成类的另一种方法(可能更干净)是定义一个类加载器子类,它的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);
	}
}

实际上,使用生成的类的方法取决于上下文,它超出了ASM API的范围。如果您正在编写编译器,则该类的生成过程将由一个抽象语法树驱动,该语法树表示程序进行编译,生成的类将存储在磁盘上。如果您正在编写动态代理类生成器或切面编织器,您将以某种方式使用ClassLoader。

2.2.4. Transforming classes

到目前为止,ClassReader和ClassWriter组件都是单独使用的。这些事件是“手工”生成的,并直接由ClassWriter使用,或者,同步地,它们是由ClassReader生成并“手工”使用的,即由自定义ClassVisitor实现使用。当这些组件一起使用时,事情开始变得非常有趣。第一步是将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

这本身并不有趣(有更简单的方法复制字节数组!),但是请稍候。下一步是在类阅读器和类编写器之间引入一个类访问者:

通过修改visit方法的其他参数,您可以实现其他转换,而不仅仅是修改类版本。例如,可以将接口添加到已实现接口列表中。也可以更改类的名称,但这不仅需要更改visit方法中的name参数。实际上,类的名称可以出现在已编译类的许多不同位置,必须更改所有这些发生的情况才能真正重命名类。byte[] b1 = ...;
ClassWriter cw = new ClassWriter(0);
// cv forwards all events to cw
ClassVisitor cv = new ClassVisitor(ASM4, cw) {
    // 何白白:这里可以搞事情啊~,改下一类加载器,重构一下class啥的
    // O(∩_∩)O哈哈~
};
ClassReader cr = new ClassReader(b1);
cr.accept(cv, 0);
byte[] b2 = cw.toByteArray(); // b2 represents the same class as b1

图2.6中描述了与上述代码对应的体系结构,其中组件用正方形表示,事件用箭头表示(用一条垂直的时间线表示,如序列图中所示)。

图2.6。:转换链

但是,结果不会改变,因为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);
    }
}

该类只重写ClassVisitor类的一个方法。因此,所有调用都会原封不动地转发给类访问者对象cv通过构造函数,但对visit方法的调用除外,visit方法修改了类的的版本号(V1_5)。显示了相应的顺序图2.7中。

图2.7。: ChangeVersionAdapter的序列图

通过修改visit方法的其他参数,您可以实现其他转换,而不仅仅是修改类版本。例如,可以将接口添加到已实现接口列表中。也可以更改类的名称,但这不仅需要更改visit方法中的name参数。实际上,类的名称可以出现在已编译类的许多不同位置,必须更改所有这些发生的情况才能真正重命名类。

Optimization

前面的转换只改变了原始类中的四个字节。但是,使用上面的代码,b1将被完全解析,并使用相应的事件从头构建b2,这不是很有效。如果复制b1中没有直接转换成b2的部分,而不解析这些部分,也不生成相应的事件,那么效率会高得多。ASM自动执行方法的优化:

  • 如果一个ClassReader组件检测到了一个由作为参数传递给它的accept方法的ClassVisitor来自一个ClassWriter(原文:If a ClassReader component detects that a MethodVisitor returned by the ClassVisitor passed as argument to its accept method comes from a 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%之间。不幸的是,这种优化需要将原类中定义的所有常量复制到转换后的类中。对于添加字段、方法或指令的转换,这不是问题,但是对于删除或重命名许多类元素的转换,与未经过优化的情况相比,这会导致更大的类文件。因此,只要求将此优化用于“加法”转换。

Using transformed classes

转换后的类b2可以存储在磁盘上,也可以用类加载器加载,如上一节所述。但是在一个类加载器内完成转换的类,只能由该类加载器加载。如果您想转换所有类,就必须将转换放在ClassFileTransformer中,如java.lang.instrument包中定义的那样。(详见本包文件):

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();
        }
    });
}

2.2.5. Removing class members

上一节中用于转换类版本的方法当然可以应用于ClassVisitor类的其他方法。例如,通过更改visitField和visitMethod方法中的access或name参数,您可以更改修饰符或字段或方法的名称。此外,您可以选择完全不转发此调用,而不是转发带有修改参数的方法调用。其效果是删除了相应的类元素。

例如,下面的类适配器删除了关于外部类和内部类的信息,以及编译类的源文件的名称(生成的类仍然是完整的功能类,因为这些元素仅用于调试目的)。这是通过不转发任何东西在适当的访问方法:

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);
    }
}

2.2.6. Adding class members

您可以“转发”更多的调用,而不是转发比接收到的更少的调用,这具有添加类元素的效果。只要遵守调用各种visitXxx方法的顺序(请参阅2.2.1节),新调用可以插入原始方法调用之间的多个位置。

例如,如果要向类添加字段,必须在原始方法调用之间插入对visitField的新调用,并且必须将此新调用放入类适配器的visit方法之一。例如,您不能在visit方法中执行此操作,因为这可能会导致对visitField的调用,然后是visitSource、visitOuterClass、visitAnnotation或visitAttribute,这是无效的。出于同样的原因,您不能将这个新调用放入visitSource、visitOuterClass、visitAnnotation或visitAttribute方法中。唯一的可能性是visitInnerClass、visitField、visitMethod或visitEnd方法。

如果将新调用放入visitEnd方法中,字段将始终被添加(除非添加显式条件),因为该方法始终被调用。如果将其放入visitField或visitMethod中,将添加几个字段:每个字段或方法在原始类中添加一个字段。两种解决方案都有意义;这取决于你需要什么。例如,您可以添加一个计数器字段来计算对象上的调用,或者为每个方法添加一个计数器来分别计算每个方法的调用。

Note:

实际上,唯一真正正确的解决方案是通过在visitEnd方法中进行额外调用来添加新成员。确实,一个类必须不包含重复的成员,并且确保新成员是惟一的唯一方法是将它与所有现有成员进行比较,这只能在所有成员都被访问之后才能完成,即在visitEnd方法中。这是相当有限的。使用不太可能被程序员使用的生成的名称,如_counter$或_4B7F_,在实践中就足以避免重复的成员,而不必在visitEnd中添加它们。注意,正如第一章中所讨论的,tree API没有这个限制:可以在转换中随时使用这个API添加新成员。

为了说明上面的讨论,这里有一个类适配器,它将一个字段添加到一个类中,除非这个字段已经存在:

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方法不是为了修改现有字段或删除字段,而是为了检测要添加的字段是否已经存在。注意visitEnd方法中的fv != null测试,在调用fv.visitEnd()之前:这是因为,正如我们在前一节中看到的,类访问者可以在visitField中返回null。

2.2.7. Transformation chains

到目前为止,我们已经看到了由类阅读器、类适配器和类编写器组成的简单转换链。当然也可以使用更复杂的链,将几个类适配器链接在一起。链接几个适配器允许您组合几个独立的类转换,以便执行复杂的转换。还要注意,转换链不一定是线性的。你可以编写一个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(这需要一些预防措施来,例如,在这个ClassVisitor上只调用一次visit和visitEnd方法)。因此,如图2.8所示的转换链是完全可能的。

2.3. Tools

org.objectweb.asm.util包中在提供了ClassVisitor类和相关的ClassReader和ClassWriter组件之外,ASM还在包中提供了一些工具,这些工具在类生成器或适配器的开发过程中非常有用,但是在运行时并不需要。ASM还提供了一个实用程序类,用于在运行时操作内部名称、类型描述符和方法描述符。下面介绍了所有这些工具。

图2.8。:复杂转换链

2.3.1. Type

正如您在前面几节中看到的,ASM API公开了存储在编译类中的Java类型,即内部名称或类型描述符。可以在源代码中显示它们,使代码更具可读性。但是这需要在ClassReader和ClassWriter中的两种表示之间进行系统的转换,这会降低性能。这就是ASM不透明地将内部名称和类型描述符转换为其等效源代码形式的原因。但是,它提供了Type类,以便在必要时手动执行此操作。

类型对象表示Java类型,可以从类型描述符或类对象构造。Type类还包含表示基本类型的静态变量。例如类型。INT_TYPE是表示int类型的类型对象。

getInternalName方法返回类型的内部名称。例如Type.getType(String.class).getinternalname()给出了字符串类的内部名称,即“java/lang/String”。此方法只能用于类或接口类型。

getDescriptor方法返回类型的描述符。举个例子要在代码中使用“Ljava/lang/String;”,可以使用Type.getType(String.class). getdescriptor()。或者,您可以使用Type.INT_TYPE.getDescriptor()代替I。

类型对象也可以表示方法类型。可以从方法描述符或方法对象构造这样的对象。然后getDescriptor方法返回与此类型对应的方法描述符。此外,getArgumentTypes和getReturnType方法可用于获取与方法的参数类型和返回类型对应的类型对象。例如Type.getArgumentTypes(“(I)V”)返回一个包含单个元素Type.INT_TYPE的数组。类似地,对Type. getreturntype(“(I)V”)的调用返回该类型。VOID_TYPE对象。

2.3.2. TraceClassVisitor

为了检查生成或转换的类是否符合您的期望,ClassWriter返回的字节数组实际上没有什么帮助,因为它是人类无法读取的。文本表示将更容易使用。这就是TraceClassVisitor类所提供的。顾名思义,该类扩展了ClassVisitor类,并构建了已访问类的文本表示。因此,您可以使用TraceClassVisitor来获得实际生成的可读跟踪,而不是使用ClassWriter来生成类。或者,更好的是,您可以同时使用这两种方法。实际上TraceClassVisitor除了默认行为外,还可以将对其方法的所有调用委托给另一个访问者,例如ClassWriter:

ClassWriter cw = new ClassWriter(0);
TraceClassVisitor cv = new TraceClassVisitor(cw, printWriter);
cv.visit(...);
...
cv.visitEnd();
byte b[] = cw.toByteArray();

//何白白:这段代码并不能直接用,你可以向下面这样,他能在控制台打印出String.class的信息
ClassWriter cw = new ClassWriter(0);
PrintWriter printWriter = new PrintWriter(System.out);
ClassReader classReader = new ClassReader(String.class.getName());
TraceClassVisitor cv = new TraceClassVisitor(cw, printWriter);
classReader.accept(cv, Opcodes.ASM5);
cv.visitEnd();
byte b[] = cw.toByteArray();

这段代码创建一个TraceClassVisitor,它将接收到的所有调用委托给cw,并将这些调用的文本表示形式打印给printWriter。例如,在第2.2.3节的示例中使用TraceClassVisitor将会给出:

// class version 49.0 (49)
// access flags 1537
public abstract interface pkg/Comparable implements pkg/Mesurable {
// access flags 25
public final static I LESS = -1
// access flags 25
public final static I EQUAL = 0
// access flags 25
public final static I GREATER = 1
// access flags 1025
public abstract compareTo(Ljava/lang/Object;)I
}

注意,您可以在生成或转换链的任何位置使用TraceClassVisitor,而不仅仅是在ClassWriter之前,以便查看在链的这一点上发生了什么。还要注意,这个适配器生成的类的文本表示形式可以使用String.equals()轻松地比较类。

2.3.3. 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之前,以便在链中的这个点上检查类。

2.3.4. ASMifier

该类为TraceClassVisitor工具提供了一个备选后端(默认情况下,TraceClassVisitor工具使用一个Textifier后端,生成如上所示的输出)。这个后端使TraceClassVisitor类的每个方法打印用于调用它的Java代码。例如,调用visitEnd()方法将输出vc .visitEnd();。结果是,当带有ASMifier后端访问类的TraceClassVisitor访问一个类时,它会打印源代码来用ASM生成这个类。如果您使用这个访问者访问一个已经存在的类,这将非常有用。例如,如果您不知道如何使用ASM生成编译后的类,那么编写相应的源代码,使用javac编译它,然后使用ASMifier访问编译后的类。您将获得ASM代码来生成这个编译后的类!

ASMifier类可以从命令行中使用。例如使用:

java -classpath asm.jar:asm-util.jar 
    org.objectweb.asm.util.ASMifier 
    java.lang.Runnable
//何白白:上面的是手册上的原文,但是无法使用,因为找不到ASMifier。
//可以使用下面的命令
java jdk.internal.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();
    }
}

阅读原文...


微信扫一扫,分享到朋友圈

ASM中文手册-Part I. Core API-Classes篇
0

博客园-原创精华区

Vue组件库工程探索与实践之按需加载

上一篇

Axure教程:如何制作有趣的小红书促销小游戏?

下一篇

评论已经被关闭。

插入图片

热门分类

往期推荐

ASM中文手册-Part I. Core API-Classes篇

长按储存图像,分享给朋友