通过 ASM 库生成和修改 class 文件
在 JVM中 Class 文件分析 主要详细讲解了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 库的介绍和使用
ASM 库是一款基于 Java 字节码层面的代码分析和修改工具,那 ASM 和访问者模式有什么关系呢?访问者模式主要用于修改和操作一些数据结构比较稳定的数据,通过前面的学习,我们知道 .class 文件的结构是固定的,主要有常量池、字段表、方法表、属性表等内容,通过使用访问者模式在扫描 .class 文件中各个表的内容时,就可以修改这些内容了。在学习 ASM 之前,可以通过深入浅出访问者模式 这篇文章学习一下访问者模式。
ASM 可以直接生产二进制的 .class 文件,也可以在类被加载入 JVM 之前动态修改类行为。下文将通过两个例子,分别介绍如何生成一个 class 文件和修改 Java 类中方法的字节码。
在刚开始使用的时候,可能对字节码的执行不是很清楚,使用 ASM 会比较困难,ASM 官方也提供了一个帮助工具 ASMifier,我们可以先写出目标代码,然后通过 javac 编译成 .class 文件,然后通过 ASMifier 分析此 .class 文件就可以得到需要插入的代码对应的 ASM 代码了。
ASM 生成 class 文件
下面简单看一个 java 类:
package work; public class Example {public static void main(String[] var0) { System.out.println("createExampleClass"); } }
这个 Example 类很简单,只有简单的包名,加上一个静态 main 方法,打印输出 createExampleClass 。
现在问题来了,你如何生成这个 Example.java 的 class 文件,不能在开发时通过上面的源码来编译成, 而是要动态生成。
下面开始介绍如何使用 ASM 动态生成上述源码对应的字节码。
代码示例
public class Main extends ClassLoader { // 此处记得替换成自己的文件地址 public static final String PATH = "/Users/xxx/IdeaProjects/untitled/src/work/"; public static void main(String[] args) { createExampleClass(); } private static void createExampleClass() { ClassWriter cw = new ClassWriter(0); // 定义一个叫做Example的类,并且这个类是在 work 目录下面 cw.visit(V1_8, ACC_PUBLIC + ACC_SUPER, "work/Example", null, "java/lang/Object", null); // 生成默认的构造方法 MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null); // 生成构造方法的字节码指令 mv.visitVarInsn(ALOAD, 0); mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false); mv.visitInsn(RETURN); mv.visitMaxs(1, 1); // 构造函数访问结束 mv.visitEnd(); // 生成main方法中的字节码指令 mv = cw.visitMethod(ACC_PUBLIC + ACC_STATIC, "main", "([Ljava/lang/String;)V", null, null); mv.visitCode(); // 获取该方法 mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"); // 加载字符串参数 mv.visitLdcInsn("createExampleClass"); // 调用该方法 mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false); mv.visitInsn(RETURN); mv.visitMaxs(2, 1); mv.visitEnd(); // 获取生成的class文件对应的二进制流 byte[] code = cw.toByteArray(); // 将二进制流写到本地磁盘上 FileOutputStream fos = null; try { fos = new FileOutputStream(PATH + "Example.class"); fos.write(code); System.out.println(fos.getFD()); fos.close(); } catch (Exception e) { System.out.print(" FileOutputStream error " + e.getMessage()); e.printStackTrace(); } loadclass("Example.class", "work.Example"); } private static void loadclass(String className, String packageNamePath) { //通过反射调用main方法 MyClassLoader myClassLoader = new MyClassLoader(PATH + className); // 类的全称,对应包名 try { // 加载class文件 Class<?> Log = myClassLoader.loadClass(packageNamePath); System.out.println("类加载器是:" + Log.getClassLoader()); // 利用反射获取main方法 Method method = Log.getDeclaredMethod("main", String[].class); String[] arg = {"ad"}; method.invoke(null, (Object) arg); } catch (Exception e) { e.printStackTrace(); } } }
为了证明表示我们生成的 class 可以正常调用,还需要将其加载,然后通过反射调用该类的方法,这样才能说明生成的 class 文件是没有问题并且可运行的。
下面是自定义的一个 class 加载类:
public class MyClassLoader extends ClassLoader { // 指定路径 private String path; public MyClassLoader(String classPath) { path = classPath; } /** * 重写findClass方法 * * @param name 是我们这个类的全路径 * @return * @throws ClassNotFoundException */ @Override protected Class<?> findClass(String name) throws ClassNotFoundException { Class log = null; // 获取该class文件字节码数组 byte[] classData = getData(); if (classData != null) { // 将class的字节码数组转换成Class类的实例 log = defineClass(name, classData, 0, classData.length); } return log; } /** * 将class文件转化为字节码数组 * * @return */ private byte[] getData() { File file = new File(path); if (file.exists()) { FileInputStream in = null; ByteArrayOutputStream out = null; try { in = new FileInputStream(file); out = new ByteArrayOutputStream(); byte[] buffer = new byte[1024]; int size = 0; while ((size = in.read(buffer)) != -1) { out.write(buffer, 0, size); } } catch (IOException e) { e.printStackTrace(); } finally { try { in.close(); } catch (IOException e) { e.printStackTrace(); } } return out.toByteArray(); } else { return null; } } }
代码详解
下面详细介绍生成class的过程:
首先定义一个类
ClassWriter cw = new ClassWriter(0); // 定义一个叫做Example的类,并且这个类是在 work 目录下面 cw.visit(V1_8, ACC_PUBLIC + ACC_SUPER, "work/Example", null, "java/lang/Object", null);
ClassWriter 类是 ASM 中的核心 API , 用于生成一个类的字节码。 ClassWriter 的 visit 方法定义一个类。
-
第一个参数 V1_8 是生成的 class 的版本号, 对应class文件中的主版本号和次版本号, 即 minor_version 和 major_version 。
-
第二个参数ACC_PUBLIC表示该类的访问标识。这是一个public的类。 对应class文件中的access_flags 。
-
第三个参数是生成的类的类名。 需要注意,这里是类的全限定名。 如果生成的class带有包名, 如com.jg.xxx.Example, 那么这里传入的参数必须是com/jg/xxx/Example 。对应 class 文件中的 this_class 。
-
第四个参数是和泛型相关的, 这里我们不关新, 传入null表示这不是一个泛型类。这个参数对应class文件中的Signature属性(attribute) 。
-
第五个参数是当前类的父类的全限定名。 该类直接继承Object。 这个参数对应class文件中的super_class 。
-
第六个参数是 String[] 类型的, 传入当前要生成的类的直接实现的接口。 这里这个类没实现任何接口, 所以传入null 。 这个参数对应class文件中的interfaces 。
定义默认构造方法, 并生成默认构造方法的字节码指令
// 生成默认的构造方法 MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null); // 生成构造方法的字节码指令 mv.visitVarInsn(ALOAD, 0); mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false); mv.visitInsn(RETURN); mv.visitMaxs(1, 1); // 构造函数访问结束 mv.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 属性。
-
调用 visitVarInsn 方法,生成 aload 指令, 将第 0 个本地变量(也就是 this)压入操作数栈。
-
调用 visitMethodInsn方法, 生成 invokespecial 指令, 调用父类(也就是 Object)的构造方法。
-
调用 visitInsn 方法,生成 return 指令, 方法返回。
-
调用 visitMaxs 方法, 指定当前要生成的方法的最大局部变量和最大操作数栈。 对应 Code 属性中的 max_stack 和 max_locals 。
-
最后调用 visitEnd 方法, 表示当前要生成的构造方法已经创建完成。
定义main方法, 并生成main方法中的字节码指令
这里与构造函数一样,就不多说了。
生成class数据, 保存到磁盘中, 加载class数据
// 获取生成的class文件对应的二进制流 byte[] code = cw.toByteArray(); // 将二进制流写到本地磁盘上 FileOutputStream fos = null; try { fos = new FileOutputStream(PATH + "Example.class"); fos.write(code); fos.close(); } catch (Exception e) { System.out.print(" FileOutputStream error " + e.getMessage()); e.printStackTrace(); } loadclass("Example.class", "work.Example");
这段代码执行完, 可以看到控制台有以下输出:
生成 ASM 代码
那么还有个问题是前面的 ASM 代码是如何生成的呢?
还是以前文提到的 EXample.java 为例:
javac Example.java // 生成 Example class 文件 java -classpath asm-all-6.0_ALPHA.jar org.objectweb.asm.util.ASMifier Example.class // 利用 ASMifier 将class 文件转为 asm 代码
在 Terminal 窗口中输入这两个命令,就可以得到下面的 asm 代码:
import java.util.*; import org.objectweb.asm.*; public class ExampleDump implements Opcodes { public static byte[] dump () throws Exception { ClassWriter cw = new ClassWriter(0); FieldVisitor fv; MethodVisitor mv; AnnotationVisitor av0; cw.visit(V1_8, ACC_PUBLIC + ACC_SUPER, "Example", null, "java/lang/Object", null); { mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null); mv.visitCode(); mv.visitVarInsn(ALOAD, 0); mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false); mv.visitInsn(RETURN); mv.visitMaxs(1, 1); mv.visitEnd(); } { mv = cw.visitMethod(ACC_PUBLIC + ACC_STATIC, "main", "([Ljava/lang/String;)V", null, null); mv.visitCode(); mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"); mv.visitLdcInsn("createExampleClass"); mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false); mv.visitInsn(RETURN); mv.visitMaxs(2, 1); mv.visitEnd(); } cw.visitEnd(); return cw.toByteArray(); } }
可以看到输出结果与前面的生成的 class 文件的代码是一样的。
到这里,相信你对 ASM 的使用已经有了初步的了解了,当然可能不是很熟悉,但是多写写练练掌握格式就好多了。
利用 ASM 修改方法
下面介绍如何修改一个 class 文件的方法。
还是在原来的代码基础上,Main 类下面新增一个方法 modifyMethod 方法,具体代码如下:
private static void modifyMethod() { byte[] code = null; try { // 需要注意把 . 变成 /, 比如 com.example.a.class 变成 com/example/a.class InputStream inputStream = new FileInputStream(PATH + "Example.class"); ClassReader reader = new ClassReader(inputStream); // 1. 创建 ClassReader 读入 .class 文件到内存中 ClassWriter writer = new ClassWriter(reader, ClassWriter.COMPUTE_MAXS); // 2. 创建 ClassWriter 对象,将操作之后的字节码的字节数组回写 ClassVisitor change = new ChangeVisitor(writer); // 3. 创建自定义的 ClassVisitor 对象 reader.accept(change, ClassReader.EXPAND_FRAMES); code = writer.toByteArray(); System.out.println(code); FileOutputStream fos = new FileOutputStream(PATH + "Example.class"); fos.write(code); fos.close(); } catch (Exception e) { System.out.println("FileInputStream " + e.getMessage()); e.printStackTrace(); } try { if (code != null) { System.out.println(code); FileOutputStream fos = new FileOutputStream(PATH + "Example.class"); fos.write(code); fos.close(); } } catch (Exception e) { System.out.println("FileOutputStream "); e.printStackTrace(); } loadclass("Example.class", "work.Example"); }
新建一个 adapter,继承自 AdviceAdapter,AdviceAdapter 本质也是一个 MethodVisitor,但是里面对很多对方法的操作逻辑进行了封装,使得我们不用关心 ASM 内部的访问逻辑,只需要在对应的方法下面添加代码逻辑即可。
public class ChangeAdapter extends AdviceAdapter { private String methodName = null; ChangeAdapter(int api, MethodVisitor mv, int access, String name, String desc) { super(api, mv, access, name, desc); methodName = name; } @Override protected void onMethodEnter() { super.onMethodEnter(); Label l0 = new Label(); Label l1 = new Label(); Label l2 = new Label(); mv.visitTryCatchBlock(l0, l1, l2, "java/lang/InterruptedException"); mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false); // 把当前的时间戳存起来 mv.visitVarInsn(LSTORE, 1); mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"); mv.visitLdcInsn("ChangeAdapter onMethodEnter "); mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false); mv.visitLabel(l0); mv.visitLdcInsn(new Long(100L)); mv.visitMethodInsn(INVOKESTATIC, "java/lang/Thread", "sleep", "(J)V", false); mv.visitLabel(l1); Label l3 = new Label(); mv.visitJumpInsn(GOTO, l3); mv.visitLabel(l2); mv.visitFrame(Opcodes.F_FULL, 2, new Object[] {"[Ljava/lang/String;", Opcodes.LONG}, 1, new Object[] {"java/lang/InterruptedException"}); mv.visitVarInsn(ASTORE, 3); mv.visitVarInsn(ALOAD, 3); mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/InterruptedException", "printStackTrace", "()V", false); mv.visitLabel(l3); mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null); mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false); // 把当前的时间戳存起来 mv.visitVarInsn(LSTORE, 3); } @Override protected void onMethodExit(int opcode) { super.onMethodExit(opcode); mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"); // 把之前存储的时间戳取出来 mv.visitVarInsn(LLOAD, 3); mv.visitVarInsn(LLOAD, 1); mv.visitInsn(LSUB); mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(J)V", false); } @Override public void visitMaxs(int i, int i1) { super.visitMaxs(i, i1); }
在 adapter 中,有两个非常重要的方法:
-
onMethodEnter:表示正在进入一个方法,在执行方法里的内容前会调用。因此,此处是对一个方法添加相关处理逻辑的很好的办法。
-
onMethodExit:表示正在退出一个方法,在执行 return 之前。如果一个方法存在返回值,只能再该方法添加静态方法。
public class ChangeVisitor extends ClassVisitor { ChangeVisitor(ClassVisitor classVisitor) { super(Opcodes.ASM5, classVisitor); } @Override public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { MethodVisitor methodVisitor = super.visitMethod(access, name, desc, signature, exceptions); System.out.print(name); if (name.equals("main")) { return new ChangeAdapter(Opcodes.ASM4, methodVisitor, access, name, desc); } return methodVisitor; } }
ChangeVisitor 主要就是对 ASM 访问 class 文件方法的时候,做个拦截。如果发现方法名是 main,就让其走前面写好的 ChangeAdapter,这样,我们就可以改写 class 文件的方法了。
运行结果
可以看到输出结果,是 100 ms,成功的对 main 方法的耗时进行了计算。
如果方法带有返回值
前面修改的 main 是没有返回值的,那么如果存在返回值?这么写还合适吗?
如果你添加了非静态方法的调用,去看生成的 class 文件也许可能是对的,但是在调用的时候就会报错。示例如下:
protected void onMethodExit(int opcode) { mv.visitVarInsn(LLOAD, longT); mv.visitInsn(LSUB); mv.visitVarInsn(LSTORE, longT); mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"); mv.visitLdcInsn("work2 createExampleClass"); mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false); mv.visitVarInsn(LLOAD, longT); }
这里是调用了一些非静态方法,接下去看生成的 class 文件:
从class 文件来看,生成的 class 文件是没有问题的,结果在反射调用的时候报了异常:
通过 javap -c Example.class 将反编译结果输出如下:
$ javap -c Example.class public class work2.Example { public work2.Example(); Code: 0: aload_0 1: invokespecial #8 // Method java/lang/Object."<init>":()V 4: return public long computer(); Code: 0: ldc2_w #29 // long 32423l 3: lstore_1 4: getstatic #16 // Field java/lang/System.out:Ljava/io/PrintStream; 7: ldc #18 // String work2 createExampleClass 9: invokevirtual #24 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 12: invokestatic #27 // Method java/lang/System.currentTimeMillis:()J 15: lstore_2 16: invokestatic #27 // Method java/lang/System.currentTimeMillis:()J 19: lstore 4 21: lload 4 23: lload_2 24: lsub 25: lstore 6 27: lload 6 29: lload_1 30: lsub 31: lstore_1 32: getstatic #16 // Field java/lang/System.out:Ljava/io/PrintStream; 35: ldc #18 // String work2 createExampleClass 37: invokevirtual #24 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 40: lload_1 41: lreturn }
下面的是修改前的带有返回值的反编译结果:
$ javap -c Example.class public class work2.Example { public work2.Example(); Code: 0: aload_0 1: invokespecial #8 // Method java/lang/Object."<init>":()V 4: return public long computer(); Code: 0: ldc2_w #29 // long 32423l 3: lstore_1 4: getstatic #16 // Field java/lang/System.out:Ljava/io/PrintStream; 7: ldc #18 // String work2 createExampleClass 9: invokevirtual #24 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 12: invokestatic #27 // Method java/lang/System.currentTimeMillis:()J 15: lstore_2 16: invokestatic #27 // Method java/lang/System.currentTimeMillis:()J 19: lstore 4 21: lload 4 23: lload_2 24: lsub 25: lstore 6 27: lload 6 29: lreturn }
可以发现 27 行前面的代码都是一样的,27 后面我们尝试修改 class 文件,同时替换返回值,但是最终还是失败了。这里原因我没有去寻找,应该就是我们的修改导致堆栈信息存在变化,从而导致校验失败。
如果我们实在需要对带有返回值的返回值进行修改,可以参考下面的实例,使用静态方法:
protected void onMethodExit(int opcode) { super.onMethodExit(opcode); mv.visitLdcInsn("main"); mv.visitMethodInsn(INVOKESTATIC, "work2/Main", "test", "(Ljava/lang/String;)V", false); mv.visitLdcInsn("ssss"); mv.visitMethodInsn(INVOKESTATIC, "work2/Main", "test", "(Ljava/lang/String;)V", false); }
可以从 INVOKESTATIC 关键字看出,这些都是静态方法。
到这里,关于 ASM 使用说明到这里就结束了。
源码已上传到 CSDN : ASM-demo.zip 。