本章说明如何使用核心 ASM API 来生成和转换经过编译的 Java 类。首先介绍已编译类,然 后将利用大量说明性示例,介绍用于生成和转换已编译类的相应 ASM 接口、组件和工具。方法、 注释和泛型的内容将在之后各章中说明。
2.1 结构
2.1.1 概述
已编译类的总体结构非常简单。实际上,与原生编译应用程序不同,已编译类中保留了来自 源代码的结构信息和几乎所有符号。事实上,已编译类中包含如下各部分:
专门一部分,述类的修饰符(比如public和private)、名字、超类、接口和注 释。
类中声明的每个字段各有一部分。每一部分述一个字段的修饰符、名字、类型和注释。 类中声明的每个方法及构造器各有一部分。每一部分述一个方法的修饰符、名字、返 回类型与参数类型、注释。它还以 Java 字节代码指令的形式,包含了该方法的已编译
代码。
但在源文件类和已编译类之间还是有一些差异:
一个已编译类仅述一个类,而一个源文件中可以包含几个类。比如,一个源文件述 了一个类,这个类又有一个内部类,那这个源文件会被编译为两个类文件:主类和内 部类各一个文件。但是,主类文件中包含对其内部类的引用,定义了内部方法的内层 类会包含引用,引向其封装的方法。
已编译类中当然不包含注释(comment),但可以包含类、字段、方法和代码属性,可 以利用这些属性为相应元素关联更多信息。Java 5 中引入可用于同一目的的注释 (annotaion)以后,属性已经变得没有什么用处了。
编译类中不包含package和import部分,因此,所有类型名字都必须是完全限定的。 另一个非常重要的结构性差异是已编译类中包含常量池(constant pool)部分。这个池是一个数组,其中包含了在类中出现的所有数值、字符串和类型常量。这些常量仅在这个常量池部分 中定义一次,然后可以利用其索引,在类文件中的所有其他各部分进行引用。幸好,ASM 隐藏 了与常量池有关的所有细节,所以我们不用再为它操心了。图 2.1 中总结了一个已编译类的整体 结构。其确切结构在《Java 虚拟机规范》第 4 节中述。
另一个重要的差别是 Java 类型在已编译类和源文件类中的表示不同。后面几节将解释它们 在已编译类中的表示。
2.1.2 内部名
在许多情况下,一种类型只能是类或接口类型。例如,一个类的超类、由一个类实现的接口, 或者由一个方法抛出的异常就不能是基元类型或数组类型,必须是类或接口类型。这些类型在已 编译类中用内部名字表示。一个类的内部名就是这个类的完全限定名,其中的点号用斜线代替。 例如,String 的内部名为 java/lang/String。
2.1.3 类型述符
内部名只能用于类或接口类型。所有其他 Java 类型,比如字段类型,在已编译类中都是用
类型述符表示的(见图 2.2)。
基元类型的述符是单个字符:Z 表示 boolean,C 表示 char,B 表示 byte,S 表示 short, I 表示 int,F 表示 float,J 表示 long,D 表示 double。一个类类型的述符是这个类的 内部名,前面加上字符 L,后面跟有一个分号。例如,String 的类型述符为 Ljava/lang/String;。而一个数组类型的述符是一个方括号后面跟有该数组元素类型的 述符。
2.1.4 方法述符
方法述符是一个类型述符列表,它用一个字符串述一个方法的参数类型和返回类型。 方法述符以左括号开头,然后是每个形参的类型述符,然后是一个右括号,接下来是返回类 型的类型述符,如果该方法返回 void,则是 V(方法述符中不包含方法的名字或参数名)。
一旦知道了类型述符如何工作,方法述符的理解就容易了。例如,(I)I 述一个方法,
它接受一个 int 类型的参数,返回一个 int。图 2.3 给出了几个方法述符示例。 2.2 接口和组件
2.2.1 介绍
用于生成和变转已编译类的ASM API是基于ClassVisitor抽象类的(见图2.4)。这个 类中的每个方法都对应于同名的类文件结构部分(见图 2.1)。简单的部分只需一个方法调用就能 访问,这个调用返回 void,其参数述了这些部分的内容。有些部分的内容可以达到任意长度、 任意复杂度,这样的部分可以用一个初始方法调用来访问,返回一个辅助的访问者类。 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();
}
图2.4 ClassVisitor类
针对这些辅助类递归适用同样的原则。例如,FieldVisitor 抽象类中的每个方法(见图 2.5)对应于同名的类文件子结构,visitAnnotation 返回一个辅助的 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类
ClassVisitor 类的方法必须按以下顺序调用(在这个类的 Javadoc 中规定):
visit visitSource? visitOuterClass? ( visitAnnotation |
visitAttribute )*
( visitInnerClass | visitField | visitMethod )*
visitEnd
这意味着必须首先调用 visit,然后是对 visitSource 的最多一个调用,接下来是对 visitOuterClass 的最多一个调用,然后是可按任意顺序对 visitAnnotation 和 visitAttribute 的任意多个访问,接下来是可按任意顺序对 visitInnerClass、 visitField 和 visitMethod 的任意多个调用,最后以一个 visitEnd 调用结束。
ASM 供了三个基于 ClassVisitor API 的核心组件,用于生成和变化类:
ClassReader类分析以字节数组形式给出的已编译类,并针对在其accept方法参数 中传送的 ClassVisitor 实例,调用相应的 visitXxx 方法。这个类可以看作一个事 件产生器。
ClassWriter 类是 ClassVisitor 抽象类的一个子类,它直接以二进制形式生成编 译后的类。它会生成一个字节数组形式的输出,其中包含了已编译类,可以用 toByteArray 方法来取。这个类可以看作一个事件使用器。
ClassVisitor类将它收到的所有方法调用都委托给另一个ClassVisitor类。这个 类可以看作一个事件筛选器。
接下来的各节将用一些具体示例来说明如何使用这些组件来生成和转换类。
2.2.2 分析类
在分析一个已经存在的类时,惟一必需的组件是 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 的
getResourceAsStream 方法,可以获得一个读取类内容的输入流,如下:
cl.getResourceAsStream(classname.replace(’.’, ’/’) + “.class”);
示例:
ClassPrinter
package com;
import org.objectweb.asm.*;
import java.io.IOException;
public class ClassPrinter extends ClassVisitor {
private static final int ASM5 = 327680;
public ClassPrinter(int i) throws IOException {
super(ASM5);
}
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("}");
}
public static void main(String[] args) throws IOException {
ClassPrinter cp = new ClassPrinter(327680);
ClassReader cr = new ClassReader("java.lang.String");
cr.accept(cp,0);
}
}
输出
2.2.3 生成类
为生成一个类,惟一必需的组件是 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();
第一行创建了一个 ClassWriter 实例,它实际上将创建类的字节数组表示(构造器参数 在下一章解释)。
对 visit 方法的调用定义了类的标头。V1_5 参数是一个常数,与所有其他 ASM 常量一样, 在ASM Opcodes接口中定义。它指明了类的版本——Java 1.5。ACC_XXX常量是与Java修饰 符对应的标志。这里规定这个类是一个接口,而且它是 public 和 abstract 的(因为它不能 被实例化)。下一个参数以内部形式规定了类的名字(见 2.1.2 节)。回忆一下,已编译类不包含 Package 和 Import 部分,因此,所有类名都必须是完全限定的。下一个参数对应于泛型(见 4.1 节)。在我们的例子中,这个参数是 null,因为这个接口并没有由类型变量进行参数化。第 五个参数是内部形式的超类(接口类隐式继承自 Object)。最后一个参数是一个数组,其中是 被扩展的接口,这些接口由其内部名指定。
接下来对 visitField 的三次调用是类似的,用于定义三个接口字段。第一个参数是一组 标志,对应于 Java 修饰符。这里规定这些字段是 public、final 和 static 的。第二个参数 是字段的名字,与它在源代码中的显示相同。第三个参数是字段的类型,采用类型述符形式。 这里,这些字段是 int 字段,它们的述符是 I。第四个参数对应于泛型。在我们的例子中, 它是 null,因为这些字段类型没有使用泛型。最后一个参数是字段的常量值:这个参数必须仅
用于真正的常量字段,也就是 final static 字段。对于其他字段,它必须为 null。由于此处 没有注释,所以立即调用所返回的 FieldVisitor 的 visitEnd 方法,即对其 visitAnnotation 或 visitAttribute 方法没有任何调用。
visitMethod 调用用于定义 compareTo 方法,同样,第一个参数是一组对应于 Java 修饰 符的标志。第二个参数是方法名,与其在源代码中的显示一样。第三个参数是方法的述符。第 四个参数对应于泛型。在我们的例子中,它是 null,因为这个方法没有使用泛型。最后一个参 数是一个数组,其中包括可由该方法抛出的异常,这些异常由其内部名指明。它在这里为 null, 因为这个方法没有声明任何异常。visitMethod 方法返回 MethodVisitor(见图 3.4),可用 于定义该方法的注释和属性,最重要的是这个方法的代码。这里,由于没有注释,而且这个方法 是抽象的,所以我们立即调用所返回的 MethodVisitor 的 visitEnd 方法。
对 visitEnd 的最后一个调用是为了通知 cw:这个类已经结束,对 toByteArray 的调用 用于以字节数组的形式取它。
- 使用生成的类
前面的字节数组可以存储在一个 Comparable.class 文件中,供以后使用。或者,也可 以用 ClassLoader 动态加载它。一种方法是定义一个 ClassLoader 子类,它的 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);
另一种加载已生成类的方法可能更清晰一些,那就是定义一个 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);
}
}
事实上,所生成类的使用方式取决于上下文,这已经超出了 ASM API 的范围。如果你正在 编写编译器,那类生成过程将由一个抽象语法树驱动,这个语法树代表将要编译的程序,而生成 的类将被存储在磁盘上。如果你正在编写动态代理类生成器或方面编织器,那将会以这种或那种 方式使用一个 ClassLoader。
上面的方式我没有成功,这里有一个成功的demo,感谢博文https://blog.csdn.net/zhangjg_blog/article/details/22976929#commentBox
示例:
Helloworld
package com;
import java.io.FileOutputStream;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
public class Helloworld extends ClassLoader implements Opcodes {
public static void main(final String args[]) throws Exception {
//定义一个叫做Example的类
ClassWriter cw = new ClassWriter(0);
cw.visit(V1_1, ACC_PUBLIC, "Example", null, "java/lang/Object", null);
//生成默认的构造方法
MethodVisitor mw = cw.visitMethod(ACC_PUBLIC,
"<init>",
"()V",
null,
null);
//生成构造方法的字节码指令
mw.visitVarInsn(ALOAD, 0);
mw.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V");
mw.visitInsn(RETURN);
mw.visitMaxs(1, 1);
mw.visitEnd();
//生成main方法
mw = cw.visitMethod(ACC_PUBLIC + ACC_STATIC,
"main",
"([Ljava/lang/String;)V",
null,
null);
//生成main方法中的字节码指令
mw.visitFieldInsn(GETSTATIC,
"java/lang/System",
"out",
"Ljava/io/PrintStream;");
mw.visitLdcInsn("Hello world!");
mw.visitMethodInsn(INVOKEVIRTUAL,
"java/io/PrintStream",
"println",
"(Ljava/lang/String;)V");
mw.visitInsn(RETURN);
mw.visitMaxs(2, 2);
//字节码生成完成
mw.visitEnd();
// 获取生成的class文件对应的二进制流
byte[] code = cw.toByteArray();
//将二进制流写到本地磁盘上
FileOutputStream fos = new FileOutputStream("/Users/leesin/Desktop/asm/Example.class");
fos.write(code);
fos.close();
//直接将二进制流加载到内存中
Helloworld loader = new Helloworld();
Class<?> exampleClass = loader.defineClass("Example", code, 0, code.length);
//通过反射调用main方法
exampleClass.getMethods()[0].invoke(null, new Object[] { null });
}
}
输出
反编译软件打开输出的class文件
转载:https://blog.csdn.net/dataiyangu/article/details/100703818