[トップ]ASM(二)Core APIを利用してクラスメンバーを変更する


前のページで簡単にASMフレームを紹介しました。この記事は引き続きCoreAppを拡張します。ここでは依然としてClass Writer、Class ReaderとClass Visitorへの応用の拡張を続けます。前の記事では主にClass WriterとClass Readerが単独で使うシーンを紹介しています。この2つをプロデュースとして、consumer(Class Writer)とconsumer(Class Writer)を組み合わせて、他の用途を紹介します。
  一、遷移クラス
    イベントの生産者クラスリーダーはaccept方法でクラスWriterに伝えられます。前の編では、Class WriterがClass Visitorから継承されていることを知っています。Class Readerは、Class Visitorの具体的な実装クラスを受信し、順次アクセスすることによってクラス全体のファイル構造を解析します。まずは例を見ます。簡単にするために、私たちは既存のclassファイルChildClassを読みます。そして解析を経て、Class Readerの例を得る。そして、Class Writerを通して、Classを再構築し、cw.toByteArayを通じて前と同じクラスのバイト配列を返します。
 
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 VisitorはClass Visitorのインスタンスを受信できます。Class WriterはVisitorのサブクラスとしてVisitorに呼び出されます。ASM公式文書の下の図は、コールチェーン全体をよく説明しています。この中でも、より多くのadapperを用いて順番に呼び出すことができます。
    ですから、私たちは固定化されたVisitorを作ることができます。Class Visitor cv=new Change Access Adapter(cw);この行はコメントを抜きにしてみます。ここでは自分のクラスVisitorを書いて、クラスの訪問修飾を修正します。public abstractをpublicに変えます。第一編の紹介によれば、私達は自分でvisit方法を実現し、訪問パラメータを設定する必要があります。Change Access Adapterコードは以下の通りです。
 
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()方法によって、私達はクラスのメンバーにアクセス、解析することができます。Inneraclass、OuterClassなどのクラスのメンバーを削除する必要がある場合は、直接にレスポンスを引き継ぐvisit OuterClass、visit Inners Class方法を通じて、メソッド体を実現しないで除去目的を達成することができます。MethodとFieldメンバーの除去は、次の階層の呼び出しを終了する必要があります。すなわち、MethodVisitorまたはFieldVisitorの例ではなく、nullに戻ります。例では除去が必要なクラスは、最初の編の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方法を削除します。同じように、自分のClass Visitorを実現する必要があります。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);
    }
}
    以下では、全体の呼び出しチェーンを構築し、削除したクラスのバイトストリームをファイルに出力します。  
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ファイルは次の私達が望むクラスファイルになりました。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");
    }
}
 三、クラスメンバーを追加する
   クラスのメンバーを追加して、私達は同様にクラスのVisitorを引き継いで私達自身のアダプターを書く必要があります。除去の場合、私たちはクラスのバイトフローを終了するためのエルゴードと呼び出しです。私たちは追加する時に、ヴィシトFieldまたはvisit Method方法を呼び出さなければなりません。ここで注意したいのは、単にvisitメソッドにFieldVisitorまたはMehttodVisitorのインスタンスを追加できないと、visit Fieldまたはvisit Methodを再起動することができないという点です。ASMは順番にclassバイナリバイトストリームを解析するので、visitメソッドの後にvisitSource、visit OuterClass、visit Attributeなどの方法をトリガします。visit Fieldまたはvisit Methodの方法で実装するのにも問題があります。visit Fieldメソッドを呼び出すたびに、多くの追加が必要なFieldが繰り返し発生します。
    この問題を解決するために、私達はvisit Endメソッドに実際にクラスメンバーを追加してもいいです。(visit Endメソッドはいつも呼び出されますので)、visit Fieldメソッドにクラスメンバーが存在しているかどうかを判断して、引き続き実行します。つまり、counterの方式によって、重複追加を防止します。私達は各新しい属性に一つのカウンタを追加してもいいです。一つのカウント方法を追加してそれぞれの方法で呼び出すこともできます。
   簡単な例を見ます。まず最初にadapperを書いてクラスメンバーを追加します。例では、私たちはプライベートなintタイプのFiledをTask.classに追加します。私達はcounterをvisit Fieldに書いて、この属性があるかどうかを判断します。もしないなら、一回のマーキングを行います。そしてvisit Endで構築します。
 
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();
    }
}
      visit End方法では、FieldVisitorのインスタンスが空かどうかを判断する必要があります。visit Field方法の実装において、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");
    }
}
     ここでは、より複雑な論理をカスタマイズするために、様々なadaterチェーンを呼び出すことができることを発見した。外層チェーンで呼び出すことができます。Class Visitor vca=new AClassVisitor;Class Visitor cvb=new BlassVisitor(cva)…。呼び出しチェーン配列を導入することによってAdalterにも送ることができる。ここで直接に公式説明文書の例を出してみます。MultiClass Adapterは私達のクラスVisitorの「総代理」です。
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);
        }
    }
}
 
 
四、工具アプリ    
    ASMのCore APIでは、いくつかのツールを提供してくれました。 org.objectweb.asm.utilカバンの中にあります。Trace Class Visitor、CheckClass Adapter、ASMifier、Typeなどがあります。これらのツールクラスを通じて、私たちの動的なバイトコードの生成ロジックをより容易にすることができます。ここではTrace Class Visitorを簡単に説明します。
   Trace Class Visitorは名前の通りに、私達は「trace」という情報を印刷します。これらの情報はClass Writerが提供してくれたbyteバイト配列です。バイナリバイトの流れを読むと、やはり一つのタイプのファイルの構造が分かりにくくなります。Trace Class Visitorは、一つのクラスWriterと一つのPrinterオブジェクトを初期化することによって、私たちが必要とするバイトフロー情報を印刷することができます。Trace Class Visitorを通じて、二つの種類のファイルをよりよく比較して、classのデータ構造をより簡単に分析することができます。
  例えば、私たちはTrace Class Visitorで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フレームのCoreAppiのベースクラスを紹介しました。CoreAppのMethodsインターフェースとコンポーネントを紹介します。及びTreeApp。Methodsクラスの前に、JVM中の運行期間の方法の呼び出しと実行を知っておく必要があります。ASMでどうやって動的な拡張ができるかをよりよく理解してくれます。