盘古BPM体验地址    盘古BPM交流群盘古BPM交流群号:963222735

使用asm实现aop功能

分享牛 3039℃

本文我们重点分析一下,如何通过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功能