ASM读写字节码
来源:互联网 发布:小甲鱼python好吗 编辑:程序博客网 时间:2024/05/01 10:03
ASM 3.0 编程框架
ASM 通过树这种数据结构来表示复杂的字节码结构,并利用 Push 模型来对树进行遍历,在遍历过程中对字节码进行修改。所谓的 Push 模型类似于简单的 Visitor 设计模式,因为需要处理字节码结构是固定的,所以不需要专门抽象出一种 Vistable 接口,而只需要提供 Visitor 接口。所谓 Visitor 模式和 Iterator 模式有点类似,它们都被用来遍历一些复杂的数据结构。Visitor 相当于用户派出的代表,深入到算法内部,由算法安排访问行程。Visitor 代表可以更换,但对算法流程无法干涉,因此是被动的,这也是它和 Iterator 模式由用户主动调遣算法方式的最大的区别。
在 ASM 中,提供了一个 ClassReader
类,这个类可以直接由字节数组或由 class 文件间接的获得字节码数据,它能正确的分析字节码,构建出抽象的树在内存中表示字节码。它会调用 accept
方法,这个方法接受一个实现了 ClassVisitor
接口的对象实例作为参数,然后依次调用ClassVisitor
接口的各个方法。字节码空间上的偏移被转换成 visit 事件时间上调用的先后,所谓 visit 事件是指对各种不同 visit 函数的调用,ClassReader
知道如何调用各种 visit 函数。在这个过程中用户无法对操作进行干涉,所以遍历的算法是确定的,用户可以做的是提供不同的 Visitor 来对字节码树进行不同的修改。ClassVisitor
会产生一些子过程,比如 visitMethod
会返回一个实现 MethordVisitor
接口的实例,visitField
会返回一个实现 FieldVisitor
接口的实例,完成子过程后控制返回到父过程,继续访问下一节点。因此对于 ClassReader
来说,其内部顺序访问是有一定要求的。实际上用户还可以不通过 ClassReader
类,自行手工控制这个流程,只要按照一定的顺序,各个 visit 事件被先后正确的调用,最后就能生成可以被正确加载的字节码。当然获得更大灵活性的同时也加大了调整字节码的复杂度。
各个 ClassVisitor
通过职责链 (Chain-of-responsibility) 模式,可以非常简单的封装对字节码的各种修改,而无须关注字节码的字节偏移,因为这些实现细节对于用户都被隐藏了,用户要做的只是覆写相应的 visit 函数。
ClassAdaptor
类实现了 ClassVisitor
接口所定义的所有函数,当新建一个 ClassAdaptor
对象的时候,需要传入一个实现了 ClassVisitor
接口的对象,作为职责链中的下一个访问者 (Visitor),这些函数的默认实现就是简单的把调用委派给这个对象,然后依次传递下去形成职责链。当用户需要对字节码进行调整时,只需从 ClassAdaptor
类派生出一个子类,覆写需要修改的方法,完成相应功能后再把调用传递下去。这样,用户无需考虑字节偏移,就可以很方便的控制字节码。
每个 ClassAdaptor
类的派生类可以仅封装单一功能,比如删除某函数、修改字段可见性等等,然后再加入到职责链中,这样耦合更小,重用的概率也更大,但代价是产生很多小对象,而且职责链的层次太长的话也会加大系统调用的开销,用户需要在低耦合和高效率之间作出权衡。用户可以通过控制职责链中 visit 事件的过程,对类文件进行如下操作:
删除类的字段、方法、指令:只需在职责链传递过程中中断委派,不访问相应的 visit 方法即可,比如删除方法时只需直接返回
null
,而不是返回由visitMethod
方法返回的MethodVisitor
对象。class DelLoginClassAdapter extends ClassAdapter { public DelLoginClassAdapter(ClassVisitor cv) { super(cv); } public MethodVisitor visitMethod(final int access, final String name, final String desc, final String signature, final String[] exceptions) { if (name.equals("login")) { return null; } return cv.visitMethod(access, name, desc, signature, exceptions); } }
修改类、字段、方法的名字或修饰符:在职责链传递过程中替换调用参数。
class AccessClassAdapter extends ClassAdapter { public AccessClassAdapter(ClassVisitor cv) { super(cv); } public FieldVisitor visitField(final int access, final String name, final String desc, final String signature, final Object value) { int privateAccess = Opcodes.ACC_PRIVATE; return cv.visitField(privateAccess, name, desc, signature, value); } }
增加新的类、方法、字段
ASM 的最终的目的是生成可以被正常装载的 class 文件,因此其框架结构为客户提供了一个生成字节码的工具类 —— ClassWriter
。它实现了ClassVisitor
接口,而且含有一个 toByteArray()
函数,返回生成的字节码的字节流,将字节流写回文件即可生产调整后的 class 文件。一般它都作为职责链的终点,把所有 visit 事件的先后调用(时间上的先后),最终转换成字节码的位置的调整(空间上的前后),如下例:
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS); ClassAdaptor delLoginClassAdaptor = new DelLoginClassAdapter(classWriter); ClassAdaptor accessClassAdaptor = new AccessClassAdaptor(delLoginClassAdaptor); ClassReader classReader = new ClassReader(strFileName); classReader.accept(classAdapter, ClassReader.SKIP_DEBUG);
综上所述,ASM 的时序图如下:
图 4. ASM – 时序图
在ASM3.3.1中,提供了7个jar包,分别是
asm-3.3.1.jar
asm-commons-3.3.1.jar
asm-tree-3.3.1.jar
asm-analysis-3.3.1.jar
asm-util-3.3.1.jar
asm-xml-3.3.1.jar
参看ASM的javadoc(http://asm.ow2.org/asm33/javadoc/user/index.html),可以看到一共有7个package,package和jar的对应关系如下
asm-3.3.1.jar 包含了org.objectweb.asm和org.objectweb.asm.signature两个packages
asm-commons-3.3.1.jar包含了org.objectweb.asm.commons这个package
asm-tree-3.3.1.jar 包含了org.objectweb.asm.tree这个package
asm-analysis-3.3.1.jar包含了org.objectweb.asm.tree.analysis这个package
asm-util-3.3.1.jar包含了org.objectweb.asm.util这个package
asm-xml-3.3.1.jar包含了org.objectweb.asm.xml这个package
其中asm-3.3.1.jar,是包含了核心的功能,而其他的jar,都是基于这个核心的扩展。
我们这里来看一下,如何读写字节码
ClassReader和ClassVisitor
如大家所了解的,ASM是一个操作字节码(bytecode)的框架,非常的小巧和快速,这个asm-3.3.1.jar,只有43k的大小。
asm提供了字节码的读写的功能。而asm的核心,采用的是visitor的模式,提供了ClassReader和ClassWriter这两个非常重要的类以及ClassVisitor这个核心的接口。
ClassReader的职责是读取字节码。可以用InputStream、byte数组、类名(需要ClassLoader.getSystemResourceAsStream能够加载到的class文件)作为构造函数的参数构造ClassReader对象,来读取字节码。而ClassReader的工作,就是根据字节码的规范,从输入中读取bytecode。而通过ClassReader对象,获取bytecode信息有两种方式,一种就是采用visitor的模式,传入一个ClassVisitor对象给ClassReader的accept方法。另外一种,是使用Low Level的方式,使用ClassReader提供了readXXX以及getXXX的方法来获取信息。
对于一般使用,用ClassReader的accept方法,使用visitor模式就可以了。
那么接着我们来看一下ClassVisitor。
ClassVisitor这个接口,定义了一系列的visit方法,而这些visit方法,我们通过实现ClassVisitor接口中的visit方法(其实就是一堆的回调函数),就能够得到相应的信息。
在asm中,ClassAdapter这个类,用asm的javadoc上的话说,就是一个代理到其他ClassVisitor的一个空的ClassVisitor(An empty ClassVisitor that delegates to another ClassVisitor.)。具体来说,构造ClassAdapter对象的时候,需要传递一个ClassVisitor的对象给ClassAdapter的构造函数,而ClassAdapter对ClassVisitor的实现,就是直接调用这个传给ClassAdapter的ClassVisitor对象的对应visit方法。
后面我们会看到一个使用ClassAdapter的例子。
说到这里,我们主要介绍了ClassReader这个类,ClassVisitor这个接口以及ClassAdapter。那么我们先看一个简单的例子,是使用ClassReader和ClassVisitor的一个示例。
2
3 import java.io.IOException;
4
5 import org.objectweb.asm.AnnotationVisitor;
6 import org.objectweb.asm.Attribute;
7 import org.objectweb.asm.ClassReader;
8 import org.objectweb.asm.ClassVisitor;
9 import org.objectweb.asm.FieldVisitor;
10 import org.objectweb.asm.MethodVisitor;
11
12 public class ClassReaderExample {
13 private static class MyClassVisitor implements ClassVisitor {
14
15 @Override
16 public void visit(int version, int access, String name,
17 String signature, String superName, String[] interfaces) {
18 System.out.println("class name:" + name);
19 System.out.println("super class name:" + superName);
20 System.out.println("class version:" + version);
21 System.out.println("class access:" + access);
22 System.out.println("class signature:" + signature);
23 if(interfaces != null && interfaces.length > 0){
24 for(String str : interfaces){
25 System.out.println("implemented interface name:" + str);
26 }
27 }
28
29 }
30
31 @Override
32 public void visitSource(String source, String debug) {
33 }
34
35 @Override
36 public void visitOuterClass(String owner, String name, String desc) {
37 }
38
39 @Override
40 public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
41 return null;
42 }
43
44 @Override
45 public void visitAttribute(Attribute attr) {
46
47 }
48
49 @Override
50 public void visitInnerClass(String name, String outerName,
51 String innerName, int access) {
52 }
53
54 @Override
55 public FieldVisitor visitField(int access, String name, String desc,
56 String signature, Object value) {
57 return null;
58 }
59
60 @Override
61 public MethodVisitor visitMethod(int access, String name, String desc,
62 String signature, String[] exceptions) {
63 return null;
64 }
65
66 @Override
67 public void visitEnd() {
68
69 }
70 }
71
72 public static void main(String args[]) throws IOException{
73 ClassReader classReader = new ClassReader("java.lang.String");
74 classReader.accept(new MyClassVisitor(), 0);
75 }
76 }
这个简单的例子,打印了String这个类的基本信息。
从上面的例子看,ClassVisitor这个接口的方法中,有些返回值是void,有些是返回了XXXXVisitor。
这里,返回XXXVisitor的,我们举个例子,比如visitMethod这个方法,返回的是MethodVisitor。这里,在visitMethod的方法中,给出了Method的基本信息,如果需要去获取Method内部的详细的信息(包括代码,注解等),那么需要返回一个MethodVisitor对象出去,如果返回null,那么在ClassReader中,就不会去展开对这个Method的详细信息进行遍历了。
除了MethodVisitor,还有,AnnotationVisitor,FieldVisitor。
另外,在org.objectweb.asm.signature这个包中,有一个SignatureVisitor以及SignatureReader和SignatureWriter,这个是处理Signature的。
而除了ClassAdapter外还有MethodAdapter,作用类似与ClassAdapter,只是MethodAdapter是针对MethodVisitor的。
到这里,我们已经了解了如何读取bytecode的信息了,也了解了如何使用ClassReader和ClassVisitor了。 接下来,我们要看一下,如果使用ClassWriter这个类了。 ClassWriter,顾名思义,是用来写字节码的。ClassWriter实现了ClassVisitor这个接口。这里可能大家会觉得奇怪,ClassReader什么接口都没有实现,为啥ClassWriter要实现ClassVisitor的接口呢?ClassWriter只要提供了方法,让我写字节码就行了哇。
大家还记得,通过ClassReader获取类的字节码,有两种方式,一种是使用Visitor模式,这种方法很简单,前面也有demo,另外是使用low level的get和read来进行,这个很复杂。而如果ClassWriter提供的是low level的put和write这类的方法,会提高门槛,很不好用。而ClassWriter实现了ClassVisitor接口,那么就能够很好的跟ClassReader的Visitor模式结合起来。并且,我们使用ASM操作字节码,在写方面更多的是修改、添加和删除,而不是用ASM来完全去写新的class。
ClassWriter中,还有一个toByteArray的方法,这个是把ClassWriter对象生成的字节码,写到一个byte数组中。用于我们来获取字节码的最终结果。
那么,我们可以做一个很简单的例子。就是把ClassWriter对象作为ClassReader accept方法的参数,传给ClassReader,然后在accept方法结束后,我们用ClassWriter对象的toByteArray方法,就能够获得类的字节码了。这个例子,就不贴源码了,有兴趣的同学,可以自己搞一下。
那么,我们把ClassWriter用在哪里呢?上文提到了,删除、修改、增加。那么一般来说,删除我们是不太用的。主要是修改和增加。
比方说我们如果自己要做AOP,用到的就是修改和增加。假如我有一个类,其中有一个execute方法,我如果自己做AOP,我可以把这个execute方法改名,比如叫做execute$1,然后我增加一个execute方法,新的execute方法中,我可以调用原来的execute方法(现在是execute$1)了,并且在调用前后做一个处理。
我们来看一下具体代码
2
3 import org.objectweb.asm.ClassAdapter;
4 import org.objectweb.asm.ClassReader;
5 import org.objectweb.asm.ClassWriter;
6 import org.objectweb.asm.MethodVisitor;
7 import org.objectweb.asm.Opcodes;
8
9 public class ClassWriterAopExample {
10 public static class Foo {
11 public void execute(){
12 System.out.println("Hello World");
13 }
14 }
15
16 public static void main(String[] args) throws Exception{
17 Foo foo = new Foo();
18 foo.execute();
19 ClassReader cr = new ClassReader(Foo.class.getName());
20 ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
21 cr.accept(new ClassAdapter(cw){
22 @Override
23 public void visit(
24 final int version,
25 final int access,
26 final String name,
27 final String signature,
28 final String superName,
29 final String[] interfaces)
30 {
31 cv.visit(version, access, name + "$1", signature, superName, interfaces);
32 }
33
34 @Override
35 public MethodVisitor visitMethod(
36 final int access,
37 final String name,
38 final String desc,
39 final String signature,
40 final String[] exceptions)
41 {
42 if("execute".equals(name)){
43 //这里只是简单的比较了方法名字,其实还需要比较方法参数,参数信息在desc中
44 return cv.visitMethod(access, name + "$1", desc, signature, exceptions);
45 }
46 return cv.visitMethod(access, name, desc, signature, exceptions);
47 }
48
49 }, 0);
50 //到这里,如果调用cr.toByteArray,生成的字节码中,已经没有execute方法了,而是execute$1
51
52 //我们接着需要增加一个execute方法
53 MethodVisitor mv = cw.visitMethod(Opcodes.ACC_PUBLIC, "execute",
54 "()V", null,
55 null);
56 //开始增加代码
57 mv.visitCode();
58 //接下来,我们需要把新的execute方法的内容,增加到这个方法中
59 mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
60 mv.visitLdcInsn("Before execute");
61 mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V");
62 mv.visitVarInsn(Opcodes.ALOAD, 0);
63 mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "org/vanadies/bytecode/example/asm3/ClassWriterAopExample$Foo$1", "execute$1", "()V");
64 mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
65 mv.visitLdcInsn("End execute");
66 mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V");
67 mv.visitInsn(Opcodes.RETURN);
68 mv.visitMaxs(0, 0); //这个地方,最大的操作数栈和最大的本地变量的空间,是自动计算的,是因为构造ClassWriter的时候使用了ClassWriter.COMPUTE_MAXS
69 mv.visitEnd();
70 //到这里,就完成了execute方法的添加。
71 //可以把字节码写入到文件,用javap -c,来看下具体的内容
72 }
73 }
这个代码,要比之前的ClassReader的例子要复杂一些。主要复杂的地方在重写execute方法那里,其实就是类似在用汇编在写代码。具体怎么来写或者生产这个代码,有很多办法。
我们这里只是先展示一下如果能够去修改字节码。这个例子展示了,修改原有方法名字,以及增加方法的做法,其他的增加字段等,都可以通过ClassWriter来处理。另外,需要注意的是,在3.0的ASM中,构造函数的名字,一定要使
用<init>,而不能使用“”,之前看到在2.x中,貌似有这样的用法,但是没有去验证,在网上别人的例子中,也看到过methodName使用“”来表示构造函数的,这个在3.x中是不对的。
具体在运行时能够改变类的行为,我们可以通过Instrumentation的方式或者采用自己实现的ClassLoader的方式来处理。上面例子中,只是展示了如何修改字节码,这个和实际在运行时达到AOP的效果,还是有距离的。
参考文献:
http://www.ibm.com/developerworks/cn/java/j-lo-asm30/#N101ED
http://www.blogjava.net/vanadies10/archive/2011/02/23/344899.html
- ASM读写字节码
- Java字节码框架ASM-读写字节码的用法
- Java字节码框架ASM-读写字节码的用法
- Java字节码框架ASM-读写字节码的用法
- 字节码操作(ObjectWeb ASM)
- asm动态生成字节码
- 10013---ASM字节码框架
- ASM java字节码框架
- 字节码及ASM使用
- 字节码及ASM使用
- Java字节码框架ASM
- ASM JAVA字节码生成架构
- ASM - Java 字节码操控框架
- JAVA ASM字节码操纵框架
- 【ASM系列2】字节码介绍
- Java字节码框架asm快速入门
- asm java字节码操控工具学习
- java 字节码增强之ASM
- JQuery弹出层效果制作
- VPN介绍
- Android中的.9.png图形的机制及制作和使用方法
- 剑指offer面试题13
- [WEB]Velocity
- ASM读写字节码
- POJ2371Questions and answers
- matlab 直方图 整理
- SQL总结(一)基本查询
- hdu 1232简单并查集
- 基于jQuery弹出层有9种效果
- 01_translation_avtivity生命周期04
- nodeValue以及其与value的区别以及JS nodeName、nodeValue、nodeType返回类型
- oracle 连接字符串(拼接字段)