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 事件的过程,对类文件进行如下操作:

  1. 删除类的字段、方法、指令:只需在职责链传递过程中中断委派,不访问相应的 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);  }  }
  2. 修改类、字段、方法的名字或修饰符:在职责链传递过程中替换调用参数。

     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);     }  }
  3. 增加新的类、方法、字段

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 – 时序图
图 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的一个示例。


 1 package org.vanadies.bytecode.example.asm3;
 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)了,并且在调用前后做一个处理。


我们来看一下具体代码


 1 package org.vanadies.bytecode.example.asm3;
 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(00); //这个地方,最大的操作数栈和最大的本地变量的空间,是自动计算的,是因为构造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

0 0
原创粉丝点击