ASM是java字节码操作领域公认的标准,被众多知名的开源框架使用,如cglib、mybatis,fastjson等。通过ASM提供的API,我们可以方便的修改类文件的字节码,并ASM会自动帮我们做很多事情,如维护常量池的索引、计算栈大小max_stack,局部变量表大小max_locals等。ASM提供了两种类型的API,基于事件触发的core api和基于对象的tree api,下面主要介绍基于事件触发的core api。
1. ASM core api介绍
core api是基于事件驱动的,其中最核心的三个类是ClassReader,ClassVisitor与ClassWriter。
ClassReader主要负责class文件字节码的读取与分析,在解析类文件的各个节点与阶段会触发相应的事件(如ClassVisitor,MethodVisitor),我们可以通过重写事件的回调方法来改写字节码。
ClassVisitor是一个抽象类,当需要读取或改写类文件的字节码时,我们需要继承该类,ClassReader的accept方法需要传入一个ClassVisitor对象,ClassReader在解析class文件的过程中遇到不同的节点时会调用ClassVisitor的不同的visitXXX方法,如下:
各事件回调函数的调用顺序,其中visit最先被调用,接着调用0次或1次visitSource;调用0次或1次visitOuterClass;接下来任意顺序调用任意多次visitAnnotation和visitAttribute(取决于类文件的结构);然后任意顺序调用任意多次visitInnerClass,visitField,visitMethod;最后调用visitEnd。上述visitXXX的过程中还可能触发一些子过程,如visitAnnotation会触发AnnotationVisitor,visitMethod会触发MethodVisitor,在这些visitXXX的过程中(包括ClassVisitor,AnnotationVisitor,MethodVisitor等),我们可以去修改各个子节点的字节码。完整的调用顺序图如下:
ClassWriter是ClassVisitor的一个实现类,ClassWriter的toByteArray方法可以返回修改后的字节码的byte数组。ClassReader,ClassVisitor,ClassWriter的关系如下:
2. ASM操作字节码示例
首先创建一个MyMain类,如下:
目标是修改test01方法,如下:
@SneakyThrows
@Test
public void testModifiedMethod() {
byte[] bytes = FileUtils.readFileToByteArray(new File(
"/Users/xycode/IdeaProjects/techlecture/techlecture/src/main/java/com/xycode/techlecture/asm/MyMain"
+ ".class"));
ClassReader classReader = new ClassReader(bytes);
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES);
ClassVisitor classVisitor = new ClassVisitor(Opcodes.ASM7, classWriter) {
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature,
String[] exceptions) {
//先删除test01()方法
if ("test01".equals(name)) {
return null;
}
return super.visitMethod(access, name, desc, signature, exceptions);
}
};
classReader.accept(classVisitor, 0);
byte[] removedMethodArray = classWriter.toByteArray();
classReader = new ClassReader(removedMethodArray);
//指定 自动计算操作数栈与局部变量表的大小
classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES);
classVisitor = new ClassVisitor(Opcodes.ASM7, classWriter) {
@Override
public void visitEnd() {
super.visitEnd();
//新增test01()方法
MethodVisitor methodVisitor = this.visitMethod(Opcodes.ACC_PUBLIC, "test01", "(I)I", null, null);
if (methodVisitor != null) {
methodVisitor.visitCode();
methodVisitor.visitVarInsn(Opcodes.ILOAD, 1); // iload_1
methodVisitor.visitVarInsn(Opcodes.BIPUSH, 100); // bipush 100
methodVisitor.visitInsn(Opcodes.IADD); // iadd
methodVisitor.visitInsn(Opcodes.IRETURN); // ireturn
//notice: 设置了COMPUTE_MAXS后, 这里需要手动触发自动计算, 填的参数值无所谓
methodVisitor.visitMaxs(-1, -1);
methodVisitor.visitEnd();
}
}
};
classReader.accept(classVisitor, 0);
byte[] modifiedMethodArray = classWriter.toByteArray();
FileUtils.writeByteArrayToFile(new File(
"/Users/xycode/IdeaProjects/techlecture/techlecture/src/test/java/com/xycode/techlecture/asm/MyMain5"
+ ".class"), modifiedMethodArray);
}
生成的MyMain5.class文件反编译如下:
使用javap命令查看MyMain5.class的字节码指令,如下:
可以看出,红框中,即test01方法的字节码和我们在代码中添加的指令是一致的。
3. javassist介绍与实践
如上所示,ASM框架的使用门槛是比较高的,需要熟悉底层的JVM字节码指令。而javassist是一个性能稍逊ASM,但使用门槛很低的字节码操作框架,因此在业界也得到了很多应用。javassist的核心api如下所示:
其中CtClass用来代表一个类对象,通过CtClass可以修改、新增类的方法(CtMethod)与字段(CtField),ClassPool是javassist定义的一个容器,用于存放CtClass。下面来演示如何使用javassist:
首先创建一个MyMain类,如下:
目标是新增一个foo方法,首先看一下CtMethod的构造器用法,如下:
下面使用CtMethod来为MyMain类添加foo方法:
ClassPool classPool = ClassPool.getDefault();
//添加类扫描路径
classPool.insertClassPath(new ClassClassPath(MyMain.class));
//加载MyMain类
CtClass ctClass = classPool.get(MyMain.class.getName());
CtMethod ctMethod = new CtMethod(CtClass.doubleType, "foo", new CtClass[] {CtClass.intType, CtClass.doubleType},
ctClass);
//$1代表取局部变量表index为1的值, 以此类推
ctMethod.setBody("return $1 * $2;");
ctClass.addMethod(ctMethod);
ctClass.writeFile("/Users/xycode/IdeaProjects/techlecture/techlecture/src/test/java/com/xycode/techlecture/javassist");
生成的MyMain类文件反编译如下:
可以看出使用javassit来操作字节门槛低了很多,下面调用该类文件中的foo方法,如下:
/**
* 自定义类加载器
*/
static class MyClassLoader extends ClassLoader {
public Class<?> findMyClass(String name, byte[] bytes) throws IOException {
return defineClass(name, bytes, 0, bytes.length);
}
}
@Test
public void testLoadClass() throws Exception{
MyClassLoader myClassLoader = new MyClassLoader();
byte[] bytes = FileUtils.readFileToByteArray(new File("/Users/xycode/IdeaProjects/techlecture/techlecture/src/test/java/com/xycode/techlecture/javassist/com/xycode/techlecture/asm/MyMain.class"));
Class<?> clazz = myClassLoader.findMyClass("com.xycode.techlecture.asm.MyMain", bytes);
Object o = clazz.newInstance();
Method foo = clazz.getMethod("foo", int.class, double.class);
System.out.println(foo.invoke(o, 123, 456.1));
}
执行结果:
javassit也可以方便地对类文件中已经存在的方法进行修改,如下:
//读取MyMain.class
CtClass ctClass = classPool.makeClass(FileUtils.openInputStream(new File(
"/Users/xycode/IdeaProjects/techlecture/techlecture/src/test/java/com/xycode/techlecture/javassist/com/xycode/techlecture/asm/MyMain.class")));
//获取foo方法(根据 方法名 + 方法签名)
CtMethod ctMethod = ctClass.getMethod("foo", "(ID)D");
//插入指定语句, javassit支持直接插入java代码, 而不仅仅是字节码指令, 这里的$_代表函数的返回值
ctMethod.insertAfter("System.out.println(\"foo(\"+$1+\",\"+$2+\")=\"+$_);");
ctClass.writeFile("/Users/xycode/IdeaProjects/techlecture/techlecture/src/test/java/com/xycode/techlecture/javassist");
生成的类文件反编译如下:
反射调用修改后的foo方法,如下: