ASM(四) 利用Method 組件動態注入方法邏輯

      這篇繼續結合例子來深入瞭解下Method組件動態變更方法字節碼的實現。通過前面一篇,知道ClassVisitor 的visitMethod()方法可以返回一個MethodVisitor的實例。那麼我們也基本可以知道,同ClassVisitor改變類成員一樣,MethodVIsistor如果需要改變方法成員,注入邏輯,也可以通過繼承MethodVisitor,來編寫一個MethodXXXAdapter來實現對於方法邏輯的注入。通過下面的兩個例子來介紹下無狀態注入和有狀態注入方法邏輯的實現。例子主要參考官方文檔介紹,大家按照這個思路可以擴展更多種場景的應用。

    一、無狀態注入

        先看一個例子,也是比較常見的一種場景,我們需要給下面這個類的所有方法注入一個計時的邏輯。

        源碼如下:

package asm.core.methord;

/**
 * Created by yunshen.ljy on 2015/6/29.
 */
public class Time {
    public void myCount() throws Exception {
        int i = 5;
        int j = 10;
        System.out.println(j - i);
    }

    public void myDeal() {
        try {
            int[] myInt = { 1, 2, 3, 4, 5 };
            int f = myInt[10];
            System.out.println(f);
        } catch (ArrayIndexOutOfBoundsException e) {
            e.printStackTrace();
        }
    }
}
 
      我們目標的class 字節碼如下:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package asm.core.methord;

public class Time {
    public static long timer;

    public Time() {
    }

    public void myCount() throws Exception {
        timer -= System.currentTimeMillis();
        byte i = 5;
        byte j = 10;
        System.out.println(j - i);
        timer += System.currentTimeMillis();
    }

    public void myDeal() {
        timer -= System.currentTimeMillis();

        try {
            int[] e = new int[]{1, 2, 3, 4, 5};
            int f = e[10];
            System.out.println(f);
        } catch (ArrayIndexOutOfBoundsException var3) {
            var3.printStackTrace();
        }

        timer += System.currentTimeMillis();
    }
}
    

      通過查看字節碼結構可以知道,首先我們需要增加一個field給Time類。然後在除了構造器以外的方法注入計時邏輯的字節碼。我們先以第一個方法myCount()爲例,用javap工具查看字節碼信息如下:

public void myCount() throws java.lang.Exception;
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=7, locals=3, args_size=1
         0: getstatic     #18                 // Field timer:J
         3: invokestatic  #24                 // Method java/lang/System.currentTimeMillis:()J
         6: lsub
         7: putstatic     #18                 // Field timer:J
        10: iconst_5
        11: istore_1
        12: bipush        10
        14: istore_2
        15: getstatic     #28                 // Field java/lang/System.out:Ljava/io/PrintStream;
        18: iload_2
        19: iload_1
        20: isub
        21: invokevirtual #34                 // Method java/io/PrintStream.println:(I)V
        24: getstatic     #18                 // Field timer:J
        27: invokestatic  #24                 // Method java/lang/System.currentTimeMillis:()J
        30: ladd
        31: putstatic     #18                 // Field timer:J
        34: return
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
           10      25     0  this   Lasm/core/methord/Time;
           12      23     1     i   I
           15      20     2     j   I
      LineNumberTable:
        line 8: 10
        line 9: 12
        line 10: 15
        line 11: 24
    Exceptions:
      throws java.lang.Exception
    

       從方法的偏移量0 到 7 是我們的  timer -=System.currentTimeMillis();對應的字節碼實現。24 到31 是timer += System.currentTimeMillis();的字節碼實現。基本可以判定,我們需要再方法剛進入的時候先生成timer -= System.currentTimeMillis();的字節碼,然後在方法返回return 指令或者是athrow指令之前生成timer+= System.currentTimeMillis()的字節碼。

      timer -=System.currentTimeMillis()我們可以通過visitCode(方法開始是通過此方法的調用)方法中添加ASM提供的字節碼指令生成的幾個方法來實現:

@Override
        public void visitCode() {
            mv.visitCode();
            mv.visitFieldInsn(Opcodes.GETSTATIC, owner, "timer", "J");
            mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
            mv.visitInsn(Opcodes.LSUB);
            mv.visitFieldInsn(Opcodes.PUTSTATIC, owner, "timer", "J");
        }

    

      timer +=System.currentTimeMillis()需要通過visitInsn(int opcode)方法來完成,遍歷所有的操作碼來判斷我們當前的指令是否是return 或者athrow 。如果是那麼前插入我們需要的指令,再繼續調用下一層mv.visitInsn(opcode)。代碼如下:

@Override
        public void visitInsn(int opcode) {
            if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN) || opcode == Opcodes.ATHROW) {
                mv.visitFieldInsn(Opcodes.GETSTATIC, owner, "timer", "J");
                mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
                mv.visitInsn(Opcodes.LADD);
                mv.visitFieldInsn(Opcodes.PUTSTATIC, owner, "timer", "J");
            }
            mv.visitInsn(opcode);
        }

        那麼最後還剩下,需要在class中生成一個timer的屬性,如前面ClassVisitor的介紹一樣,需要在ClassVisitor 的適配子類中的visitEnd()方法中插入我們的FieldVisitor。

@Override
    public void visitEnd() {
        if (!isInterface) {
            FieldVisitor fv = cv.visitField(Opcodes.ACC_PUBLIC + Opcodes.ACC_STATIC, "timer", "J", null, null);
            if (fv != null) {
                fv.visitEnd();
            }
        }
        cv.visitEnd();
    }

      至此,我們的字節碼已經創建和生成完畢,爲了健壯性考慮,我們只要再加上是否是Interface的判斷,因爲接口是沒有方法實現體的,並且還要判斷,構造器方法中不添加timer計時邏輯。這裏我們把需要注入邏輯的Class的name通過參數owner傳遞給MethodVisitor。整體Adapter方法如下:

package asm.core.methord;

import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

/**
 * Created by yunshen.ljy on 2015/6/29.
 */
public class AddTimerAdapter extends ClassVisitor {
    private String owner;
    private boolean isInterface;

    public AddTimerAdapter(ClassVisitor cv) {
        super(Opcodes.ASM4, 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);
        owner = name;
        isInterface = (access & Opcodes.ACC_INTERFACE) != 0;
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
        MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
        if (!isInterface && mv != null && !name.equals("<init>")) {
            mv = new AddTimerMethodAdapter(mv);
        }
        return mv;
    }

    @Override
    public void visitEnd() {
        if (!isInterface) {
            FieldVisitor fv = cv.visitField(Opcodes.ACC_PUBLIC + Opcodes.ACC_STATIC, "timer", "J", null, null);
            if (fv != null) {
                fv.visitEnd();
            }
        }
        cv.visitEnd();
    }

    class AddTimerMethodAdapter extends MethodVisitor {
        public AddTimerMethodAdapter(MethodVisitor mv) {
            super(Opcodes.ASM4, mv);
        }

        @Override
        public void visitCode() {
//            mv.visitCode();
            mv.visitFieldInsn(Opcodes.GETSTATIC, owner, "timer", "J");
            mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
            mv.visitInsn(Opcodes.LSUB);
            mv.visitFieldInsn(Opcodes.PUTSTATIC, owner, "timer", "J");
        }

        @Override
        public void visitInsn(int opcode) {
            if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN) || opcode == Opcodes.ATHROW) {
                mv.visitFieldInsn(Opcodes.GETSTATIC, owner, "timer", "J");
                mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
                mv.visitInsn(Opcodes.LADD);
                mv.visitFieldInsn(Opcodes.PUTSTATIC, owner, "timer", "J");
            }
            mv.visitInsn(opcode);
        }

        @Override
        public void visitMaxs(int maxStack, int maxLocals) {
            // 手動擋需要計算棧空間,這裏兩個long型變量的操作需要4個slot
            mv.visitMaxs(maxStack + 4, maxLocals);
        }

    }
}
    

二、有狀態注入

        這裏的有狀態是相對於無狀態來說的。剛纔的例子中是對於方法絕對偏移量的一種邏輯注入。簡單來說就是注入的邏輯不依賴前一個指令的操作或者指令的參數。對於Class文件的所有方法都是相同的邏輯注入。但是如果考慮一種情況,那就是當前需要注入的字節碼指令依賴於前面指令的執行結果狀態。那麼我們就必須存儲前面這個指令的狀態。

       下面這個例子來源於自官方文檔中的舉例。考慮如下方法:

public void myCount(){
        int i = 5;
        int j = 10;
        System.out.println(j - i);
        System.out.println(j + i);
        System.out.println(j + 0);
          }
      這裏我們知道j+0 或者j-0 的輸出結果都是j。那麼如果我們要讓上面的代碼去掉+0 以及-0這兩種操作,也就是需要變成如下的方法:

public void myCount(){
        byte i = 5;
        byte j = 10;
        System.out.println(j - i);
        System.out.println(j + i);
        System.out.println(j);
          }
        通過查看原方法的字節碼信息如下:


  0: iconst_5
         1: istore_1
         2: bipush        10
         4: istore_2
         5: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         8: iload_2
         9: iload_1
        10: isub
        11: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
        14: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        17: iload_2
        18: iload_1
        19: iadd
        20: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
        23: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        26: iload_2
        27: iconst_0
        28: iadd
        29: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
        32: return
        可以發現iadd 指令是iconst_0 的後置指令。但是我們不能單純得判斷當前字節碼指令時iadd或者iconst_0 就直接remove。當然remove的實現方式MethodVisitor 同ClassVisitor的適配器實現方式相近,都是通過不繼續調用mv.visitInsn(opcode);方法的方式。但這裏我們需要標記iconst_0指令的狀態。iconst_0指令執行時標記一個狀態,在下一條指令執行的時候判斷狀態值,如果下一條命令是iadd那麼就直接return掉方法來移除指令。官方實現非常的優雅,這裏加了一些註釋,方便理解實現。
  
package asm.core.methord;

import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

/**
 * Created by yunshen.ljy on 2015/7/1.
 */
public class RemoveAddZeroAdapter extends MethodVisitor {
    private static int SEEN_ICONST_0 = 1;
    protected final static int SEEN_NOTHING = 0;
    protected int state;
    public RemoveAddZeroAdapter(MethodVisitor mv) {
        super(Opcodes.ASM4, mv);
    }

    @Override
    public void visitInsn(int opcode) {
        // 是否檢測到前一個指令是ICONST_0
        if (state == SEEN_ICONST_0) {
            // 並且當前指令時iadd
            if (opcode == Opcodes.IADD) {
                // 重新初始化指令狀態
                state = SEEN_NOTHING;
                // 移除指令序列
                return;
            }
        }
        visitInsn();
        // 如果當前指令是ICONST_0 記錄指令狀態,並且直接返回(移除)
        if (opcode == Opcodes.ICONST_0) {
            state = SEEN_ICONST_0;
            return;
        }
        // 繼續訪問下一條指令
        mv.visitInsn(opcode);
    }

    protected void visitInsn() {
        // 如果最後訪問的是SEEN_ICONST_0指令,那麼還原指令(因爲剛纔被移除了)
        if (state == SEEN_ICONST_0) {
            mv.visitInsn(Opcodes.ICONST_0);
        }
        state = SEEN_NOTHING;
    }

}

      這裏再補充一下,我們不需要處理StacckMapFrame以及像前一部分需要計算局部變量表和操作數棧的size,那是因爲我們沒有增加額外的屬性,並且示例中也沒有無條件跳轉語句等,需要驗證的操作。但如果我們要實現更復雜的情況,還需要覆蓋visitMaxs方法、visitFrame visitLable方法等。(保證移除指令不會影響其他指令的正常跳轉,需要調用visitInsn()方法)

      其實我個人覺得,處理有狀態的字節碼指令移除、添加、轉移還是需要注意各種字節碼指令的情況。字節碼指令的順序,上下文,棧信息對於編寫一段健壯的ASM 邏輯注入代碼都非常關鍵。有時候其實還是建議先去把注入前後情況的class文件分析一遍,再進行編碼。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章