Class文件格式实战:使用ASM动态生成class文件
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文件:
- public class Example {
- public static void main (String[] args) {
- System.out.println("Hello world!");
- }
但是这个class文件不能在开发时通过上面的源码来编译成, 而是要动态生成。 下面我们介绍如何使用ASM动态生成上述源码对应的字节码。
下面是代码示例(该实例来自于ASM官方的sample):
- import java.io.FileOutputStream;
- import org.objectweb.asm.ClassWriter;
- import org.objectweb.asm.MethodVisitor;
- import org.objectweb.asm.Opcodes;
- public class Helloworld extends ClassLoader implements Opcodes {
- public static void main(final String args[]) throws Exception {
- //定义一个叫做Example的类
- ClassWriter cw = new ClassWriter(0);
- cw.visit(V1_1, ACC_PUBLIC, "Example", null, "java/lang/Object", null);
- //生成默认的构造方法
- MethodVisitor mw = cw.visitMethod(ACC_PUBLIC,
- "<init>",
- "()V",
- null,
- null);
- //生成构造方法的字节码指令
- mw.visitVarInsn(ALOAD, 0);
- mw.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V");
- mw.visitInsn(RETURN);
- mw.visitMaxs(1, 1);
- mw.visitEnd();
- //生成main方法
- mw = cw.visitMethod(ACC_PUBLIC + ACC_STATIC,
- "main",
- "([Ljava/lang/String;)V",
- null,
- null);
- //生成main方法中的字节码指令
- mw.visitFieldInsn(GETSTATIC,
- "java/lang/System",
- "out",
- "Ljava/io/PrintStream;");
- mw.visitLdcInsn("Hello world!");
- mw.visitMethodInsn(INVOKEVIRTUAL,
- "java/io/PrintStream",
- "println",
- "(Ljava/lang/String;)V");
- mw.visitInsn(RETURN);
- mw.visitMaxs(2, 2);
- //字节码生成完成
- mw.visitEnd();
- // 获取生成的class文件对应的二进制流
- byte[] code = cw.toByteArray();
- //将二进制流写到本地磁盘上
- FileOutputStream fos = new FileOutputStream("Example.class");
- fos.write(code);
- fos.close();
- //直接将二进制流加载到内存中
- Helloworld loader = new Helloworld();
- Class<?> exampleClass = loader.defineClass("Example", code, 0, code.length);
- //通过反射调用main方法
- exampleClass.getMethods()[0].invoke(null, new Object[] { null });
- }
- }
下面详细介绍生成class的过程:
1 首先定义一个类
相关代码片段如下:
- //定义一个叫做Example的类
- ClassWriter cw = new ClassWriter(0);
- 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 定义默认构造方法, 并生成默认构造方法的字节码指令
相关代码片段如下:
- //生成默认的构造方法
- MethodVisitor mw = cw.visitMethod(ACC_PUBLIC,
- "<init>",
- "()V",
- null,
- null);
- //生成构造方法的字节码指令
- mw.visitVarInsn(ALOAD, 0);
- mw.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V");
- mw.visitInsn(RETURN);
- mw.visitMaxs(1, 1);
- 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方法中的字节码指令
对应的代码片段如下:
- mw = cw.visitMethod(ACC_PUBLIC + ACC_STATIC,
- "main",
- "([Ljava/lang/String;)V",
- null,
- null);
- //生成main方法中的字节码指令
- mw.visitFieldInsn(GETSTATIC,
- "java/lang/System",
- "out",
- "Ljava/io/PrintStream;");
- mw.visitLdcInsn("Hello world!");
- mw.visitMethodInsn(INVOKEVIRTUAL,
- "java/io/PrintStream",
- "println",
- "(Ljava/lang/String;)V");
- mw.visitInsn(RETURN);
- mw.visitMaxs(2, 2);
- mw.visitEnd();
这个过程和上面的生成默认构造方法的过程是一致的。 读者可对比上一步执行分析。
4 生成class数据, 保存到磁盘中, 加载class数据
对应代码片段如下:
- // 获取生成的class文件对应的二进制流
- byte[] code = cw.toByteArray();
- //将二进制流写到本地磁盘上
- FileOutputStream fos = new FileOutputStream("Example.class");
- fos.write(code);
- fos.close();
- //直接将二进制流加载到内存中
- Helloworld loader = new Helloworld();
- Class<?> exampleClass = loader.defineClass("Example", code, 0, code.length);
- //通过反射调用main方法
- 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
输出的完整信息如下:
- Classfile /C:/Users/纪刚/Desktop/生成字节码/AsmJavaTest/Example.class
- Last modified 2014-4-5; size 338 bytes
- MD5 checksum 281abde0e2012db8ad462279a1fbb6a4
- public class Example
- minor version: 3
- major version: 45
- flags: ACC_PUBLIC
- Constant pool:
- #1 = Utf8 Example
- #2 = Class #1 // Example
- #3 = Utf8 java/lang/Object
- #4 = Class #3 // java/lang/Object
- #5 = Utf8 <init>
- #6 = Utf8 ()V
- #7 = NameAndType #5:#6 // "<init>":()V
- #8 = Methodref #4.#7 // java/lang/Object."<init>":()V
- #9 = Utf8 main
- #10 = Utf8 ([Ljava/lang/String;)V
- #11 = Utf8 java/lang/System
- #12 = Class #11 // java/lang/System
- #13 = Utf8 out
- #14 = Utf8 Ljava/io/PrintStream;
- #15 = NameAndType #13:#14 // out:Ljava/io/PrintStream;
- #16 = Fieldref #12.#15 // java/lang/System.out:Ljava/io/PrintStream;
- #17 = Utf8 Hello world!
- #18 = String #17 // Hello world!
- #19 = Utf8 java/io/PrintStream
- #20 = Class #19 // java/io/PrintStream
- #21 = Utf8 println
- #22 = Utf8 (Ljava/lang/String;)V
- #23 = NameAndType #21:#22 // println:(Ljava/lang/String;)V
- #24 = Methodref #20.#23 // java/io/PrintStream.println:(Ljava/lang/String;)V
- #25 = Utf8 Code
- {
- public Example();
- flags: ACC_PUBLIC
- Code:
- stack=1, locals=1, args_size=1
- 0: aload_0
- 1: invokespecial #8 // Method java/lang/Object."<init>":()V
- 4: return
- public static void main(java.lang.String[]);
- flags: ACC_PUBLIC, ACC_STATIC
- Code:
- stack=2, locals=2, args_size=1
- 0: getstatic #16 // Field java/lang/System.out:Ljava/io/PrintStream;
- 3: ldc #18 // String Hello world!
- 5: invokevirtual #24 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
- 8: return
- }
正是一个标准的class格式的文件, 它和以下源码是对应的:
- public class Example {
- public static void main (String[] args) {
- System.out.println("Hello world!");
- }
只是, 上面的class文件不是由这段源代码生成的, 而是使用ASM动态创建的。
ASM示例二: 生成字段, 并给字段加注解
上面的HelloWorld示例演示了如何生成类和方法, 该示例演示如何生成字段, 并给字段加注解。
- public class BeanTest extends ClassLoader implements Opcodes {
- /*
- * 生成以下类的字节码
- *
- * public class Person {
- *
- * @NotNull
- * public String name;
- *
- * }
- */
- public static void main(String[] args) throws Exception {
- /********************************class***********************************************/
- // 创建一个ClassWriter, 以生成一个新的类
- ClassWriter cw = new ClassWriter(0);
- cw.visit(V1_6, ACC_PUBLIC, "com/pansoft/espdb/bean/Person", null, "java/lang/Object", null);
- /*********************************constructor**********************************************/
- MethodVisitor mw = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null,
- null);
- mw.visitVarInsn(ALOAD, 0);
- mw.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V");
- mw.visitInsn(RETURN);
- mw.visitMaxs(1, 1);
- mw.visitEnd();
- /*************************************field******************************************/
- //生成String name字段
- FieldVisitor fv = cw.visitField(ACC_PUBLIC, "name", "Ljava/lang/String;", null, null);
- AnnotationVisitor av = fv.visitAnnotation("LNotNull;", true);
- av.visit("value", "abc");
- av.visitEnd();
- fv.visitEnd();
- /***********************************generate and load********************************************/
- byte[] code = cw.toByteArray();
- BeanTest loader = new BeanTest();
- Class<?> clazz = loader.defineClass(null, code, 0, code.length);
- /***********************************test********************************************/
- Object beanObj = clazz.getConstructor().newInstance();
- clazz.getField("name").set(beanObj, "zhangjg");
- String nameString = (String) clazz.getField("name").get(beanObj);
- System.out.println("filed value : " + nameString);
- String annoVal = clazz.getField("name").getAnnotation(NotNull.class).value();
- System.out.println("annotation value: " + annoVal);
- }
- }
上面代码是完整的代码, 用于生成一个和以下代码相对应的class:
- public class Person {
- @NotNull
- public String name;
- }
生成类和构造方法的部分就略过了, 和上面的示例是一样的。 下面看看字段和字段的注解是如何生成的。 相关逻辑如下:
- FieldVisitor fv = cw.visitField(ACC_PUBLIC, "name", "Ljava/lang/String;", null, null);
- AnnotationVisitor av = fv.visitAnnotation("LNotNull;", true);
- av.visit("value", "abc");
- av.visitEnd();
- 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文件中的什么信息, 都列在下面:
- //定义一个类
- public void visit(
- int version,
- int access,
- String name,
- String signature,
- String superName,
- String[] interfaces)
- //定义源文件相关的信息,对应class文件中的Source属性
- public void visitSource(String source, String debug)
- //以下两个方法定义内部类和外部类相关的信息, 对应class文件中的InnerClasses属性
- public void visitOuterClass(String owner, String name, String desc)
- public void visitInnerClass(
- String name,
- String outerName,
- String innerName,
- int access)
- //定义class文件中的注解信息, 对应class文件中的RuntimeVisibleAnnotations属性或者RuntimeInvisibleAnnotations属性
- public AnnotationVisitor visitAnnotation(String desc, boolean visible)
- //定义其他非标准属性
- public void visitAttribute(Attribute attr)
- //定义一个字段, 返回的FieldVisitor用于生成字段相关的信息
- public FieldVisitor visitField(
- int access,
- String name,
- String desc,
- String signature,
- Object value)
- //定义一个方法, 返回的MethodVisitor用于生成方法相关的信息
- public MethodVisitor visitMethod(
- int access,
- String name,
- String desc,
- String signature,
- String[] exceptions)
每个方法都是和class文件中的某部分数据相对应的, 如果对class文件的格式比较熟悉的话, 使用ASM生成一个简单的类, 还是很容易的。
总结
在本文中, 通过使用开源的ASM库, 动态生成了两个类。 通过讲解这两个类的生成过程, 可以加深对class文件格式的理解。 因为ASM库中的每个API都是对应class文件中的某部分信息的。 如果对class文件格式不熟悉, 可以参考本专栏之前的讲解class文件格式的一系列博客。
本文使用的两个示例都放在了一个单独的, 可直接运行的工程中, 该工程已经上传到我的百度网盘, 这个工程的lib目录中, 有ASM 4.0的jar包。 和该工程一起打包的, 还有ASM 4.0的源码和示例程序。
上述资源下载地址: 百度网盘-链接不存在
Class文件格式实战:使用ASM动态生成class文件相关推荐
- 通过asm动态构造class文件
<!--动态生成class文件--><dependency><groupId>asm</groupId><artifactId>asm< ...
- 动态生成JSP文件,并调用JSP程序
2019独角兽企业重金招聘Python工程师标准>>> 下面这个JSP程序,动态生成可一个testDynamicInclude.inc.jsp,然后在后面调用它. 在JSP的动态生成 ...
- java指令导出data文件_直接用 java 命令行动态生成jpg文件 (转)
直接用 java 命令行动态生成jpg文件 (转)[@more@] /** * jeruGraphics v 1.0 * * 看到一些动态生成图象的例子都是完成的, * 而且很长,觉得不是无论从实用性 ...
- java在linux生成pdf文件,从 Java 应用程序动态生成 PDF 文件
简介: 如果您的应用程序需要动态生成 PDF 文档,那么您需要 iText 库.开源的 iText 库使得 PDF 的创建变得轻松易行.本文介绍了 iText 并提供了一个使用它从 Java 技术应用 ...
- java动态生成pdf文件的方法
java动态生成pdf文件 文章目录 java动态生成pdf文件 前言 一.生成pdf模板 二.使用步骤 1.使用jar包 2.pdf实现方法 总结 前言 java开发过程中难免会遇到生成文件的需求, ...
- 如何动态生成pdf文件?
pdfService系统 一. 背景 在许多开发需求中都有动态生成pdf文件的需求,例如根据已有的json字符串渲染到一个表格中,然后生成对应的PDF文档,以往的解决方法是调用许多个接口生产pdf文件 ...
- Java读取pdf模板,并动态生成pdf文件,如动态生成准考证
Java读取pdf模板,并动态生成pdf文件,如动态生成准考证 前几天遇到了一个生成准考证的需求,并提供用户下载,然后百度了一圈还是觉得使用itextpdf这个框架好用点.但是还需要找到一个能创建 ...
- freeswitch实战八(动态生成拨号计划)
动态生成拨号计划加动态录音设置 模块创建,动态dialplan的生成配置 1. 安装mod_xml_curl模块1. 在源码目录下:2. make mod_xml_curl && ma ...
- php动态生成链接,PHP动态生成javascript文件的2个例子
一.PHP动态生成 第一步:在PHP文件中直接写入JS代码,并在头部声明这是一个JavaScript文件 复制代码 代码如下:<?php header("Content-Type: a ...
最新文章
- xdebug模块输出文件名的配置说明
- 【Flutter】Flutter 混合开发 ( Flutter 与 Native 通信 | 通信场景 | Channel 通信机制 | Channel 支持的通信数据类型 | Channel 类型 )
- 自下而上滚动公告栏(可悬停)
- 蓝桥杯2017初赛-外星日历-数论
- OnLongClickListener长按事件设置墙纸
- 08 友盟项目--拆分日志为五个表---UDTF自定义函数
- MySQL高可用实现:主从结构下ProxySQL中的读写分离
- HTTP Error 502.5 - Process Failure 解决方案
- 如何告别“芯繁魂乱”难题?这个 OS 来支招!
- 流程图伪代码计算机语言,论文中伪代码怎么写
- 10.5 欧拉通路与哈密顿通路
- 我的IT之路2011(一)
- 小黄鸡 php,小黄鸡simsimi接口
- ArcGIS创建tpk切片缓存
- Boost:宏BOOST_LOG_TRIVIAL的测试
- mac pro词典无法使用问题
- python安装菜鸟教程_Python菜鸟教程 | 多平台安装
- Mesosphere DCOS快速部署手册
- Git提交前端代码遇到Lint errors found in the listed files.和husky - pre-push hook failed (add --no-verify)
- 人脸活体检测、红外人脸数据集下载
热门文章
- 苹果授权登陆 服务端验证(java)
- sinoces 2013 消费电子
- LaTeX初学者模板 控制纸张大小
- 计算机属性里面用户变量没有path怎么办,把系统变量path删了怎么办
- 字符识别系统仿真–Matlab GUI实现
- 【jumpserver升级】docker pulling image报错dial tcp 104.18.124.25:443: i/o timeout
- 2022081班李亚楠20220919
- overleaf 插入图片_Overleaf—使用教程-致谢原作者
- Canny的C++实现
- ajp协议: httpd 代理 tomcat集群