分享

Class文件格式实战:使用ASM动态生成class文件

 看风景D人 2016-10-07


概述


本专栏前面的文章,主要详细讲解了Class文件的格式,并且在上一篇文章中做了总结。 众所周知, JVM在运行时, 加载并执行class文件, 这个class文件基本上都是由我们所写的Java源文件通过javac编译而得到的。 但是, 我们有时候会遇到这种情况:在前期(编写程序时)不知道要写什么类, 只有到运行时, 才能根据当时的程序执行状态知道要使用什么类。 举一个常见的例子就是JDK中的动态代理。这个代理能够使用一套API代理所有的符合要求的类, 那么这个代理就不可能在JDK编写的时候写出来, 因为当时还不知道用户要代理什么类。 


当遇到上述情况时, 就要考虑这种机制:在运行时动态生成class文件。 也就是说, 这个class文件已经不是由你的Java源码编译而来,而是由程序动态生成。 能够做这件事的,有JDK中的动态代理API, 还有一个叫做cglib的开源库。 这两个库都是偏重于动态代理的, 也就是以动态生成class的方式来支持代理的动态创建。 除此之外, 还有一个叫做ASM的库, 能够直接生成class文件,它的api对于动态代理的API来说更加原生, 每个api都和class文件格式中的特定部分相吻合, 也就是说, 如果对class文件的格式比较熟练, 使用这套API就会相对简单。 下面我们通过一个实例来讲解ASM的使用, 并且在使用的过程中, 会对应class文件中的各个部分来说明。


ASM示例:HelloWorld


ASM的实现基于一套Java API, 所以我们首先得到ASM库, 在这个我使用的是ASM 4.0的jar包 。 


首先以ASM中的HelloWorld实例来讲解, 比如我们要生成以下代码对应的class文件:

  1. public class Example {  
  2.   
  3.     public static void main (String[] args) {  
  4.         System.out.println("Hello world!");  
  5. }  

但是这个class文件不能在开发时通过上面的源码来编译成, 而是要动态生成。 下面我们介绍如何使用ASM动态生成上述源码对应的字节码。


下面是代码示例(该实例来自于ASM官方的sample):

  1. import java.io.FileOutputStream;  
  2.   
  3. import org.objectweb.asm.ClassWriter;  
  4. import org.objectweb.asm.MethodVisitor;  
  5. import org.objectweb.asm.Opcodes;  
  6.   
  7. public class Helloworld extends ClassLoader implements Opcodes {  
  8.   
  9.     public static void main(final String args[]) throws Exception {  
  10.   
  11.   
  12.         //定义一个叫做Example的类  
  13.         ClassWriter cw = new ClassWriter(0);  
  14.         cw.visit(V1_1, ACC_PUBLIC, "Example", null, "java/lang/Object", null);  
  15.   
  16.         //生成默认的构造方法  
  17.         MethodVisitor mw = cw.visitMethod(ACC_PUBLIC,  
  18.                 "<init>",  
  19.                 "()V",  
  20.                 null,  
  21.                 null);  
  22.   
  23.         //生成构造方法的字节码指令  
  24.         mw.visitVarInsn(ALOAD, 0);  
  25.         mw.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V");  
  26.         mw.visitInsn(RETURN);  
  27.         mw.visitMaxs(1, 1);  
  28.         mw.visitEnd();  
  29.   
  30.         //生成main方法  
  31.         mw = cw.visitMethod(ACC_PUBLIC + ACC_STATIC,  
  32.                 "main",  
  33.                 "([Ljava/lang/String;)V",  
  34.                 null,  
  35.                 null);  
  36.   
  37.         //生成main方法中的字节码指令  
  38.         mw.visitFieldInsn(GETSTATIC,  
  39.                 "java/lang/System",  
  40.                 "out",  
  41.                 "Ljava/io/PrintStream;");  
  42.   
  43.         mw.visitLdcInsn("Hello world!");  
  44.         mw.visitMethodInsn(INVOKEVIRTUAL,  
  45.                 "java/io/PrintStream",  
  46.                 "println",  
  47.                 "(Ljava/lang/String;)V");  
  48.         mw.visitInsn(RETURN);  
  49.         mw.visitMaxs(2, 2);  
  50.   
  51.         //字节码生成完成  
  52.         mw.visitEnd();  
  53.   
  54.         // 获取生成的class文件对应的二进制流  
  55.         byte[] code = cw.toByteArray();  
  56.   
  57.   
  58.         //将二进制流写到本地磁盘上  
  59.         FileOutputStream fos = new FileOutputStream("Example.class");  
  60.         fos.write(code);  
  61.         fos.close();  
  62.   
  63.         //直接将二进制流加载到内存中  
  64.         Helloworld loader = new Helloworld();  
  65.         Class<?> exampleClass = loader.defineClass("Example", code, 0, code.length);  
  66.   
  67.         //通过反射调用main方法  
  68.         exampleClass.getMethods()[0].invoke(null, new Object[] { null });  
  69.   
  70.           
  71.     }  
  72. }  

下面详细介绍生成class的过程:


1 首先定义一个类


相关代码片段如下:

  1. //定义一个叫做Example的类  
  2. ClassWriter cw = new ClassWriter(0);  
  3. cw.visit(V1_1, ACC_PUBLIC, "Example", null, "java/lang/Object", null);  

ClassWriter类是ASM中的核心API , 用于生成一个类的字节码。 ClassWriter的visit方法定义一个类。 

第一个参数V1_1是生成的class的版本号, 对应class文件中的主版本号和次版本号, 即minor_version和major_version 。 

第二个参数ACC_PUBLIC表示该类的访问标识。这是一个public的类。 对应class文件中的access_flags 。

第三个参数是生成的类的类名。 需要注意,这里是类的全限定名。 如果生成的class带有包名, 如com.jg.zhang.Example, 那么这里传入的参数必须是com/jg/zhang/Example  。对应class文件中的this_class  。

第四个参数是和泛型相关的, 这里我们不关新, 传入null表示这不是一个泛型类。这个参数对应class文件中的Signature属性(attribute) 。
 
第五个参数是当前类的父类的全限定名。 该类直接继承Object。 这个参数对应class文件中的super_class 。 

第六个参数是String[]类型的, 传入当前要生成的类的直接实现的接口。 这里这个类没实现任何接口, 所以传入null 。 这个参数对应class文件中的interfaces 。 


2 定义默认构造方法, 并生成默认构造方法的字节码指令 


相关代码片段如下:
  1. //生成默认的构造方法  
  2. MethodVisitor mw = cw.visitMethod(ACC_PUBLIC,  
  3.         "<init>",  
  4.         "()V",  
  5.         null,  
  6.         null);  
  7.   
  8. //生成构造方法的字节码指令  
  9. mw.visitVarInsn(ALOAD, 0);  
  10. mw.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V");  
  11. mw.visitInsn(RETURN);  
  12. mw.visitMaxs(1, 1);  
  13. mw.visitEnd();  


使用上面创建的ClassWriter对象, 调用该对象的visitMethod方法, 得到一个MethodVisitor对象, 这个对象定义一个方法。 对应class文件中的一个method_info 。 


第一个参数是 ACC_PUBLIC , 指定要生成的方法的访问标志。 这个参数对应method_info 中的access_flags 。 

第二个参数是方法的方法名。 对于构造方法来说, 方法名为<init> 。 这个参数对应method_info 中的name_index , name_index引用常量池中的方法名字符串。 

第三个参数是方法描述符, 在这里要生成的构造方法无参数, 无返回值, 所以方法描述符为 ()V  。 这个参数对应method_info 中的descriptor_index 。 

第四个参数是和泛型相关的, 这里传入null表示该方法不是泛型方法。这个参数对应method_info 中的Signature属性。

第五个参数指定方法声明可能抛出的异常。 这里无异常声明抛出, 传入null 。 这个参数对应method_info 中的Exceptions属性。

接下来调用MethodVisitor中的多个方法, 生成当前构造方法的字节码。 对应method_info 中的Code属性。

1 调用visitVarInsn方法,生成aload指令, 将第0个本地变量(也就是this)压入操作数栈。

2 调用visitMethodInsn方法, 生成invokespecial指令, 调用父类(也就是Object)的构造方法。

3 调用visitInsn方法,生成return指令, 方法返回。 

4 调用visitMaxs方法, 指定当前要生成的方法的最大局部变量和最大操作数栈。 对应Code属性中的max_stack和max_locals 。 

5 最后调用visitEnd方法, 表示当前要生成的构造方法已经创建完成。 


3 定义main方法, 并生成main方法中的字节码指令


对应的代码片段如下:
  1. mw = cw.visitMethod(ACC_PUBLIC + ACC_STATIC,  
  2.         "main",  
  3.         "([Ljava/lang/String;)V",  
  4.         null,  
  5.         null);  
  6.   
  7. //生成main方法中的字节码指令  
  8. mw.visitFieldInsn(GETSTATIC,  
  9.         "java/lang/System",  
  10.         "out",  
  11.         "Ljava/io/PrintStream;");  
  12.   
  13. mw.visitLdcInsn("Hello world!");  
  14. mw.visitMethodInsn(INVOKEVIRTUAL,  
  15.         "java/io/PrintStream",  
  16.         "println",  
  17.         "(Ljava/lang/String;)V");  
  18. mw.visitInsn(RETURN);  
  19. mw.visitMaxs(2, 2);  
  20. mw.visitEnd();  

这个过程和上面的生成默认构造方法的过程是一致的。 读者可对比上一步执行分析。


4 生成class数据, 保存到磁盘中, 加载class数据


对应代码片段如下:
  1. // 获取生成的class文件对应的二进制流  
  2. byte[] code = cw.toByteArray();  
  3.   
  4.   
  5. //将二进制流写到本地磁盘上  
  6. FileOutputStream fos = new FileOutputStream("Example.class");  
  7. fos.write(code);  
  8. fos.close();  
  9.   
  10. //直接将二进制流加载到内存中  
  11. Helloworld loader = new Helloworld();  
  12. Class<?> exampleClass = loader.defineClass("Example", code, 0, code.length);  
  13.   
  14. //通过反射调用main方法  
  15. exampleClass.getMethods()[0].invoke(null, new Object[] { null });  

这段代码首先获取生成的class文件的字节流, 把它写在本地磁盘的Example.class文件中。 然后加载class字节流, 并通过反射调用main方法。

这段代码执行完, 可以看到控制台有以下输出:
  1. Hello world!  

然后在当前测试工程的根目录下, 生成一个Example.class文件文件。



下面我们使用javap反编译这个class文件:
  1. javap -c -v -classpath . -private Example  

输出的完整信息如下:
  1. Classfile /C:/Users/纪刚/Desktop/生成字节码/AsmJavaTest/Example.class  
  2.   Last modified 2014-4-5; size 338 bytes  
  3.   MD5 checksum 281abde0e2012db8ad462279a1fbb6a4  
  4. public class Example  
  5.   minor version: 3  
  6.   major version: 45  
  7.   flags: ACC_PUBLIC  
  8. Constant pool:  
  9.    #1 = Utf8               Example  
  10.    #2 = Class              #1             //  Example  
  11.    #3 = Utf8               java/lang/Object  
  12.    #4 = Class              #3             //  java/lang/Object  
  13.    #5 = Utf8               <init>  
  14.    #6 = Utf8               ()V  
  15.    #7 = NameAndType        #5:#6          //  "<init>":()V  
  16.    #8 = Methodref          #4.#7          //  java/lang/Object."<init>":()V  
  17.    #9 = Utf8               main  
  18.   #10 = Utf8               ([Ljava/lang/String;)V  
  19.   #11 = Utf8               java/lang/System  
  20.   #12 = Class              #11            //  java/lang/System  
  21.   #13 = Utf8               out  
  22.   #14 = Utf8               Ljava/io/PrintStream;  
  23.   #15 = NameAndType        #13:#14        //  out:Ljava/io/PrintStream;  
  24.   #16 = Fieldref           #12.#15        //  java/lang/System.out:Ljava/io/PrintStream;  
  25.   #17 = Utf8               Hello world!  
  26.   #18 = String             #17            //  Hello world!  
  27.   #19 = Utf8               java/io/PrintStream  
  28.   #20 = Class              #19            //  java/io/PrintStream  
  29.   #21 = Utf8               println  
  30.   #22 = Utf8               (Ljava/lang/String;)V  
  31.   #23 = NameAndType        #21:#22        //  println:(Ljava/lang/String;)V  
  32.   #24 = Methodref          #20.#23        //  java/io/PrintStream.println:(Ljava/lang/String;)V  
  33.   #25 = Utf8               Code  
  34. {  
  35.   public Example();  
  36.     flags: ACC_PUBLIC  
  37.     Code:  
  38.       stack=1, locals=1, args_size=1  
  39.          0: aload_0  
  40.          1: invokespecial #8                  // Method java/lang/Object."<init>":()V  
  41.          4: return  
  42.   
  43.   public static void main(java.lang.String[]);  
  44.     flags: ACC_PUBLIC, ACC_STATIC  
  45.     Code:  
  46.       stack=2, locals=2, args_size=1  
  47.          0: getstatic     #16                 // Field java/lang/System.out:Ljava/io/PrintStream;  
  48.          3: ldc           #18                 // String Hello world!  
  49.          5: invokevirtual #24                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V  
  50.          8: return  
  51. }  

正是一个标准的class格式的文件, 它和以下源码是对应的:

  1. public class Example {  
  2.   
  3.     public static void main (String[] args) {  
  4.         System.out.println("Hello world!");  
  5. }  

只是, 上面的class文件不是由这段源代码生成的, 而是使用ASM动态创建的。 



ASM示例二: 生成字段, 并给字段加注解


上面的HelloWorld示例演示了如何生成类和方法, 该示例演示如何生成字段, 并给字段加注解。 

  1. public class BeanTest extends ClassLoader implements Opcodes {  
  2.   
  3.     /* 
  4.      * 生成以下类的字节码 
  5.      *  
  6.      * public class Person { 
  7.      *  
  8.      *      @NotNull 
  9.      *      public String name; 
  10.      *  
  11.      * } 
  12.      */  
  13.   
  14.     public static void main(String[] args) throws Exception {  
  15.           
  16.         /********************************class***********************************************/  
  17.   
  18.         // 创建一个ClassWriter, 以生成一个新的类  
  19.   
  20.         ClassWriter cw = new ClassWriter(0);  
  21.         cw.visit(V1_6, ACC_PUBLIC, "com/pansoft/espdb/bean/Person", null, "java/lang/Object", null);  
  22.           
  23.           
  24.           
  25.         /*********************************constructor**********************************************/  
  26.           
  27.         MethodVisitor mw = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null,  
  28.                 null);  
  29.         mw.visitVarInsn(ALOAD, 0);  
  30.         mw.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V");  
  31.         mw.visitInsn(RETURN);  
  32.         mw.visitMaxs(1, 1);  
  33.         mw.visitEnd();  
  34.           
  35.           
  36.         /*************************************field******************************************/  
  37.       
  38.         //生成String name字段  
  39.         FieldVisitor  fv = cw.visitField(ACC_PUBLIC, "name", "Ljava/lang/String;", null, null);  
  40.         AnnotationVisitor  av = fv.visitAnnotation("LNotNull;", true);  
  41.         av.visit("value", "abc");  
  42.         av.visitEnd();  
  43.         fv.visitEnd();  
  44.   
  45.           
  46.           
  47.         /***********************************generate and load********************************************/  
  48.           
  49.         byte[] code = cw.toByteArray();  
  50.           
  51.         BeanTest loader = new BeanTest();  
  52.         Class<?> clazz = loader.defineClass(null, code, 0, code.length);  
  53.           
  54.           
  55.         /***********************************test********************************************/  
  56.           
  57.         Object beanObj = clazz.getConstructor().newInstance();  
  58.           
  59.         clazz.getField("name").set(beanObj, "zhangjg");  
  60.           
  61.         String nameString = (String) clazz.getField("name").get(beanObj);  
  62.         System.out.println("filed value : " + nameString);  
  63.           
  64.         String annoVal = clazz.getField("name").getAnnotation(NotNull.class).value();  
  65.         System.out.println("annotation value: " + annoVal);  
  66.           
  67.     }  
  68. }  

上面代码是完整的代码, 用于生成一个和以下代码相对应的class:
  1. public class Person {  
  2.   
  3.      @NotNull  
  4.      public String name;  
  5.   
  6. }  

生成类和构造方法的部分就略过了, 和上面的示例是一样的。 下面看看字段和字段的注解是如何生成的。 相关逻辑如下:

  1. FieldVisitor  fv = cw.visitField(ACC_PUBLIC, "name", "Ljava/lang/String;", null, null);  
  2. AnnotationVisitor  av = fv.visitAnnotation("LNotNull;", true);  
  3. av.visit("value", "abc");  
  4. av.visitEnd();  
  5. fv.visitEnd();  

ClassWriter的visitField方法, 用于定义一个字段。 对应class文件中的一个filed_info 。 

第一个参数是字段的访问修饰符, 这里传入ACC_PUBLIC表示是一个public的属性。 这个参数和filed_info 中的access_flags相对应。

第二个参数是字段的字段名。 这个参数和filed_info 中的name_index相对应。

第三个参数是字段的描述符, 这个字段是String类型的,它的字段描述符为 "Ljava/lang/String;" 。 这个参数和filed_info 中的descriptor_index相对应。

第四个参数和泛型相关的, 这里传入null, 表示该字段不是泛型的。 这个参数和filed_info 中的Signature属性相对应。

第五个参数是字段的值, 只适用于静态字段,当前要生成的字段不是静态的, 所以传入null 。 这个参数和filed_info 中的ConstantValue属性相对应。

使用visitField方法定义完当前字段, 返回一个FieldVisitor对象。 下面调用这个对象的visitAnnotation方法, 为该字段生成注解信息。 visitAnnotation的两个参数如下:

第一个参数是要生成的注解的描述符, 传入"LNotNull;" 。

第二个参数表示该注解是否运行时可见。 如果传入true, 表示运行时可见, 这个注解信息就会生成filed_info 中的一个RuntimeVisibleAnnotation属性。 传入false, 表示运行时不可见,个注解信息就会生成filed_info 中的一个RuntimeInvisibleAnnotation属性 。 

接下来调用上一步返回的AnnotationVisitor对象的visit方法, 来生成注解的值信息。 



ClassWriter的其他重要方法


ClassWriter中还有其他一些重要方法, 这些方法能够生成class文件中的所有相关信息。 这些方法, 以及对象生成class文件中的什么信息, 都列在下面:

  1. //定义一个类  
  2. public void visit(  
  3.     int version,  
  4.     int access,  
  5.     String name,  
  6.     String signature,  
  7.     String superName,  
  8.     String[] interfaces)  
  9.   
  10.   
  11. //定义源文件相关的信息,对应class文件中的Source属性  
  12. public void visitSource(String source, String debug)  
  13.   
  14. //以下两个方法定义内部类和外部类相关的信息, 对应class文件中的InnerClasses属性  
  15. public void visitOuterClass(String owner, String name, String desc)   
  16.   
  17. public void visitInnerClass(  
  18.     String name,  
  19.     String outerName,  
  20.     String innerName,  
  21.     int access)  
  22.   
  23.   
  24. //定义class文件中的注解信息, 对应class文件中的RuntimeVisibleAnnotations属性或者RuntimeInvisibleAnnotations属性  
  25. public AnnotationVisitor visitAnnotation(String desc, boolean visible)  
  26.   
  27. //定义其他非标准属性  
  28. public void visitAttribute(Attribute attr)  
  29.   
  30.   
  31.   
  32. //定义一个字段, 返回的FieldVisitor用于生成字段相关的信息  
  33. public FieldVisitor visitField(  
  34.     int access,  
  35.     String name,  
  36.     String desc,  
  37.     String signature,  
  38.     Object value)  
  39.   
  40.   
  41. //定义一个方法, 返回的MethodVisitor用于生成方法相关的信息  
  42. public MethodVisitor visitMethod(  
  43.     int access,  
  44.     String name,  
  45.     String desc,  
  46.     String signature,  
  47.     String[] exceptions)  

每个方法都是和class文件中的某部分数据相对应的, 如果对class文件的格式比较熟悉的话, 使用ASM生成一个简单的类, 还是很容易的。


总结


在本文中, 通过使用开源的ASM库, 动态生成了两个类。 通过讲解这两个类的生成过程, 可以加深对class文件格式的理解。 因为ASM库中的每个API都是对应class文件中的某部分信息的。 如果对class文件格式不熟悉, 可以参考本专栏之前的讲解class文件格式的一系列博客。 

本文使用的两个示例都放在了一个单独的, 可直接运行的工程中, 该工程已经上传到我的百度网盘, 这个工程的lib目录中, 有ASM 4.0的jar包。 和该工程一起打包的, 还有ASM 4.0的源码和示例程序。 

上述资源下载地址: http://pan.baidu.com/s/1dDmq84T 




更多关于深入理解Java的文章, 请关注我的专栏 : http://blog.csdn.net/column/details/zhangjg-java-blog.html

更多关于Java和Android等其他技术的文章, 请关注我的博客: http://blog.csdn.net/zhangjg_blog



    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多