ASM(二) 利用Core API 變更類成員

      之前一篇簡單介紹了一下ASM框架。這一篇繼續對CoreApi進行擴展。這裏還是繼續對ClassWriter ,ClassReader和ClassVisitor的應用的擴展。前面一篇主要介紹的是ClassWriter和ClassReader單獨應用的場景。這一篇把這兩者作爲producer(ClassReader)和consumer(ClassWriter)來結合起來介紹一下另外一些用途。

  一、遷移轉換類

    事件的生產者ClassReader通過accept方法可以傳遞給ClassWriter。上一篇我們知道ClassWriter繼承自ClassVisitor。而ClassReader可以接收ClassVisitor具體實現類,通過順序訪問實現類的方法來解析整個class文件結構。先看個例子。爲了簡便,我們讀取一個現成的class文件ChildClass.class(前一篇用ASM生成的class,源碼見前一篇)。然後經過解析拿到一個ClassReader實例。然後再通過ClassWriter重新構造了一個Class ,通過cw.toByteArray()返回一個和前面一樣的Class 的字節數組。

 

package asm.core;
 
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
 
import java.io.*;
 
/**
 * Created by yunshen.ljy on 2015/6/9.
 */
public class TransformClasses {
 
    public static void main(String[] args) throws IOException {
        File file = new File("ChildClass.class");
        InputStream input = new FileInputStream(file);
        // 構造一個byte數組
        byte[] byt = new byte[input.available()];
        input.read(byt);
        ClassWriter cw = new ClassWriter(0);
        ClassVisitor cv = new ClassVisitor (cw){};
       //  改變class的訪問修飾
       //  ClassVisitor cv = new ChangeAccessAdapter(cw);
        ClassReader cr = new ClassReader(byt);
        cr.accept(cv, 0);
        byte[] toByte = cw.toByteArray();// byt 和toByte其實是相同的數組
        // 輸出到class文件
        File tofile = new File("ChildClass.class");
        FileOutputStream fout = new FileOutputStream(tofile);
        fout.write(toByte);
        fout.close();
 
    }
}

    光這樣解析然後構造一個相同的Class覺得沒什麼實際意義,但是我們注意到ClassVisitor 可以接收一個ClassVisitor 實例,而ClassWriter 作爲Visitor的子類,是可以被Visitor接收調用的。。ASM官方文檔的下面這張圖,很好地描述了整個調用鏈。而這其中也可以套用更多的adapter層層傳遞,順序調用。


    所以我們這裏可以創建一個定製化的Visitor。ClassVisitor cv = new ChangeAccessAdapter(cw);這行我們去掉註釋再看看,這裏我們寫了一個自己的ClassVisitor來修改class的訪問修飾。把public abstract變成public。根據第一篇的介紹,我們需要自己實現visit方法,並設置訪問參數。ChangeAccessAdapter 代碼如下:

 

package asm.core;
 
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.Opcodes;
 
/**
 * Created by yunshen.ljy on 2015/6/10.
 */
public class ChangeAccessAdapter extends ClassVisitor {
 
    public ChangeAccessAdapter(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, Opcodes.ACC_PUBLIC , name, signature, superName, interfaces);
    }
 
}

  二、移除類成員

         通過visit()方法,我們可以訪問、解析類成員。當我們需要移除一個類成員,比如InnerClass、OuterClass就可以直接通過繼承響應的visitOuterClass、visitInnerClass方法,但是不去實現方法體來達到移除目的。Method和Field成員的移除需要終止下一層繼續調用,也就是返回null 而不是MethodVisitor 或者FieldVisitor實例。例子中需要移除的Class 還是以第一篇的Task 類爲例。這次我們加入了一個內部類給Task。代碼如下:


package asm.core;
 
/**
 * Created by yunshen.ljy on 2015/6/8.
 */
public class Task {
 
    private int isTask = 0;
 
    private long tell = 0;
 
    public void isTask(boolean test){
        System.out.println("call isTask");
    }
    public void tellMe(){
        System.out.println("call tellMe");
    }
 
    class TaskInner{
        int inner;
    }
}


   我們這次把Task的內部類以及 isTask方法移除,一樣,需要實現自己的ClassVisitor,Visitor 代碼如下。 


package asm.core;
 
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
 
/**
 * Created by yunshen.ljy on 2015/6/12.
 */
public class RemovingClassesVisitor extends ClassVisitor{
 
    public RemovingClassesVisitor(int api) {
        super(api);
    }
 
    public RemovingClassesVisitor(ClassWriter cw) {
        super(Opcodes.ASM4,cw);
    }
 
    // 移除內部類
    @Override
    public void visitInnerClass(String name, String outerName, String innerName, int access) {
 
    }
 
    @Override
    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
        if (name.startsWith("is")) {
            // 移除以is開頭的方法名的方法
            return null;
        }
        return cv.visitMethod(access, name, desc, signature, exceptions);
    }
}

    下面就來構造整個調用鏈,將移除後的class字節流輸出到文件中:  

package asm.core;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
 
public class RemovingClassesTest {
    public static void main(String[] args) throws IOException {
        ClassReader cr = new ClassReader("asm.core.Task");
        ClassWriter cw = new ClassWriter(0);
        ClassVisitor cv = new RemovingClassesVisitor(cw);
        cr.accept(cv, 0);
        byte[] toByte = cw.toByteArray();// byt 和toByte其實是相同的數組
        // 輸出到class文件
        File file = new File("Task.class");
        FileOutputStream fout = new FileOutputStream(file);
        fout.write(toByte);
        fout.close();
    }
 
}

  然後,Task.class 文件就變成了下面我們期望的class文件。isTask()方法已經被移除。

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
 
package asm.core;
 
public class Task {
    private int isTask = 0;
    private long tell = 0L;
 
    public Task() {
    }
 
    public void tellMe() {
        System.out.println("call tellMe");
    }
}


 三、添加類成員

   添加類成員,我們一樣需要繼承ClassVisitor 來寫我們自己的適配器。移除的情況,我們是終止class字節流的遍歷和調用。那麼添加的時候我們就需要去多調用一次visitField或者visitMethod方法。但這裏我們需要注意的一點是,如果我們無法單純在visit方法中去添加一個FieldVisitor或MehtodVisitor實例來實現再次調用visitField或者visitMethod。因爲ASM是按照順序來解析class二進制字節流的,visit方法後續還會再次觸發visitSource, visitOuterClass, visitAttribute,等方法。那麼實現在visitField或者visitMethod方法中也會有問題,因爲比如每次調用visitField方法,會重複產生很多你需要添加的Field。

    爲了解決這個問題,我們可以在visitEnd方法中去實際添加類成員(因爲visitEnd方法總是會被調用到),在visitField方法中加入判斷是否已經存在類成員,再繼續往下執行。也就是通過counter的方式,防止重複添加,我們可以在每個新加的屬性上加一個counter,也可以添加一個計數方法分別在每個方法中調用。

   下面看一個簡單的例子。首先先寫一個adapter 來添加類成員。例子中我們添加一個私有的int類型的Filed 到Task.class中。我們把counter寫在visitField中,判斷是否已經有這個屬性,如果沒有,進行一次標記。然後在visitEnd中去構建。

 

package asm.core;
 
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.Opcodes;
 
/**
 * Created by yunshen.ljy on 2015/6/13.
 */
public class AddingClassesVisitor  extends ClassVisitor {
 
 
    private int fAcc;
    private String fName;
    private String fDesc;
    private boolean isFieldPresent;
    public AddingClassesVisitor(ClassVisitor cv, int fAcc, String fName,
                           String fDesc) {
        super(Opcodes.ASM4, cv);
        this.fAcc = fAcc;
        this.fName = fName;
        this.fDesc = fDesc;
    }
    @Override
    public FieldVisitor visitField(int access, String name, String desc,
                                   String signature, Object value) {
        if (name.equals(fName)) {
            isFieldPresent = true;
        }
        return cv.visitField(access, name, desc, signature, value);
    }
    @Override
    public void visitEnd() {
        if (!isFieldPresent) {
            FieldVisitor fv = cv.visitField(fAcc, fName, fDesc, null, null);
            if (fv != null) {
                fv.visitEnd();
            }
        }
        cv.visitEnd();
    }
}


      在visitEnd方法中我們需要判斷FieldVisitor實例是否爲空,因爲visitField方法的實現中,是會有返回null的情況。

      調用的代碼中,只要把前面的Test類替換成如下的調用就可以了


     ClassReader cr = new ClassReader("asm.core.Task");
        ClassWriter cw = new ClassWriter(0);
        ClassVisitor cv = new AddingClassesVisitor(cw, Opcodes.ACC_PRIVATE,"addedField","I");
        cr.accept(cv, 0);

   再來看一下這次生成的Task.class 已經添加了我們期望的類成員。

 

package asm.core;
 
public class Task {
    private int isTask = 0;
    private long tell = 0L;
    private int addedField;
 
    public Task() {
    }
 
    public void isTask(boolean test) {
        System.out.println("call isTask");
    }
 
    public void tellMe() {
        System.out.println("call tellMe");
    }
}


     這裏我們發現,可以把各種adapter鏈式調用,來實現複雜的調用鏈,定製更加複雜的邏輯。我們可以在外層鏈式調用,ClassVisitor vca = new AClassVisitor(classWriter);ClassVisitor cvb= new BClassVisitor(cva)…。也可以通過傳入一個調用鏈數組給一個Adalter。這裏直接把官方說明文檔的例子拿出來看下MultiClassAdapter 就是我們的ClassVisitor 的“總代理”:

package asm.core;
 
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.Opcodes;
 
 
public class MultiClassAdapter extends ClassVisitor {
    protected ClassVisitor[] cvs;
    public MultiClassAdapter(ClassVisitor[] cvs) {
        super(Opcodes.ASM4);
        this.cvs = cvs;
    }
    @Override public void visit(int version, int access, String name,
                                String signature, String superName, String[] interfaces) {
        for (ClassVisitor cv : cvs) {
            cv.visit(version, access, name, signature, superName, interfaces);
        }
    }
}
 

 

四、工具Api    

    ASM的Core API 中給我們提供了一些工具類,都在 org.objectweb.asm.util包中。有TraceClassVisitor、CheckClassAdapter、ASMifier、Type等。通過這些工具類,能更方便實現我們的動態生成字節碼邏輯。這裏就簡述一下TraceClassVisitor 。

   TraceClassVisitor 顧名思義,我們可以“trace”也就是打印一些信息,這些信息就是ClassWriter 提供給我們的byte字節數組。因爲我們閱讀一個二進制字節流還是比較難以理解和解析一個類文件的結構。TraceClassVisitor通過初始化一個classWriter 和一個Printer對象,來實現打印我們需要的字節流信息。通過TraceClassVisitor 我們能更好地比較兩個類文件,更輕鬆得分析class的數據結構。

  下面看個例子,我們用TraceClassVisitor 來打印Task 類信息。

package asm.core;
 
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.util.TraceClassVisitor;
 
import java.io.IOException;
import java.io.PrintWriter;
 
/**
 * Created by yunshen.ljy on 2015/6/13.
 */
public class TraceClassVisitorTest {
 
    public static void main(String[] args) throws IOException {
        ClassReader cr = new ClassReader("asm.core.Task");
        ClassWriter cw = new ClassWriter(0);
        TraceClassVisitor cv = new TraceClassVisitor(cw, new PrintWriter(System.out));
        cr.accept(cv, 0);
    }
}

 控制檯的結果如下,Task的類的局部變量表、操作數棧的一些信息也能打印出來,這比看二進制字節碼文件舒服多了。

// class version 50.0 (50)
// access flags 0x21
public class asm/core/Task {
 
  // compiled from: Task.java
  // access flags 0x0
  INNERCLASS asm/core/Task$TaskInner asm/core/Task TaskInner
 
  // access flags 0x2
  private I isTask
 
  // access flags 0x2
  private J tell
 
  // access flags 0x1
  public <init>()V
   L0
    LINENUMBER 6 L0
    ALOAD 0
    INVOKESPECIAL java/lang/Object.<init> ()V
   L1
    LINENUMBER 8 L1
    ALOAD 0
    ICONST_0
    PUTFIELD asm/core/Task.isTask : I
   L2
    LINENUMBER 10 L2
    ALOAD 0
    LCONST_0
    PUTFIELD asm/core/Task.tell : J
   L3
    LINENUMBER 19 L3
    RETURN
   L4
    LOCALVARIABLE this Lasm/core/Task; L0 L4 0
    MAXSTACK = 3
    MAXLOCALS = 1
 
  // access flags 0x1
  public isTask(Z)V
   L0
    LINENUMBER 13 L0
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    LDC "call isTask"
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
   L1
    LINENUMBER 14 L1
    RETURN
   L2
    LOCALVARIABLE this Lasm/core/Task; L0 L2 0
    LOCALVARIABLE test Z L0 L2 1
    MAXSTACK = 2
    MAXLOCALS = 2
 
  // access flags 0x1
  public tellMe()V
   L0
    LINENUMBER 16 L0
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    LDC "call tellMe"
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
   L1
    LINENUMBER 17 L1
    RETURN
   L2
    LOCALVARIABLE this Lasm/core/Task; L0 L2 0
    MAXSTACK = 2
    MAXLOCALS = 1
}

      ASM框架的CoreApi的基礎類已經介紹完畢。後面會陸續介紹CoreApi 中的Methods接口和組件。以及TreeApi。在Methods 類庫之前,需要先了解下JVM中的運行期方法調用和執行,能幫助我們更好地理解怎麼樣用ASM實現動態擴展。

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