https://blog.csdn.net/zhangjg_blog/article/details/22976929

概述

本专栏前面的文章,主要详细讲解了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. public static void main (String[] args) {
  3. System.out.println("Hello world!");
  4. }

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

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

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

下面详细介绍生成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. mw.visitVarInsn(ALOAD, 0);
  9. mw.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V");
  10. mw.visitInsn(RETURN);
  11. mw.visitMaxs(1, 1);
  12. 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. //生成main方法中的字节码指令
  7. mw.visitFieldInsn(GETSTATIC,
  8. "java/lang/System",
  9. "out",
  10. "Ljava/io/PrintStream;");
  11. mw.visitLdcInsn("Hello world!");
  12. mw.visitMethodInsn(INVOKEVIRTUAL,
  13. "java/io/PrintStream",
  14. "println",
  15. "(Ljava/lang/String;)V");
  16. mw.visitInsn(RETURN);
  17. mw.visitMaxs(2, 2);
  18. mw.visitEnd();

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

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

对应代码片段如下:

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

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

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

Hello world!

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

下面我们使用javap反编译这个class文件:

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. public static void main(java.lang.String[]);
  43. flags: ACC_PUBLIC, ACC_STATIC
  44. Code:
  45. stack=2, locals=2, args_size=1
  46. 0: getstatic     #16                 // Field java/lang/System.out:Ljava/io/PrintStream;
  47. 3: ldc           #18                 // String Hello world!
  48. 5: invokevirtual #24                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
  49. 8: return
  50. }

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

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

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

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

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

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

上面代码是完整的代码, 用于生成一个和以下代码相对应的class:

  1. public class Person {
  2. @NotNull
  3. public String name;
  4. }

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

  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. //定义源文件相关的信息,对应class文件中的Source属性
  10. public void visitSource(String source, String debug)
  11. //以下两个方法定义内部类和外部类相关的信息, 对应class文件中的InnerClasses属性
  12. public void visitOuterClass(String owner, String name, String desc)
  13. public void visitInnerClass(
  14. String name,
  15. String outerName,
  16. String innerName,
  17. int access)
  18. //定义class文件中的注解信息, 对应class文件中的RuntimeVisibleAnnotations属性或者RuntimeInvisibleAnnotations属性
  19. public AnnotationVisitor visitAnnotation(String desc, boolean visible)
  20. //定义其他非标准属性
  21. public void visitAttribute(Attribute attr)
  22. //定义一个字段, 返回的FieldVisitor用于生成字段相关的信息
  23. public FieldVisitor visitField(
  24. int access,
  25. String name,
  26. String desc,
  27. String signature,
  28. Object value)
  29. //定义一个方法, 返回的MethodVisitor用于生成方法相关的信息
  30. public MethodVisitor visitMethod(
  31. int access,
  32. String name,
  33. String desc,
  34. String signature,
  35. String[] exceptions)

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

总结

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

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

上述资源下载地址: 百度网盘-链接不存在

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

  1. 通过asm动态构造class文件

    <!--动态生成class文件--><dependency><groupId>asm</groupId><artifactId>asm< ...

  2. 动态生成JSP文件,并调用JSP程序

    2019独角兽企业重金招聘Python工程师标准>>> 下面这个JSP程序,动态生成可一个testDynamicInclude.inc.jsp,然后在后面调用它. 在JSP的动态生成 ...

  3. java指令导出data文件_直接用 java 命令行动态生成jpg文件 (转)

    直接用 java 命令行动态生成jpg文件 (转)[@more@] /** * jeruGraphics v 1.0 * * 看到一些动态生成图象的例子都是完成的, * 而且很长,觉得不是无论从实用性 ...

  4. java在linux生成pdf文件,从 Java 应用程序动态生成 PDF 文件

    简介: 如果您的应用程序需要动态生成 PDF 文档,那么您需要 iText 库.开源的 iText 库使得 PDF 的创建变得轻松易行.本文介绍了 iText 并提供了一个使用它从 Java 技术应用 ...

  5. java动态生成pdf文件的方法

    java动态生成pdf文件 文章目录 java动态生成pdf文件 前言 一.生成pdf模板 二.使用步骤 1.使用jar包 2.pdf实现方法 总结 前言 java开发过程中难免会遇到生成文件的需求, ...

  6. 如何动态生成pdf文件?

    pdfService系统 一. 背景 在许多开发需求中都有动态生成pdf文件的需求,例如根据已有的json字符串渲染到一个表格中,然后生成对应的PDF文档,以往的解决方法是调用许多个接口生产pdf文件 ...

  7. Java读取pdf模板,并动态生成pdf文件,如动态生成准考证

    Java读取pdf模板,并动态生成pdf文件,如动态生成准考证 ​ 前几天遇到了一个生成准考证的需求,并提供用户下载,然后百度了一圈还是觉得使用itextpdf这个框架好用点.但是还需要找到一个能创建 ...

  8. freeswitch实战八(动态生成拨号计划)

    动态生成拨号计划加动态录音设置 模块创建,动态dialplan的生成配置 1. 安装mod_xml_curl模块1. 在源码目录下:2. make mod_xml_curl && ma ...

  9. php动态生成链接,PHP动态生成javascript文件的2个例子

    一.PHP动态生成 第一步:在PHP文件中直接写入JS代码,并在头部声明这是一个JavaScript文件 复制代码 代码如下:<?php header("Content-Type: a ...

最新文章

  1. xdebug模块输出文件名的配置说明
  2. 【Flutter】Flutter 混合开发 ( Flutter 与 Native 通信 | 通信场景 | Channel 通信机制 | Channel 支持的通信数据类型 | Channel 类型 )
  3. 自下而上滚动公告栏(可悬停)
  4. 蓝桥杯2017初赛-外星日历-数论
  5. OnLongClickListener长按事件设置墙纸
  6. 08 友盟项目--拆分日志为五个表---UDTF自定义函数
  7. MySQL高可用实现:主从结构下ProxySQL中的读写分离
  8. HTTP Error 502.5 - Process Failure 解决方案
  9. 如何告别“芯繁魂乱”难题?这个 OS 来支招!
  10. 流程图伪代码计算机语言,论文中伪代码怎么写
  11. 10.5 欧拉通路与哈密顿通路
  12. 我的IT之路2011(一)
  13. 小黄鸡 php,小黄鸡simsimi接口
  14. ArcGIS创建tpk切片缓存
  15. Boost:宏BOOST_LOG_TRIVIAL的测试
  16. mac pro词典无法使用问题
  17. python安装菜鸟教程_Python菜鸟教程 | 多平台安装
  18. Mesosphere DCOS快速部署手册
  19. Git提交前端代码遇到Lint errors found in the listed files.和husky - pre-push hook failed (add --no-verify)
  20. 人脸活体检测、红外人脸数据集下载

热门文章

  1. 苹果授权登陆 服务端验证(java)
  2. sinoces 2013 消费电子
  3. LaTeX初学者模板 控制纸张大小
  4. 计算机属性里面用户变量没有path怎么办,把系统变量path删了怎么办
  5. 字符识别系统仿真–Matlab GUI实现
  6. 【jumpserver升级】docker pulling image报错dial tcp 104.18.124.25:443: i/o timeout
  7. 2022081班李亚楠20220919
  8. overleaf 插入图片_Overleaf—使用教程-致谢原作者
  9. Canny的C++实现
  10. ajp协议: httpd 代理 tomcat集群