ASM是什么?
简单地说,ASM是一个用于Java字节码操作的类库,它提供了对字节码上一层的抽象,即Java Class文件中的各个部分进行操作的API。ASM这个名字来自于C的关键字__asm__。
ASM可以干什么?
先来看一幅图,如下图:

如果我们把我们平常从Java文件到运行的过程分成编译器和Java虚拟机两部分,那么Class文件就是两者之间产生联系的桥梁。一般情况下,Class文件是通过javac编译器产生的,然后通过类加载器加载到虚拟机内,再通过执行引擎去执行。现在有了ASM们就可以它的API直接生成符合Java虚拟机规范的Class字节流,这样,ASM做的事情一定程度上正是javac解释器做的工作。
那么,我们就可以通过ASM来实现诸如代码生成,代码混淆,代码转换等等以字节码为操作目标的工作。
如何使用ASM
ASM提供了两套API供使用者使用,一套叫Core API,是基于事件的方式对字节码进行处理;另一套叫Tree API,是基于对象的方式对字节码进行处理。如果你熟悉XML解析,那么实际上Core API就是SAX这种处理模式,而Tree API就是DOM这种处理模式。
Core API
使用Core API进行对字节码进行处理一般需要三个部分:
- 一个事件的生产者,用于产生各种事件,通常这会是一个
ClassReader
- 一个事件的消费者,用于消费各种事件,通常这会是一个
ClassWriter
- 若干个事件的过滤器,这些过滤器可以对感兴趣的事件进行过滤来处理,这通常会是一些
ClassVisitor
其处理过程如下图所示:

看下面一个例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| public class ASMTest {
public static void main(String[] args) throws IOException {
FileInputStream fis = new FileInputStream(new File("/Users/apple/Desktop/Test.class"));
ClassReader cr = new ClassReader(fis);
ClassWriter cw = new ClassWriter(cr, 0);
ClassVisitor cv = new ClassVisitor(Opcodes.ASM4, cw) {
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
if ((access & Opcodes.ACC_PUBLIC) != 0) {
access = (access ^ Opcodes.ACC_PUBLIC) + Opcodes.ACC_PRIVATE;
}
return cv.visitMethod(access, name, desc, signature, exceptions);
}
};
cr.accept(cv, 0);
FileOutputStream fos = new FileOutputStream(new File("/Users/apple/Desktop/result/Test.class"));
fos.write(cw.toByteArray());
fos.flush();
fos.close();
}
}
|
这个例子先是从文件流中读入一个待处理的Class文件,然后new了ClassReader,作为事件源,然后new了一个ClassWriter,作为事件的接收者,还实现了一个ClassVisitor,这个ClassVisitor将所有的public方法变成了private的方法。接着通过调用cr.accept方法来触发事件,通过cw.toByteArray来拿到处理后的字节码并且输出到文件。
从前面的图以及代码可以看出,采用Core API处理字节码,其实就是通过继承ClassVisitor,并且覆盖ClassVisitor中的对应的方法来对特定的事件进行处理的过程,其实这里的事件基本上对应到了Class文件中的各个部分,除了常量池部分,所以如果了解了Class文件的结构,那么用Core API处理起来应该得心应手。
除了ClassVisitor之外,Core API还提供了MethodVisitor、FieldVisitor、AnnotationVisitor来对方法,字段和注解操作。
Tree API
前面说过,Tree API是基于对象的方式来处理字节码,Tree API的最核心的一个类就是ClassNode,它就代表了一个Java Class文件,它里面的属性对应到了一个Class文件的各个部分,如下图:

使用Tree API来创建一个类的过程,其实就是设置ClassNode的属性的一个过程,一个例子如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| public class ASMTest {
@SuppressWarnings("unchecked")
public static void main(String[] args) throws IOException {
ClassNode cn = new ClassNode();
cn.name = "ASMInterface";
cn.access = Opcodes.ACC_PUBLIC + Opcodes.ACC_ABSTRACT + Opcodes.ACC_INTERFACE;
cn.superName = Type.getInternalName(Object.class);
cn.interfaces.add(Type.getInternalName(Runnable.class));
cn.version = Opcodes.V1_6;
cn.sourceFile = "ASMInterface.java";
MethodNode mn = new MethodNode(Opcodes.ACC_PUBLIC + Opcodes.ACC_ABSTRACT + Opcodes.ACC_SYNTHETIC, "stop", "()V", null, null);
cn.methods.add(mn);
ClassWriter cw = new ClassWriter(0);
cn.accept(cw);
FileOutputStream fos = new FileOutputStream(new File("/Users/apple/Desktop/result/ASMInterface.class"));
fos.write(cw.toByteArray());
fos.flush();
fos.close();
}
}
|
上面的代码,新建了一个ClassNode,用来创建一个继承了java.lang.Runnable接口的接口ASMInterface,它包含了一个方法public void stop(),最后将生成的字节码通过ClassWriter输出到文件。
和创建类一样,通过Tree API修改一个类也只需要修改ClassNode的属性。如果你要修改方法,字段或者注解,那么可以通过ClassNode拿到MethodNode、FieldNode、AnnotationNode来进行对应的修改。
什么时候使用Core API,什么时候使用Tree API?
Core API和Tree API其实各有优缺点:
- Core API的优势是处理速度快,占用内存小,因为它不需要在内存中将整个Class文件表示出来,缺点是基于事件的方式处理,如果错过一个事件,那么就是过了这个村,没有这个店了,这样如果需要实现诸如将特定
GOTO出插入其他指令,就会比较麻烦,因为GOTO可以跳转到之前的指令,但是之前的指令的事件已经被处理了,到时候只能再次触发一遍事件来处理。
- Tree API的优势就是Core API的劣势,对于上面提到的
GOTO的这种情况,Tree API处理起来就轻松了很多,因为在内存中有Class文件的完整表示,随便什么样的顺序去改都是没有问题的。缺点就是占用内存比较大,处理速度比较慢。
如果你查看ClassNode的源代码,那么可以发现ClassNode事实上继承了ClassVisitor。那么,我们就可以将在实际操作的时候将Core API和Tree API结合起来,灵活运用各自的优缺点去解决问题。
辅助工具类
ASM除了提供了Core API和Tree API两套API以外,还提供了几个比较实用的工具类
CheckClassAdapter
实际上,用ASM生成的字节码可能并不符合Java虚拟机规范的,如果需要检查生成的字节码符不符合规范,那么可以用CheckClassAdapter作为一个ClassVisitor加入到ClassVisitor链中,如果字节码不符合规范,那么CheckClassAdapter就会抛出异常。
ASMifier
ASM作为一个字节码操作工具,相对于其他的字节码操作工具,比如Javassist,写起来还是比较烦琐的,如果你已经有了一个Class文件,想要知道如何通过ASM生成这个Class文件,那么就可以直接用ASMifier这个类,通过这个类,可以直接生成出生成目标类的ASM代码,一定程度上简化了直接手写ASM代码的繁琐工作。
ASMifier可以直接通过命令行来使用,比如那我们刚才生成的那个ASMInterface为例:

可以看到ASMifier直接将生成ASMInterface所需要的ASM代码直接打印出来了。
LocalVariableSorter
假设你要往一个方法里面加入一个本地变量,那么你就需要将这个变量加入到本地变量表的最后,遗憾的是,本地变量表的大小只有当你在调用visitMaxs的时候才知道,通常,这个时候已经到了方法的结尾处,再想加本地变量已经晚了,现在通过LocalVariableSorter这个ClassVisitor,你就可以非常简单插入一个本地变量。
总结
ASM作为其他很多的代码生成工具的所依赖的类库,其处理速度快,体积小,并且通过提供了对Class文件各个部分的操作API,可以让你对Class文件进行最细致的修改。但是对于大段大段的代码生成,使用ASM还是显得有些繁琐,即使有ASMifier,还是不如Javassist来的直观,使用过程中最好根据自己的需求进行取舍。
其他说明: