ASM是一套java字节码分析/生成/修改的工具,它能够在java程序运行时直接修改java字节码文件,换句话说它能够直接修改java的二进制文件;也能够跳过编译直接生成字节码文件。所以ASM功能非常强大,对于代码性能提升、代码问题定位都非常有帮助。lambda表达式的底层就是依靠ASM实现的,掌握这套java底层工具是成为java高级程序员的必经之路。
以下是官网对ASM的概述(ASM):
ASM is an all purpose Java bytecode manipulation and analysis framework. It can be used to modify existing classes or to dynamically generate classes, directly in binary form. ASM provides some common bytecode transformations and analysis algorithms from which custom complex transformations and code analysis tools can be built. ASM offers similar functionality as other Java bytecode frameworks, but is focused on performance. Because it was designed and implemented to be as small and as fast as possible, it is well suited for use in dynamic systems (but can of course be used in a static way too, e.g. in compilers).
大概意思是:ASM是全方位的java字节码控制和分析工具,能够在二进制文件的层面,直接修改已有的class文件或者动态生成class文件。
我这里要讲的利用ASM直接生成字节码的方法(无需经过编译步骤)
首先引入ASM的依赖:
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm</artifactId>
<version>9.2</version>
</dependency>
然后这里有两种方法来生成java字节码:
方法一: 直接通过程序代码来生成:
如下代码:这里实际上用ASMifier.main方法来生成字节码:
package com.JvmInvokeInstructions.asm;
import org.objectweb.asm.util.ASMifier;
import java.io.IOException;
public class AsmBase {
public void getInfo() {
System.out.println("This is info test case");
}
public static void main(String[] args) throws IOException {
// 通过ASMifier.main方法可以直接生成ASM风格的字节码
ASMifier.main(new String[]{"com.JvmInvokeInstructions.asm.AsmBase"});
}
}
看console的输出:
重点看dump()方法,这个方法返回值是byte[]字节数组,这个byte[]的内容就是java文件AsmBase的字节码。换句话说如果把这个方法的返回值保存到一个文件,那么这个文件和AsmBase.class文件是完全等同的。
为了验证这个结论,我把上面输出的dump方法复制出来,然后自己写一个类加载器来加载这个dump方法返回的字节数组,从而生成类对象,然后调用该类的方法,看看输出结果。
代码如下:
public class AsmVerifyCase1 implements Opcodes {
public static void main(String[] args) throws Exception {
byte[] codes=dump();
Class<?> clazz=new MyClassLoader().defineClass("com.JvmInvokeInstructions.asm.AsmBase", codes);
clazz.getMethod("getInfo", null).invoke(clazz.newInstance(), new Object[]{});
}
private static class MyClassLoader extends ClassLoader implements Opcodes {
public Class<?> defineClass(String name, byte[] b){
return super.defineClass(name, b, 0, b.length);
}
}
public static byte[] dump () throws Exception {
ClassWriter classWriter = new ClassWriter(0);
FieldVisitor fieldVisitor;
RecordComponentVisitor recordComponentVisitor;
MethodVisitor methodVisitor;
AnnotationVisitor annotationVisitor0;
classWriter.visit(V1_8, ACC_PUBLIC | ACC_SUPER, "com/JvmInvokeInstructions/asm/AsmBase", null, "java/lang/Object", null);
classWriter.visitSource("AsmBase.java", null);
{
methodVisitor = classWriter.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
methodVisitor.visitCode();
Label label0 = new Label();
methodVisitor.visitLabel(label0);
methodVisitor.visitLineNumber(6, label0);
methodVisitor.visitVarInsn(ALOAD, 0);
methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
methodVisitor.visitInsn(RETURN);
Label label1 = new Label();
methodVisitor.visitLabel(label1);
methodVisitor.visitLocalVariable("this", "Lcom/JvmInvokeInstructions/asm/AsmBase;", null, label0, label1, 0);
methodVisitor.visitMaxs(1, 1);
methodVisitor.visitEnd();
}
{
methodVisitor = classWriter.visitMethod(ACC_PUBLIC, "getInfo", "()V", null, null);
methodVisitor.visitCode();
Label label0 = new Label();
methodVisitor.visitLabel(label0);
methodVisitor.visitLineNumber(8, label0);
methodVisitor.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
methodVisitor.visitLdcInsn("This is info test case");
methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
Label label1 = new Label();
methodVisitor.visitLabel(label1);
methodVisitor.visitLineNumber(9, label1);
methodVisitor.visitInsn(RETURN);
Label label2 = new Label();
methodVisitor.visitLabel(label2);
methodVisitor.visitLocalVariable("this", "Lcom/JvmInvokeInstructions/asm/AsmBase;", null, label0, label2, 0);
methodVisitor.visitMaxs(2, 1);
methodVisitor.visitEnd();
}
{
methodVisitor = classWriter.visitMethod(ACC_PUBLIC | ACC_STATIC, "main", "([Ljava/lang/String;)V", null, new String[] { "java/io/IOException" });
methodVisitor.visitCode();
Label label0 = new Label();
methodVisitor.visitLabel(label0);
methodVisitor.visitLineNumber(12, label0);
methodVisitor.visitInsn(ICONST_1);
methodVisitor.visitTypeInsn(ANEWARRAY, "java/lang/String");
methodVisitor.visitInsn(DUP);
methodVisitor.visitInsn(ICONST_0);
methodVisitor.visitLdcInsn("com.JvmInvokeInstructions.asm.AsmBase");
methodVisitor.visitInsn(AASTORE);
methodVisitor.visitMethodInsn(INVOKESTATIC, "org/objectweb/asm/util/ASMifier", "main", "([Ljava/lang/String;)V", false);
Label label1 = new Label();
methodVisitor.visitLabel(label1);
methodVisitor.visitLineNumber(13, label1);
methodVisitor.visitInsn(RETURN);
Label label2 = new Label();
methodVisitor.visitLabel(label2);
methodVisitor.visitLocalVariable("args", "[Ljava/lang/String;", null, label0, label2, 0);
methodVisitor.visitMaxs(4, 1);
methodVisitor.visitEnd();
}
classWriter.visitEnd();
return classWriter.toByteArray();
}
}
可以看到程序是可以正常运行输出的,跟写普通java程序的输出结果是一样的,只不过ASM是能直接生成.class字节码文件,无需二次编译:
方法二:通过命令行来生成ASM字节码风格代码:
这里实际上通过ASM分析已有的.class文件然后生成ASM字节码风格的代码
首先需要把asm的jar包放到要分析的目标class文件所在的目录(target/classes/类的全路径):
然后运行下面的命令:
java -classpath "asm-util-9.2.jar;asm-9.2.jar" org.objectweb.asm.util.ASMifier AsmBase.class
java -classpath "asm-util-9.2.jar;asm-9.2.jar" org.objectweb.asm.util.ASMifier AsmBase.class
注意classpath里面jar包的分隔符是分号;
然后会出现跟方法一同样的ASM字节码风格的代码:
跟前面一样,将生成的代码复制出来(特别是dump方法),然后用自定义类加载器加载该方法返回的字节数组,就可以生成对应的类对象,调用类的方法,但是这个是直接生成字节码,无需二次编译。