写在前面
本文看下如何对已有类进行插装。以最经典的方法执行耗时
作为例子。
1:编码
假定有如下的代码:
public class MyMethod {
public String queryUserInfo(String uid) {
System.out.println("xxxx");
System.out.println("xxxx1");
System.out.println("xxxx2");
System.out.println("xxxx3");
return uid;
}
}
动态插装生成如下的代码:
public String queryUserInfo(String uid) {
long var2 = System.nanoTime();
System.out.println("xxxx");
System.out.println("xxxx1");
System.out.println("xxxx2");
System.out.println("xxxx3");
System.out.println("方法执行耗时(纳秒)->queryUserInfo:" + (System.nanoTime() - var2));
return uid;
}
插装代码如下:
import org.objectweb.asm.*;
import org.objectweb.asm.commons.AdviceAdapter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import static org.objectweb.asm.Opcodes.ASM5;
public class TestMonitor extends ClassLoader {
public static void main(String[] args) throws IOException, NoSuchMethodException, IllegalAccessException, InstantiationException, InvocationTargetException {
// 读取需要插装的类
ClassReader cr = new ClassReader(MyMethod.class.getName());
// ClassReader cr = new ClassReader("com.dahuyou.asm.methodWasteTime.MyMethod11");
ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_MAXS);
// 为什么要重新生成构造函数???
{
MethodVisitor methodVisitor = cw.visitMethod(Opcodes.ACC_PUBLIC, "<init>", "()V", null, null);
methodVisitor.visitVarInsn(Opcodes.ALOAD, 0);
methodVisitor.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
methodVisitor.visitInsn(Opcodes.RETURN);
methodVisitor.visitMaxs(1, 1);
methodVisitor.visitEnd();
}
ClassVisitor cv = new ProfilingClassAdapter(cw, MyMethod.class.getSimpleName());
cr.accept(cv, ClassReader.EXPAND_FRAMES);
byte[] bytes = cw.toByteArray();
Class<?> clazz = new TestMonitor().defineClass("com.dahuyou.asm.methodWasteTime.MyMethod", bytes, 0, bytes.length);
Method queryUserInfo = clazz.getMethod("queryUserInfo", String.class);
Object obj = queryUserInfo.invoke(clazz.newInstance(), "10001");
System.out.println("测试结果:" + obj);
// 写到class文件看以下,对于完成插装没有实际作用
outputClazz(bytes);
}
static class ProfilingClassAdapter extends ClassVisitor {
public ProfilingClassAdapter(final ClassVisitor cv, String innerClassName) {
super(ASM5, cv);
}
// asm在生成字节码的时候每个方法都会调用一次这个方法,让我们插装
public MethodVisitor visitMethod(int access,
String name,
String desc,
String signature,
String[] exceptions) {
System.out.println("access:" + access);
System.out.println("name:" + name);
System.out.println("desc:" + desc);
// 因为只插装queryUserInfo,方法所以其他方法直接返回null,保持不动了
if (!"queryUserInfo".equals(name)) return null;
MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
// org.objectweb.asm.commons.AdviceAdapter子类,onMethodEnter方法开始前调用,onMethodExit方法执行结束时调用
return new ProfilingMethodVisitor(mv, access, name, desc);
}
}
static class ProfilingMethodVisitor extends AdviceAdapter {
private String methodName = "";
protected ProfilingMethodVisitor(MethodVisitor methodVisitor, int access, String name, String descriptor) {
super(ASM5, methodVisitor, access, name, descriptor);
this.methodName = name;
}
/*
方法前生成代码 long var2 = System.nanoTime();
*/
@Override
protected void onMethodEnter() {
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "nanoTime", "()J", false);
mv.visitVarInsn(LSTORE, 2);
mv.visitVarInsn(ALOAD, 1);
}
/*
方法后生成代码System.out.println("方法执行耗时(纳秒)->queryUserInfo:" + (System.nanoTime() - var2));
*/
@Override
protected void onMethodExit(int opcode) {
if ((IRETURN <= opcode && opcode <= RETURN) || opcode == ATHROW) {
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitTypeInsn(NEW, "java/lang/StringBuilder");
mv.visitInsn(DUP);
mv.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);
mv.visitLdcInsn("方法执行耗时(纳秒)->" + methodName+":");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "nanoTime", "()J", false);
mv.visitVarInsn(LLOAD, 2);
mv.visitInsn(LSUB);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(J)Ljava/lang/StringBuilder;", false);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
}
}
}
private static void outputClazz(byte[] bytes) {
// 输出类字节码
FileOutputStream out = null;
try {
String pathName = TestMonitor.class.getResource("/").getPath() + "AsmTestMonitor.class";
out = new FileOutputStream(new File(pathName));
System.out.println("ASM类输出路径:" + pathName);
out.write(bytes);
} catch (Exception e) {
e.printStackTrace();
} finally {
if (null != out) try {
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
代码比较长,其中比较关键代码为:
ClassReader cr = new ClassReader(MyMethod.class.getName());
读取要插装增强的类准备插装
ClassVisitor cv = new ProfilingClassAdapter(cw, MyMethod.class.getSimpleName());
进行插装,具体是在com.dahuyou.asm.methodWasteTime.TestMonitor.ProfilingClassAdapter#visitMethod中返回自定义的methodvisitor实现插装
static class ProfilingMethodVisitor extends AdviceAdapter
methovisitor插装切面类,onMethodEnter方法插装方法执行前的逻辑,onMethodExit插装方法执行后的逻辑
byte[] bytes = cw.toByteArray();
这就拿到插装后的字节码了
运行测试:
生成的字节码如下: