本文我们重点分析一下,如何通过asm操作字节码,为既有的类动态增加功能,从而达到在字节码级别增强方法的功能。同样我们还是实例方式进行演示。
1.原有类的方法:
public class Shareniu { public void m() { System.out.println("now in Shareniu.m=="); try { Thread.sleep(1000L); } catch (InterruptedException e) { e.printStackTrace(); } } }
2.期望为类增加的方法:
public void m() { long a1 = System.currentTimeMillis(); System.out.println("now in Shareniu.m=="); try { Thread.sleep(1000L); } catch (InterruptedException e) { e.printStackTrace(); } long a2 = System.currentTimeMillis(); System.out.println("MyTimeLog now use time===="+(a2-a1)); }
我们的诉求是在m方法执行前,记录一下当前的时间,m方法执行完毕之后,记录一下当前的时间,两者时间相减,即为当前的方法执行的时间。ok,接下来就开始进行编码。
3.访问Shareniu类以及方法:
public class MyClassVisitor extends ClassVisitor implements Opcodes { public MyClassVisitor(ClassVisitor cv) { super(ASM5, cv); } @Override public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { cv.visit(version, access, name, signature, superName, interfaces); } @Override public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions); if (!name.equals("<init>") && mv!=null) { // 为方法增加计时的功能 mv=new MyMethodVisitor(mv); } return mv; } class MyMethodVisitor extends MethodVisitor implements Opcodes{ public MyMethodVisitor(MethodVisitor mv) { super(Opcodes.ASM5, mv); } @Override public void visitCode() { /*1.首先调用父类的,然后增加自身的处理逻辑 * 2.增加long a1 = System.currentTimeMillis(); * */ super.visitCode(); //获取到currentTimeMillis并压入到本地栈 mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J",false); mv.visitVarInsn(LSTORE, 1); } @Override public void visitInsn(int opcode) { if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN) || opcode == Opcodes.ATHROW) { //方法在返回之前,加入long a2 = System.currentTimeMillis(); //System.out.println("MyTimeLog now use time===="+(a2-a1)); mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J",false); mv.visitVarInsn(LSTORE, 3); mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"); mv.visitTypeInsn(NEW, "java/lang/StringBuilder"); mv.visitInsn(DUP); mv.visitLdcInsn("MyTimeLog now use time===="); mv.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "(Ljava/lang/String;)V", false); mv.visitVarInsn(LLOAD, 3); mv.visitVarInsn(LLOAD, 1); 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); } mv.visitInsn(opcode); } }
4.读取类并生成新的字节码:
public class Generator { public static void main(String[] args) throws Exception{ ClassReader cr = new ClassReader("com/shareniu/asm/aop/Shareniu"); ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS); ClassVisitor cv = new MyClassVisitor(cw); cr.accept(cv,ClassReader.SKIP_DEBUG); byte[] data = cw.toByteArray(); //输出 File f = new File("bin/com/shareniu/asm/aop/Shareniu.class"); FileOutputStream fout = new FileOutputStream(f); fout.write(data); fout.close(); System.out.println("now generator cc success!!!!!"); } }
5.测试新的类:
public class MyTest { public static void main(String[] args) throws InterruptedException { Shareniu c = new Shareniu(); c.m(); } }
不出意外的话,控制台的错误信息如下:
Exception in thread "main" java.lang.VerifyError: Bad local variable type Exception Details: Location: com/shareniu/asm/aop/Shareniu.m()V @43: lload_1 Reason: Type top (current frame, locals[1]) is not assignable to long Current Frame: bci: @43 flags: { } locals: { 'com/shareniu/asm/aop/Shareniu', top, top, long, long_2nd } stack: { 'java/io/PrintStream', 'java/lang/StringBuilder', long, long_2nd } Bytecode: 0x0000000: b800 0f40 b200 1512 17b6 001d 1400 1eb8 0x0000010: 0025 a700 084c 2bb6 0028 b800 0f42 b200 0x0000020: 15bb 002a 5912 2cb7 002e 211f 65b6 0032 0x0000030: b600 36b6 001d b1 Exception Handler Table: bci [12, 18] => handler: 21 Stackmap Table: same_locals_1_stack_item_frame(@21,Object[#17]) same_frame(@26) at com.shareniu.asm.aop.MyTest.main(MyTest.java:5)
为什么呢?我们不妨分析一下:
因为我们增加了
long a1 = System.currentTimeMillis();以及
long a2 = System.currentTimeMillis();
这个时候,方法的局部变量表以及本地栈帧我们并没有去更新。所以会导致这个问题,当然了实际开发中,我们忌讳直接在类的方法中增加变量的操作,为什么呢?
1.增加的变量不确定,我们需要修改很多的代码,比如之前的变量在3的位置,如果我们加入了一个变量,可能要将他修改为4或者5,如果方法中变量非常的多,这种方法是不可取的。
2.我们侵入了太多的字节码指定,代码维护起来不方便,不通用。
3.代码耦合度高。
最好的办法就是单独的定义一个类,在启动增强的方法中,直接调用单独的类即可。所以接下来的工作就是单独定义一个类。
6.定义打印类:
public class MyTimeLog { private static long a1; public static void start(){ a1 = System.currentTimeMillis(); } public static void end(){ long a2 = System.currentTimeMillis(); System.out.println("MyTimeLog now use time===="+(a2-a1)); } }
最注意:目前暂时定义一个静态方法调用,后续我们学习到栈的相关知识后,再来尝试使用实例方法的方式进行操作,因为实例方法的调用,相对而言稍微复杂一点而且还是会遇到上述的问题。
7.新的访问方法代码:
class MyMethodVisitor extends MethodVisitor implements Opcodes{ public MyMethodVisitor(MethodVisitor mv) { super(Opcodes.ASM5, mv); } @Override public void visitCode() { super.visitCode(); mv.visitMethodInsn(INVOKESTATIC, "com/shareniu/asm/aop/MyTimeLog", "start", "()V", false); } @Override public void visitInsn(int opcode) { if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN) || opcode == Opcodes.ATHROW) { mv.visitMethodInsn(INVOKESTATIC, "com/shareniu/asm/aop/MyTimeLog", "end", "()V", false); } mv.visitInsn(opcode); } }
8.测试:
同样运行步骤5,控制台的输出如下:
now in Shareniu.m==
MyTimeLog now use time====2000
我们已经成功通过asm实现了一个简单的aop功能。
转载请注明:分享牛 » 使用asm实现aop功能