ASM(一)Core APIによるバイトコードの解析と生成

7301 ワード


     ASMはバイトコードの解析と操作を提供するフレームワークである.CglibフレームワークはASMフレームワークに基づいて実現され,広く応用されているHibernate,SpringはCglibに基づいてAOP技術を実現している.
    AOPのJava実装といえばjavaのProxy apiを優先してinvokeメソッドで対応するコードロジックをブロック処理するかもしれませんが、proxyはインタフェース向けで、エージェントされたclassのすべてのメソッド呼び出しは反射的にinvokeメソッドを呼び出し、相対的にパフォーマンスオーバーヘッドが大きいです.またJava 5が提供するInstrumentもあり,モニタリングチェックには比較的適しているが,柔軟なコードロジックの処理には適していない.
    ASMフレームワークはユーザーにクラス全体のバイトコードの長さ、オフセット量を遮蔽し、バイトコードの解析と操作をより柔軟かつ便利に実現することができる.主に2つの主要なAPI,Core ApiおよびTree Apiを提供している.本稿ではまずCore Apiの解析とバイトコードの生成から紹介する.
   CoreApiはここで解析と生成を行い,主にクラスをClassVisitorとする.ClassVisitorは2.0バージョンではインタフェースであり、classファイル操作ではClassAdepterにアクセスします.ここでは主に4.0以降のAPIに基づいて紹介する.
   まずClassVisitorコードを見てみましょう.
public abstract class ClassVisitor {
public ClassVisitor(int api);
public ClassVisitor(int api, ClassVisitor cv);
public void visit(int version, int access, String name,
String signature, String superName, String[] interfaces);
public void visitSource(String source, String debug);
public void visitOuterClass(String owner, String name, String desc);
AnnotationVisitor visitAnnotation(String desc, boolean visible);
public void visitAttribute(Attribute attr);
public void visitInnerClass(String name, String outerName,
String innerName, int access);
public FieldVisitor visitField(int access, String name, String desc,
String signature, Object value);
public MethodVisitor visitMethod(int access, String name, String desc,
String signature, String[] exceptions);
void visitEnd();
}

    ClassVisitorの呼び出しは、次の呼び出し順序に従う必要があります.
  visit visitSource? visitOuterClass? ( visitAnnotation | visitAttribute )*
( visitInnerClass | visitField | visitMethod )*
visitEnd

   ClassVisitorの周りには、2つのコアクラスがあります.   後続の例コードでは、classはバイトストリームのバイナリファイルであり、解析と生成も一定の順序に従うため、visitメソッドを呼び出さなければならないことがわかります.ClassVisitorは私たちが操作する必要があるすべてのインタフェースを定義し、ClassVisitorもClassVisitorのインスタンスを受信して構築することができ、イベントのfilterに似ていて、多層のfilterを組み合わせて論理を処理することができます.
1、ClassReaderはclassをbyte配列に解析し、acceptメソッドを使用してバインドオブジェクト(ClassVisitorのインスタンスを継承)のメソッドを順番に呼び出します.イベントの生産者と見なすことができます.
2、ClassWriterはClassVisitorのサブクラスです.コンパイルされたclassは、toByteArray()メソッドによって返されるbyte配列の形式で直接構築できます.一つの事件と見なすことができる消費者.
   具体的な3つのApiの応用を見てみましょう.
一、解析
    Javaコンパイル後のclassファイルは、特定のフォーマットのバイトストリームからなるバイナリファイルであるためです.ここではclassファイル構造の説明を展開しません.では、ASMのClassReaderクラスを使用して、クラスのメソッド、プロパティ、注釈、親およびインタフェース情報、内部クラスなどを解析できます.次の例では、主に印刷クラスのプロパティ、メソッド情報(javapツール類似機能)を例示します.
 
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(){
        System.out.println("call isTask");
    }
    public void tellMe(){
        System.out.println("call tellMe");
    }
}

ClassPrintVisitorクラスはClassVisitorクラスから継承され、解析クラスのクラス名、親クラス名、および「is」の先頭にある属性およびメソッドを印刷します. 
package asm.core;
 
import org.objectweb.asm.*;
 
/**
 * Created by yunshen.ljy on 2015/6/8.
 */
public class ClassPrintVisitor extends ClassVisitor {
 
 
    public ClassPrintVisitor() {
        super(Opcodes.ASM4);
    }
 
    public void visit(int version, int access, String name,
                      String signature, String superName, String[] interfaces) {
        System.out.println(name + " extends " + superName + " {");
    }
 
    public FieldVisitor visitField(int access, String name, String desc,
                                   String signature, Object value) {
        if (name.startsWith("is")) {
            System.out.println(" field name: " + name + desc);
        }
        return null;
    }
 
    public MethodVisitor visitMethod(int access, String name,
                                     String desc, String signature, String[] exceptions) {
        if (name.startsWith("is")) {
            System.out.println(" start with is method: " + name + desc);
        }
        return null;
    }
 
    public void visitEnd() {
        System.out.println("}");
    }
}

次はテストクラスClassesPrintTestです.ClassPrintVisitorオブジェクトをClassReaderに渡します.ClassReaderは解析イベントのproducerとして機能し、ClassPrintVisitorによって消費される(印刷ロジックの処理).accept()メソッドはTaskバイトコードを解析し,ClassPrintVisitorメソッドを呼び出す. 
package asm.core;
 
import org.objectweb.asm.ClassReader;
 
import java.io.IOException;
 
/**
 * Created by yunshen.ljy on 2015/6/8.
 */
public class ClassesPrintTest {
 
    public static void main(String[] args) {
        try {
            ClassReader cr = new ClassReader("asm.core.Task");
            ClassPrintVisitor cp = new ClassPrintVisitor();
            cr.accept(cp, 0);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

呼び出し結果:
asm/core/Task extends java/lang/Object {
 field name: isTaskI
 start with is method: isTask()V
}

    ここではisの先頭のプロパティ名と説明,メソッド名および説明を印刷した.ここでdescは、classファイル属性修飾またはメソッドパラメータ、戻り値の全制限名(fully qualified name)である.ここでisTaskの後のIはintタイプの記述を表す.IsTask()メソッドの後のVは,戻り値がvoidであることを示す.詳細なプロパティ、メソッドの説明は、『The Java Virtual Machine Specification』を参照してください.
   
二、生成
         バイトコードを生成して、少し暴力的に聞こえます.Javaコンパイル後のclassファイルはバイトストリームとして構成されています.すなわちbyte配列を生成し、前述したようにJVMによってロード、実行可能なバイトストリームを「特定のフォーマット」で生成する.次の例はクラスChildClassを生成し、ParentInterインタフェースから継承します.これは空のインタフェースです(具体的な変数と方法はなく、コードを例示するためだけです).ASMのClassWriterで実現します.まずChildClassクラスのコードを見てみましょう.
package asm.core;
 
import asm.core.ParentInter;
 
public abstract class ChildClass implements ParentInter {
    public static final int zero = 0;
 
    public abstract int compareTo(Object var1);
}

  ClassVisitorを呼び出すことで、上記のコードを生成することができます. 
private static byte[] gen() {
        ClassWriter cw = new ClassWriter(0);
        cw.visit(Opcodes.V1_5, Opcodes.ACC_PUBLIC + Opcodes.ACC_ABSTRACT ,
                "asm/core/ChildClass", null, "java/lang/Object", new String[]{"asm/core/ParentInter"});
      
 cw.visitField(Opcodes.ACC_PUBLIC + Opcodes.ACC_FINAL + Opcodes.ACC_STATIC, "zero", "I", null, new Integer(0))
                .visitEnd();
     
  cw.visitMethod(Opcodes.ACC_PUBLIC + Opcodes.ACC_ABSTRACT, "compareTo", "(Ljava/lang/Object;)I", null, null)
                .visitEnd();
        cw.visitEnd();
        return cw.toByteArray();
    }

     classファイルを表すバイト配列を生成するには、visitメソッドを呼び出してclassのヘッダ情報を生成する必要があります.ここでのいくつかのパラメータは少し説明します.最初のパラメータOpcodeはASM内部で定義されたオペランド定数で、ここでのV 1_5はJava 1.5を表し、2番目のパラメータはクラスの修飾子を表し、ここでは抽象クラスである.3つ目はクラス名です.4番目のパラメータは、ここではタイプパラメータがないためnullを表します.後ろの2つは親とインタフェースです.visitField()メソッドとvisitMethod()メソッドはそれぞれ私たちの属性とメソッドを生成します.ここではvisitEndメソッドが呼び出されます.ここでfieldの後続には対応するvisitAnnotation、visitAttributeなどのメソッド呼び出しがなく、methodの後続にも他の呼び出しがないからです.最後にcw.visitEnd()を呼び出して作成プロセス全体を終了し、toByteArray()はこのクラスを表すbyte配列を返します.     ここではnewのClassWriterです.ClassWriterはClassVisitorから継承されます.先にClassVisitorについて説明しました.ここでClassWriterのtoByteArray()が返すバイト配列は、生成したclassを表すことができます.
    このbyte配列でクラスをClassLoaderでロードしたり、FileOutputStreamでclassファイルを生成したりすることができます.