前面我们了解了如何使用ASM的CoreAPI来操作一个类的属性,现在我们来看一下如何修改一个类方法。
场景:假设我们有一个Person类,它当中有一个sleep方法,我们希望监控一下这个sleep方法的运行时间:
一般我们会在代码里这样写:
public void sleep() {
long timer = System.currentTimeMillis();
try {
System.out.println("我要睡一会...");
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(System.currentTimeMillis()-timer);
}
标红的两行代码是我们希望有的,但是一般不会将这样的代码和业务代码耦合在一起,所以借助asm来实现动态的植入这样两行代码,就可以使业务方法很清晰。因此我们需要能够修改方法的API,在ASM中提供了对应的API,即MethodAdapter,使用这个API我们就可以随心所欲的修改方法中的字节码,甚至可以完全重写方法,当然这样是没有必要的。下面我们来看一下如何使用这个API,代码如下:
public class ModifyMethod extends MethodAdapter {
public ModifyMethod(MethodVisitor mv, int access, String name, String desc) {
super(mv);
}
@Override
public void visitCode() {
mv.visitFieldInsn(Opcodes.GETSTATIC,
Type.getInternalName(Person.class), "timer", "J");
mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System",
"currentTimeMillis", "()J");
mv.visitInsn(Opcodes.LSUB);
mv.visitFieldInsn(Opcodes.PUTSTATIC,
Type.getInternalName(Person.class), "timer", "J");
}
@Override
public void visitInsn(int opcode) {
if (opcode == Opcodes.RETURN) {
mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out",
"Ljava/io/PrintStream;");
mv.visitFieldInsn(Opcodes.GETSTATIC,
Type.getInternalName(Person.class), "timer", "J");
mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System",
"currentTimeMillis", "()J");
mv.visitInsn(Opcodes.LADD);
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream",
"println", "(J)V");
}
mv.visitInsn(opcode);
}
}
MethodAdapter类实现了MethodVisitor接口,在MethodVisitor接口中严格地规定了每个visitXXX的访问顺序,如下:
visitAnnotationDefault?( visitAnnotation | visitParameterAnnotation | visitAttribute )*( visitCode
( visitTryCatchBlock | visitLabel | visitFrame | visitXxxInsn |visitLocalVariable | visitLineNumber )*visitMaxs )?visitEnd
首先,统一一个概念,ASM访问,这里所说的ASM访问不是指ASM代码去调用某个类的具体方法,而是指去分析某个类的某个方法的二进制字节码。
在这里visitCode方法将会在ASM开始访问某一个方法时调用,因此这个方法一般可以用来在进入分析JVM字节码之前来新增一些字节码,visitXxxInsn是在ASM具体访问到每个指令时被调用,上面代码中我们使用的是visitInsn方法,它是ASM访问到无参数指令时调用的,这里我们判但了当前指令是否为无参数的return来在方法结束前添加一些指令。
通过重写visitCode和visitInsn两个方法,我们就实现了具体的业务逻辑被调用前和被调用后植入监控运行时间的代码。
ModifyMethod类只是对方法的修改类,那如何让外部类调用它,要通过我们上一篇中使用过的类,ClassAdapter的一个子类,在这里我们定义一个ModifyMethodClassAdapter类,代码如下:
public class ModifyMethodClassAdapter extends ClassAdapter {
public ModifyMethodClassAdapter(ClassVisitor cv) {
super(cv);
}
@Override
public MethodVisitor visitMethod(int access, String name, String desc,
String signature, String[] exceptions) {
if (name.equals("sleep")) {
return new ModifyMethod(super.visitMethod(access, name, desc,
signature, exceptions), access, name, desc);
}
return super.visitMethod(access, name, desc, signature, exceptions);
}
@Override
public void visitEnd() {
cv.visitField(Opcodes.ACC_PRIVATE + Opcodes.ACC_STATIC, "timer", "J",
null, null);
}
}
上述代码中我们使用visitEnd来添加了一个timer属性,用于记录时间,我们重写了visitMethod方法,当ASM访问的方法是sleep方法时,我们调用已经定义的ModifyMethod方法,让这个方法作为访问者,去访问对应的方法。
这样两个类就实现了我们要的添加执行时间的业务。
看一下测试类:
public class ModifyMethodTest {
@Test
public void modiySleepMethod() throws Exception {
ClassReader classReader = new ClassReader(
"org.victorzhzh.common.Person");
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
ClassAdapter classAdapter = new ModifyMethodClassAdapter(classWriter);
classReader.accept(classAdapter, ClassReader.SKIP_DEBUG);
byte[] classFile = classWriter.toByteArray();
GeneratorClassLoader classLoader = new GeneratorClassLoader();
@SuppressWarnings("rawtypes")
Class clazz = classLoader.defineClassFromClassFile(
"org.victorzhzh.common.Person", classFile);
Object obj = clazz.newInstance();
System.out.println(clazz.getDeclaredField("name").get(obj));
clazz.getDeclaredMethod("sleep").invoke(obj);
}
}
通过反射机制调用我们修改后的Person类,运行结果如下:
zhangzhuo
我要睡一会...
2023
2023就是我们让sleep方法沉睡的时间,看一下我们的原始Person类:
public class Person {
public String name = "zhangzhuo";
public void sayHello() {
System.out.println("Hello World!");
}
public void sleep() {
try {
System.out.println("我要睡一会...");
TimeUnit.SECONDS.sleep(2);//沉睡两秒
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
以上几篇文章都是关于ASM的大体介绍,ASM的功能可以说是十分强大,要学好这个东西个人有几点体会:
第一、要熟悉Java字节码结构,及指令:因为我们在很多时候都是要写最原始的字节吗指令的,虽然ASM也为我们提供相应的简化API替我们来做这些事情,但是最基本的东西还是要了解和掌握的,这样才能使用的更好;
第二、充分理解访问者模式有助于我们理解ASM的CoreAPI;
第三、掌握基本的ClassVisitor、ClassAdapter、MethodVisitor、MethodAdapter、FieldVisitor、FieldWriter、ClassReader和ClassWriter这几个类对全面掌握CoreAPI可以有很大的帮助;
第四、在掌握了CoreAPI后再去研究TreeAPI,这样更快速;
最后,希望这几篇文章能对研究ASM的朋友有所帮助