用户工具

站点工具


about_asm

几种java asm字节码增强场景的解决方案

作者:陈科

联系方式:chenke@dumpcache.com

转载请说明出处:http://www.dumpcache.com/wiki/doku.php?id=about_asm

最近把asm的大多数功能点都使用了一下,我总结一下使用过程中的一些场景和解决方案

0.字节码调试方法

一般来讲字节码增强在aop的场景用的比较多,所以你可以能会在原先的class中插入一些java字节码,自己手写失误率是很高的,所以建议先把你要插入的功能写到一个测试类中,并且使用javap命令进行反编译参考样例再插入。

比如:

public class Example {
    public static String get() {
        System.out.println("Hello world");
        return "aaa";
    }
 
    public static void main(String args[]) {
        System.out.println(get());
    }
}

上面的Example反编译之后:

javap -c Example

结果成了:

Compiled from "Example.java"
public class Example {
  public Example();
    Code:
       0: aload_0       
       1: invokespecial #8                  // Method java/lang/Object."<init>":()V
       4: return        
 
  public static java.lang.String get();
    Code:
       0: getstatic     #16                 // Field java/lang/System.out:Ljava/io/PrintStream;
       3: ldc           #22                 // String Hello world
       5: invokevirtual #24                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       8: ldc           #30                 // String aaa
      10: areturn       
 
  public static void main(java.lang.String[]);
    Code:
       0: getstatic     #16                 // Field java/lang/System.out:Ljava/io/PrintStream;
       3: invokestatic  #34                 // Method get:()Ljava/lang/String;
       6: invokevirtual #24                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       9: return        
}

1.获取函数返回值

以上面的example为例,get方法返回为String,所以,在return前栈顶需要有数据,所以上面执行了:

8: ldc           #30                 // String aaa

当我增强代码的时候,建议使用:AdviceAdapter

但是当我们在调用:onMethodExit方法的时候,result结果已经压入栈中了,所以我在onMethodExit中再进行其他处理必然会出错。

我这里通过一个简单的栈操作来进行结果的打印:

        dup();
        visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
        dupX1();
        pop();
        visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/Object;)V",
                        false);

dup/pop等指令对栈的影响入下图:

2.异常捕获

假如get方法会抛出RuntimeException,我们想抓住异常做一些处理,那么就需要先保存函数返回值,否则插入的代码会在ldc之后,将会出现问题,为了做到这一点,我构建了一个Holder来保存返回值:

public class Holder {
 
    private static ThreadLocal<String> holder = new ThreadLocal<String>();
 
    public static void save(String str) {
        holder.set(str);
    }
 
    public static String load() {
        String str = holder.get();
        holder.remove();
        return str;
    }
 
}

最后,AdviceAdapter的代码如下:

public static class ModifierMethodWriter extends AdviceAdapter {
 
        // methodName to make sure adding try catch block for the specific
        // method.
        private String methodName;
 
        // below label variables are for adding try/catch blocks in instrumented
        // code.
        private Label  lTryBlockStart;
        private Label  lTryBlockEnd;
        private Label  lCatchBlockStart;
        private Label  lCatchBlockEnd;
 
        /**
         * constructor for accepting methodVisitor object and methodName
         * 
         * @param api: the ASM API version implemented by this visitor
         * @param mv: MethodVisitor obj
         * @param methodName : methodName to make sure adding try catch block
         *            for the specific method.
         */
        public ModifierMethodWriter(MethodVisitor mv, String className, int access, String name,
                                    String desc) {
            super(Opcodes.ASM4, mv, access, name, desc);
            this.methodName = name;
        }
 
        // We want to add try/catch block for the entire code in the method
        // so adding the try/catch when the method is started visiting the code.
        @Override
        public void onMethodEnter() {
 
//            // adding try/catch block only if the method is hello()
            if (methodName.equals("get")) {
                lTryBlockStart = new Label();
                lTryBlockEnd = new Label();
                lCatchBlockStart = new Label();
                lCatchBlockEnd = new Label();
 
                // set up try-catch block for RuntimeException
                visitTryCatchBlock(lTryBlockStart, lTryBlockEnd, lCatchBlockStart,
                        "java/lang/Exception");
 
                // started the try block
                visitLabel(lTryBlockStart);
            }
 
        }
 
        @Override
        public void onMethodExit(int opcode) {
 
            // closing the try block and opening the catch block if the method
            // is hello()
            if (methodName.equals("get")) {
                visitMethodInsn(INVOKESTATIC,
                        "Holder", "save",
                        "(Ljava/lang/String;)V", false);
                // closing the try block
                visitLabel(lTryBlockEnd);
 
                // when here, no exception was thrown, so skip exception handler
                visitJumpInsn(Opcodes.GOTO, lCatchBlockEnd);
 
                // exception handler starts here, with RuntimeException stored
                // on stack
                visitLabel(lCatchBlockStart);
 
                //mv.visitInsn(ATHROW);
                // store the RuntimeException in local variable
                visitVarInsn(Opcodes.ASTORE, 2);
 
                // here we could for example do e.printStackTrace()
                visitVarInsn(Opcodes.ALOAD, 2); // load it
                visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/Exception", "printStackTrace",
                        "()V", false);
                // exception handler ends here:
                visitLabel(lCatchBlockEnd);
                //visitVarInsn(Opcodes.ALOAD,1);
                visitMethodInsn(INVOKESTATIC,
                        "Holder", "load",
                        "()Ljava/lang/String;", false);
            }
        }
 
    }

3.嵌入自己的业务逻辑

为了尽量减少插入代码的影响,建议构建static方法来调用:

public class DefaultSampleCollector implements SampleCollector {
    private Map<String, CollectPolicy> policyMap = new ConcurrentHashMap<String, CollectPolicy>();
    private BlockingQueue<Sample>      queue;
 
    public DefaultSampleCollector(BlockingQueue<Sample> queue) {
 
    }
 
    @Override
    public void collect(String method, long startTime, long endTime, Object returnVal,
                        Object... params) {
        System.out.println(params[0]);
        System.out.println(startTime);
        //System.out.println(method);
 
    }

最后我们来看插入的逻辑:

public class JedisMethodVisitor extends AdviceAdapter {
    private static final String VOID_J              = "()J";
    private static final String CURRENT_TIME_MILLIS = "currentTimeMillis";
    private static final String JAVA_LANG_SYSTEM    = "java/lang/System";
    private int                 time;
    private String              name;
 
    public JedisMethodVisitor(MethodVisitor mv, String className, int access, String name,
                              String desc) {
        super(Opcodes.ASM4, mv, access, name, desc);
        this.name = name;
    }
 
    @Override
    protected void onMethodEnter() {
        time = this.newLocal(Type.LONG_TYPE);
        visitMethodInsn(Opcodes.INVOKESTATIC, JAVA_LANG_SYSTEM, CURRENT_TIME_MILLIS, VOID_J, false);
        this.visitVarInsn(Opcodes.LSTORE, time);
        //        visitMethodInsn(INVOKESTATIC,
        //                "com/kuaidadi/framework/fusion/monitor/SampleCollectorHolder", "collect", "()V",
        //                false);
 
    }
 
    @Override
    protected void onMethodExit(int opcode) {
        //        dup();
        //        visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
        //        dupX1();
        //        pop();
        //        visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/Object;)V",
        //                false);
        if (opcode == ARETURN) {
            dup();
            push(name);
            visitVarInsn(LLOAD, time);
            time = this.newLocal(Type.LONG_TYPE);
            visitMethodInsn(Opcodes.INVOKESTATIC, JAVA_LANG_SYSTEM, CURRENT_TIME_MILLIS, VOID_J,
                    false);
            this.visitVarInsn(Opcodes.LSTORE, time);
            visitVarInsn(LLOAD, time);
            loadArgArray();
            visitMethodInsn(INVOKESTATIC,
                    "com/kuaidadi/framework/fusion/monitor/SampleCollectorHolder", "collect",
                    "(Ljava/lang/Object;Ljava/lang/String;JJ[Ljava/lang/Object;)V", false);
 
        }
 
    }
    //        @Override
    //        public void visitMaxs(int maxStack, int maxLocals) {
    //            super.visitMaxs(maxStack + 4, maxLocals);
    //        }
}
about_asm.txt · 最后更改: 2018/10/14 15:31 (外部编辑)